Fix 1592 empty source on error (#1792)
This commit is contained in:
@@ -0,0 +1,378 @@
|
||||
/*
|
||||
* Copyright 2013-2024 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.client.config;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import com.github.tomakehurst.wiremock.WireMockServer;
|
||||
import com.github.tomakehurst.wiremock.client.WireMock;
|
||||
import io.kubernetes.client.openapi.ApiClient;
|
||||
import io.kubernetes.client.openapi.Configuration;
|
||||
import io.kubernetes.client.openapi.JSON;
|
||||
import io.kubernetes.client.openapi.apis.CoreV1Api;
|
||||
import io.kubernetes.client.openapi.models.V1ConfigMapBuilder;
|
||||
import io.kubernetes.client.openapi.models.V1ConfigMapList;
|
||||
import io.kubernetes.client.openapi.models.V1ObjectMetaBuilder;
|
||||
import io.kubernetes.client.util.ClientBuilder;
|
||||
import org.junit.jupiter.api.AfterAll;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
|
||||
import org.springframework.boot.test.system.CapturedOutput;
|
||||
import org.springframework.boot.test.system.OutputCaptureExtension;
|
||||
import org.springframework.cloud.kubernetes.commons.KubernetesNamespaceProvider;
|
||||
import org.springframework.cloud.kubernetes.commons.config.ConfigMapConfigProperties;
|
||||
import org.springframework.cloud.kubernetes.commons.config.Constants;
|
||||
import org.springframework.cloud.kubernetes.commons.config.RetryProperties;
|
||||
import org.springframework.core.env.CompositePropertySource;
|
||||
import org.springframework.core.env.MapPropertySource;
|
||||
import org.springframework.core.env.PropertySource;
|
||||
import org.springframework.mock.env.MockEnvironment;
|
||||
|
||||
import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
|
||||
import static com.github.tomakehurst.wiremock.client.WireMock.get;
|
||||
import static com.github.tomakehurst.wiremock.client.WireMock.stubFor;
|
||||
import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.options;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* @author wind57
|
||||
*/
|
||||
@ExtendWith(OutputCaptureExtension.class)
|
||||
class KubernetesClientConfigMapErrorOnReadingSourceTests {
|
||||
|
||||
private static final V1ConfigMapList SINGLE_CONFIGMAP_LIST = new V1ConfigMapList()
|
||||
.addItemsItem(new V1ConfigMapBuilder()
|
||||
.withMetadata(
|
||||
new V1ObjectMetaBuilder().withName("two").withNamespace("default").withResourceVersion("1").build())
|
||||
.build());
|
||||
|
||||
private static final V1ConfigMapList DOUBLE_CONFIGMAP_LIST = new V1ConfigMapList()
|
||||
.addItemsItem(new V1ConfigMapBuilder()
|
||||
.withMetadata(
|
||||
new V1ObjectMetaBuilder().withName("one").withNamespace("default").withResourceVersion("1").build())
|
||||
.build())
|
||||
.addItemsItem(new V1ConfigMapBuilder()
|
||||
.withMetadata(
|
||||
new V1ObjectMetaBuilder().withName("two").withNamespace("default").withResourceVersion("1").build())
|
||||
.build());
|
||||
|
||||
private static WireMockServer wireMockServer;
|
||||
|
||||
@BeforeAll
|
||||
public static void setup() {
|
||||
wireMockServer = new WireMockServer(options().dynamicPort());
|
||||
|
||||
wireMockServer.start();
|
||||
WireMock.configureFor("localhost", wireMockServer.port());
|
||||
|
||||
ApiClient client = new ClientBuilder().setBasePath("http://localhost:" + wireMockServer.port()).build();
|
||||
client.setDebugging(true);
|
||||
Configuration.setDefaultApiClient(client);
|
||||
}
|
||||
|
||||
@AfterAll
|
||||
public static void after() {
|
||||
wireMockServer.stop();
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
public void afterEach() {
|
||||
WireMock.reset();
|
||||
}
|
||||
|
||||
/**
|
||||
* <pre>
|
||||
* we try to read all config maps in a namespace and fail,
|
||||
* thus generate a well defined name for the source.
|
||||
* </pre>
|
||||
*/
|
||||
@Test
|
||||
void namedSingleConfigMapFails() {
|
||||
String name = "my-config";
|
||||
String namespace = "spring-k8s";
|
||||
String path = "/api/v1/namespaces/" + namespace + "/configmaps";
|
||||
|
||||
stubFor(get(path).willReturn(aResponse().withStatus(500).withBody("Internal Server Error")));
|
||||
|
||||
ConfigMapConfigProperties configMapConfigProperties = new ConfigMapConfigProperties(true, List.of(), List.of(),
|
||||
Map.of(), true, name, namespace, false, true, false, RetryProperties.DEFAULT);
|
||||
|
||||
CoreV1Api api = new CoreV1Api();
|
||||
KubernetesClientConfigMapPropertySourceLocator locator = new KubernetesClientConfigMapPropertySourceLocator(api,
|
||||
configMapConfigProperties, new KubernetesNamespaceProvider(new MockEnvironment()));
|
||||
|
||||
CompositePropertySource propertySource = (CompositePropertySource) locator.locate(new MockEnvironment());
|
||||
MapPropertySource mapPropertySource = (MapPropertySource) propertySource.getPropertySources()
|
||||
.stream()
|
||||
.findAny()
|
||||
.orElseThrow();
|
||||
|
||||
assertThat(mapPropertySource.getName()).isEqualTo("configmap..spring-k8s");
|
||||
assertThat(propertySource.getProperty(Constants.ERROR_PROPERTY)).isEqualTo("true");
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* <pre>
|
||||
* there are two sources and we try to read them.
|
||||
* one fails and one passes.
|
||||
* </pre>
|
||||
*/
|
||||
@Test
|
||||
void namedTwoConfigMapsOneFails(CapturedOutput output) {
|
||||
String configMapNameOne = "one";
|
||||
String configMapNameTwo = "two";
|
||||
String namespace = "default";
|
||||
String path = "/api/v1/namespaces/default/configmaps";
|
||||
|
||||
stubFor(get(path).willReturn(aResponse().withStatus(500).withBody("Internal Server Error"))
|
||||
.inScenario("started")
|
||||
.willSetStateTo("go-to-next"));
|
||||
|
||||
stubFor(get(path).willReturn(aResponse().withStatus(200).withBody(new JSON().serialize(SINGLE_CONFIGMAP_LIST)))
|
||||
.inScenario("started")
|
||||
.whenScenarioStateIs("go-to-next")
|
||||
.willSetStateTo("done"));
|
||||
|
||||
ConfigMapConfigProperties.Source sourceOne = new ConfigMapConfigProperties.Source(configMapNameOne, namespace,
|
||||
Map.of(), null, null, null);
|
||||
ConfigMapConfigProperties.Source sourceTwo = new ConfigMapConfigProperties.Source(configMapNameTwo, namespace,
|
||||
Map.of(), null, null, null);
|
||||
|
||||
ConfigMapConfigProperties configMapConfigProperties = new ConfigMapConfigProperties(true, List.of(),
|
||||
List.of(sourceOne, sourceTwo), Map.of(), true, null, namespace, false, true, false,
|
||||
RetryProperties.DEFAULT);
|
||||
|
||||
CoreV1Api api = new CoreV1Api();
|
||||
KubernetesClientConfigMapPropertySourceLocator locator = new KubernetesClientConfigMapPropertySourceLocator(api,
|
||||
configMapConfigProperties, new KubernetesNamespaceProvider(new MockEnvironment()));
|
||||
|
||||
CompositePropertySource propertySource = (CompositePropertySource) locator.locate(new MockEnvironment());
|
||||
List<String> names = propertySource.getPropertySources().stream().map(PropertySource::getName).toList();
|
||||
|
||||
// two sources are present, one being empty
|
||||
assertThat(names).containsExactly("configmap.two.default", "configmap..default");
|
||||
assertThat(propertySource.getProperty(Constants.ERROR_PROPERTY)).isEqualTo("true");
|
||||
assertThat(output.getOut())
|
||||
.doesNotContain("sourceName : two was requested, but not found in namespace : default");
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* <pre>
|
||||
* there are two sources and we try to read them.
|
||||
* both fail.
|
||||
* </pre>
|
||||
*/
|
||||
@Test
|
||||
void namedTwoConfigMapsBothFail(CapturedOutput output) {
|
||||
String configMapNameOne = "one";
|
||||
String configMapNameTwo = "two";
|
||||
String namespace = "default";
|
||||
String path = "/api/v1/namespaces/default/configmaps";
|
||||
|
||||
stubFor(get(path).willReturn(aResponse().withStatus(500).withBody("Internal Server Error"))
|
||||
.inScenario("started")
|
||||
.willSetStateTo("go-to-next"));
|
||||
|
||||
stubFor(get(path).willReturn(aResponse().withStatus(500).withBody("Internal Server Error"))
|
||||
.inScenario("started")
|
||||
.whenScenarioStateIs("go-to-next")
|
||||
.willSetStateTo("done"));
|
||||
|
||||
ConfigMapConfigProperties.Source sourceOne = new ConfigMapConfigProperties.Source(configMapNameOne, namespace,
|
||||
Map.of(), null, null, null);
|
||||
ConfigMapConfigProperties.Source sourceTwo = new ConfigMapConfigProperties.Source(configMapNameTwo, namespace,
|
||||
Map.of(), null, null, null);
|
||||
|
||||
ConfigMapConfigProperties configMapConfigProperties = new ConfigMapConfigProperties(true, List.of(),
|
||||
List.of(sourceOne, sourceTwo), Map.of(), true, null, namespace, false, true, false,
|
||||
RetryProperties.DEFAULT);
|
||||
|
||||
CoreV1Api api = new CoreV1Api();
|
||||
KubernetesClientConfigMapPropertySourceLocator locator = new KubernetesClientConfigMapPropertySourceLocator(api,
|
||||
configMapConfigProperties, new KubernetesNamespaceProvider(new MockEnvironment()));
|
||||
|
||||
CompositePropertySource propertySource = (CompositePropertySource) locator.locate(new MockEnvironment());
|
||||
List<String> names = propertySource.getPropertySources().stream().map(PropertySource::getName).toList();
|
||||
|
||||
assertThat(names).containsExactly("configmap..default");
|
||||
assertThat(propertySource.getProperty(Constants.ERROR_PROPERTY)).isEqualTo("true");
|
||||
assertThat(output.getOut())
|
||||
.doesNotContain("sourceName : one was requested, but not found in namespace : default");
|
||||
assertThat(output.getOut())
|
||||
.doesNotContain("sourceName : two was requested, but not found in namespace : default");
|
||||
}
|
||||
|
||||
/**
|
||||
* <pre>
|
||||
* we try to read all config maps in a namespace and fail,
|
||||
* thus generate a well defined name for the source.
|
||||
* </pre>
|
||||
*/
|
||||
@Test
|
||||
void labeledSingleConfigMapFails(CapturedOutput output) {
|
||||
Map<String, String> labels = Map.of("a", "b");
|
||||
String namespace = "spring-k8s";
|
||||
String path = "/api/v1/namespaces/" + namespace + "/configmaps";
|
||||
|
||||
// one for the 'application' named configmap
|
||||
// the other for the labeled config map
|
||||
stubFor(get(path).willReturn(aResponse().withStatus(500).withBody("Internal Server Error"))
|
||||
.inScenario("started")
|
||||
.willSetStateTo("go-to-next"));
|
||||
|
||||
stubFor(get(path).willReturn(aResponse().withStatus(500).withBody("Internal Server Error"))
|
||||
.inScenario("started")
|
||||
.whenScenarioStateIs("go-to-next")
|
||||
.willSetStateTo("done"));
|
||||
|
||||
ConfigMapConfigProperties.Source configMapSource = new ConfigMapConfigProperties.Source(null, namespace, labels,
|
||||
null, null, null);
|
||||
|
||||
ConfigMapConfigProperties configMapConfigProperties = new ConfigMapConfigProperties(true, List.of(),
|
||||
List.of(configMapSource), labels, true, null, namespace, false, true, false, RetryProperties.DEFAULT);
|
||||
|
||||
CoreV1Api api = new CoreV1Api();
|
||||
KubernetesClientConfigMapPropertySourceLocator locator = new KubernetesClientConfigMapPropertySourceLocator(api,
|
||||
configMapConfigProperties, new KubernetesNamespaceProvider(new MockEnvironment()));
|
||||
|
||||
CompositePropertySource propertySource = (CompositePropertySource) locator.locate(new MockEnvironment());
|
||||
List<String> sourceNames = propertySource.getPropertySources().stream().map(PropertySource::getName).toList();
|
||||
|
||||
assertThat(sourceNames).containsExactly("configmap..spring-k8s");
|
||||
assertThat(propertySource.getProperty(Constants.ERROR_PROPERTY)).isEqualTo("true");
|
||||
assertThat(output).contains("failure in reading labeled sources");
|
||||
assertThat(output).contains("failure in reading named sources");
|
||||
}
|
||||
|
||||
/**
|
||||
* <pre>
|
||||
* there are two sources and we try to read them.
|
||||
* one fails and one passes.
|
||||
* </pre>
|
||||
*/
|
||||
@Test
|
||||
void labeledTwoConfigMapsOneFails(CapturedOutput output) {
|
||||
|
||||
Map<String, String> configMapOneLabels = Map.of("one", "1");
|
||||
Map<String, String> configMapTwoLabels = Map.of("two", "2");
|
||||
|
||||
String namespace = "default";
|
||||
String path = "/api/v1/namespaces/default/configmaps";
|
||||
|
||||
// one for 'application' named configmap and one for the first labeled configmap
|
||||
stubFor(get(path).willReturn(aResponse().withStatus(500).withBody("Internal Server Error"))
|
||||
.inScenario("started")
|
||||
.willSetStateTo("first"));
|
||||
|
||||
stubFor(get(path).willReturn(aResponse().withStatus(500).withBody("Internal Server Error"))
|
||||
.inScenario("started")
|
||||
.whenScenarioStateIs("first")
|
||||
.willSetStateTo("second"));
|
||||
|
||||
// one that passes
|
||||
stubFor(get(path).willReturn(aResponse().withStatus(200).withBody(new JSON().serialize(DOUBLE_CONFIGMAP_LIST)))
|
||||
.inScenario("started")
|
||||
.whenScenarioStateIs("second")
|
||||
.willSetStateTo("done"));
|
||||
|
||||
ConfigMapConfigProperties.Source sourceOne = new ConfigMapConfigProperties.Source(null, namespace,
|
||||
configMapOneLabels, null, null, null);
|
||||
ConfigMapConfigProperties.Source sourceTwo = new ConfigMapConfigProperties.Source(null, namespace,
|
||||
configMapTwoLabels, null, null, null);
|
||||
|
||||
ConfigMapConfigProperties configMapConfigProperties = new ConfigMapConfigProperties(true, List.of(),
|
||||
List.of(sourceOne, sourceTwo), Map.of("one", "1", "two", "2"), true, null, namespace, false, true,
|
||||
false, RetryProperties.DEFAULT);
|
||||
|
||||
CoreV1Api api = new CoreV1Api();
|
||||
KubernetesClientConfigMapPropertySourceLocator locator = new KubernetesClientConfigMapPropertySourceLocator(api,
|
||||
configMapConfigProperties, new KubernetesNamespaceProvider(new MockEnvironment()));
|
||||
|
||||
CompositePropertySource propertySource = (CompositePropertySource) locator.locate(new MockEnvironment());
|
||||
List<String> names = propertySource.getPropertySources().stream().map(PropertySource::getName).toList();
|
||||
|
||||
// two sources are present, one being empty
|
||||
assertThat(names).containsExactly("configmap.two.default", "configmap..default");
|
||||
assertThat(propertySource.getProperty(Constants.ERROR_PROPERTY)).isEqualTo("true");
|
||||
|
||||
assertThat(output).contains("failure in reading labeled sources");
|
||||
assertThat(output).contains("failure in reading named sources");
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* <pre>
|
||||
* there are two sources and we try to read them.
|
||||
* both fail.
|
||||
* </pre>
|
||||
*/
|
||||
@Test
|
||||
void labeledTwoConfigMapsBothFail(CapturedOutput output) {
|
||||
|
||||
Map<String, String> configMapOneLabels = Map.of("one", "1");
|
||||
Map<String, String> configMapTwoLabels = Map.of("two", "2");
|
||||
|
||||
String namespace = "default";
|
||||
String path = "/api/v1/namespaces/default/configmaps";
|
||||
|
||||
// one for 'application' named configmap and two for the labeled configmaps
|
||||
stubFor(get(path).willReturn(aResponse().withStatus(500).withBody("Internal Server Error"))
|
||||
.inScenario("started")
|
||||
.willSetStateTo("first"));
|
||||
|
||||
stubFor(get(path).willReturn(aResponse().withStatus(500).withBody("Internal Server Error"))
|
||||
.inScenario("started")
|
||||
.whenScenarioStateIs("first")
|
||||
.willSetStateTo("second"));
|
||||
|
||||
stubFor(get(path).willReturn(aResponse().withStatus(500).withBody("Internal Server Error"))
|
||||
.inScenario("started")
|
||||
.whenScenarioStateIs("second")
|
||||
.willSetStateTo("done"));
|
||||
|
||||
ConfigMapConfigProperties.Source sourceOne = new ConfigMapConfigProperties.Source(null, namespace,
|
||||
configMapOneLabels, null, null, null);
|
||||
ConfigMapConfigProperties.Source sourceTwo = new ConfigMapConfigProperties.Source(null, namespace,
|
||||
configMapTwoLabels, null, null, null);
|
||||
|
||||
ConfigMapConfigProperties configMapConfigProperties = new ConfigMapConfigProperties(true, List.of(),
|
||||
List.of(sourceOne, sourceTwo), Map.of("one", "1", "two", "2"), true, null, namespace, false, true,
|
||||
false, RetryProperties.DEFAULT);
|
||||
|
||||
CoreV1Api api = new CoreV1Api();
|
||||
KubernetesClientConfigMapPropertySourceLocator locator = new KubernetesClientConfigMapPropertySourceLocator(api,
|
||||
configMapConfigProperties, new KubernetesNamespaceProvider(new MockEnvironment()));
|
||||
|
||||
CompositePropertySource propertySource = (CompositePropertySource) locator.locate(new MockEnvironment());
|
||||
List<String> names = propertySource.getPropertySources().stream().map(PropertySource::getName).toList();
|
||||
|
||||
// all 3 sources ('application' named source, and two labeled sources)
|
||||
assertThat(names).containsExactly("configmap..default");
|
||||
assertThat(propertySource.getProperty(Constants.ERROR_PROPERTY)).isEqualTo("true");
|
||||
|
||||
assertThat(output).contains("failure in reading labeled sources");
|
||||
assertThat(output).contains("failure in reading named sources");
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -54,28 +54,54 @@ class KubernetesClientSecretsPropertySourceLocatorTests {
|
||||
|
||||
private static final String LIST_API = "/api/v1/namespaces/default/secrets";
|
||||
|
||||
private static final String LIST_BODY = "{\n" + "\t\"kind\": \"SecretList\",\n" + "\t\"apiVersion\": \"v1\",\n"
|
||||
+ "\t\"metadata\": {\n" + "\t\t\"selfLink\": \"/api/v1/secrets\",\n"
|
||||
+ "\t\t\"resourceVersion\": \"163035\"\n" + "\t},\n" + "\t\"items\": [{\n" + "\t\t\t\"metadata\": {\n"
|
||||
+ "\t\t\t\t\"name\": \"db-secret\",\n" + "\t\t\t\t\"namespace\": \"default\",\n"
|
||||
+ "\t\t\t\t\"selfLink\": \"/api/v1/namespaces/default/secrets/db-secret\",\n"
|
||||
+ "\t\t\t\t\"uid\": \"59ba8e6a-a2d4-416c-b016-22597c193f23\",\n"
|
||||
+ "\t\t\t\t\"resourceVersion\": \"1462\",\n" + "\t\t\t\t\"creationTimestamp\": \"2020-10-28T14:45:02Z\",\n"
|
||||
+ "\t\t\t\t\"labels\": {\n" + "\t\t\t\t\t\"spring.cloud.kubernetes.secret\": \"true\"\n" + "\t\t\t\t}\n"
|
||||
+ "\t\t\t},\n" + "\t\t\t\"data\": {\n" + "\t\t\t\t\"password\": \"cDQ1NXcwcmQ=\",\n"
|
||||
+ "\t\t\t\t\"username\": \"dXNlcg==\"\n" + "\t\t\t},\n" + "\t\t\t\"type\": \"Opaque\"\n" + "\t\t},\n"
|
||||
+ "\t\t{\n" + "\t\t\t\"metadata\": {\n" + "\t\t\t\t\"name\": \"rabbit-password\",\n"
|
||||
+ "\t\t\t\t\"namespace\": \"default\",\n"
|
||||
+ "\t\t\t\t\"selfLink\": \"/api/v1/namespaces/default/secrets/rabbit-password\",\n"
|
||||
+ "\t\t\t\t\"uid\": \"bc211cb4-e7ff-4556-b26e-c54911301740\",\n"
|
||||
+ "\t\t\t\t\"resourceVersion\": \"162708\",\n"
|
||||
+ "\t\t\t\t\"creationTimestamp\": \"2020-10-29T19:47:36Z\",\n" + "\t\t\t\t\"labels\": {\n"
|
||||
+ "\t\t\t\t\t\"spring.cloud.kubernetes.secret\": \"true\"\n" + "\t\t\t\t},\n"
|
||||
+ "\t\t\t\t\"annotations\": {\n"
|
||||
+ "\t\t\t\t\t\"kubectl.kubernetes.io/last-applied-configuration\": \"{\\\"apiVersion\\\":\\\"v1\\\",\\\"data\\\":{\\\"spring.rabbitmq.password\\\":\\\"password\\\"},\\\"kind\\\":\\\"Secret\\\",\\\"metadata\\\":{\\\"annotations\\\":{},\\\"labels\\\":{\\\"spring.cloud.kubernetes.secret\\\":\\\"true\\\"},\\\"name\\\":\\\"rabbit-password\\\",\\\"namespace\\\":\\\"default\\\"},\\\"type\\\":\\\"Opaque\\\"}\\n\"\n"
|
||||
+ "\t\t\t\t}\n" + "\t\t\t},\n" + "\t\t\t\"data\": {\n"
|
||||
+ "\t\t\t\t\"spring.rabbitmq.password\": \"cGFzc3dvcmQ=\"\n" + "\t\t\t},\n" + "\t\t\t\"type\": \"Opaque\"\n"
|
||||
+ "\t\t}\n" + "\t]\n" + "}";
|
||||
private static final String LIST_BODY = """
|
||||
{
|
||||
\t"kind": "SecretList",
|
||||
\t"apiVersion": "v1",
|
||||
\t"metadata": {
|
||||
\t\t"selfLink": "/api/v1/secrets",
|
||||
\t\t"resourceVersion": "163035"
|
||||
\t},
|
||||
\t"items": [{
|
||||
\t\t\t"metadata": {
|
||||
\t\t\t\t"name": "db-secret",
|
||||
\t\t\t\t"namespace": "default",
|
||||
\t\t\t\t"selfLink": "/api/v1/namespaces/default/secrets/db-secret",
|
||||
\t\t\t\t"uid": "59ba8e6a-a2d4-416c-b016-22597c193f23",
|
||||
\t\t\t\t"resourceVersion": "1462",
|
||||
\t\t\t\t"creationTimestamp": "2020-10-28T14:45:02Z",
|
||||
\t\t\t\t"labels": {
|
||||
\t\t\t\t\t"spring.cloud.kubernetes.secret": "true"
|
||||
\t\t\t\t}
|
||||
\t\t\t},
|
||||
\t\t\t"data": {
|
||||
\t\t\t\t"password": "cDQ1NXcwcmQ=",
|
||||
\t\t\t\t"username": "dXNlcg=="
|
||||
\t\t\t},
|
||||
\t\t\t"type": "Opaque"
|
||||
\t\t},
|
||||
\t\t{
|
||||
\t\t\t"metadata": {
|
||||
\t\t\t\t"name": "rabbit-password",
|
||||
\t\t\t\t"namespace": "default",
|
||||
\t\t\t\t"selfLink": "/api/v1/namespaces/default/secrets/rabbit-password",
|
||||
\t\t\t\t"uid": "bc211cb4-e7ff-4556-b26e-c54911301740",
|
||||
\t\t\t\t"resourceVersion": "162708",
|
||||
\t\t\t\t"creationTimestamp": "2020-10-29T19:47:36Z",
|
||||
\t\t\t\t"labels": {
|
||||
\t\t\t\t\t"spring.cloud.kubernetes.secret": "true"
|
||||
\t\t\t\t},
|
||||
\t\t\t\t"annotations": {
|
||||
\t\t\t\t\t"kubectl.kubernetes.io/last-applied-configuration": "{\\"apiVersion\\":\\"v1\\",\\"data\\":{\\"spring.rabbitmq.password\\":\\"password\\"},\\"kind\\":\\"Secret\\",\\"metadata\\":{\\"annotations\\":{},\\"labels\\":{\\"spring.cloud.kubernetes.secret\\":\\"true\\"},\\"name\\":\\"rabbit-password\\",\\"namespace\\":\\"default\\"},\\"type\\":\\"Opaque\\"}\\n"
|
||||
\t\t\t\t}
|
||||
\t\t\t},
|
||||
\t\t\t"data": {
|
||||
\t\t\t\t"spring.rabbitmq.password": "cGFzc3dvcmQ="
|
||||
\t\t\t},
|
||||
\t\t\t"type": "Opaque"
|
||||
\t\t}
|
||||
\t]
|
||||
}""";
|
||||
|
||||
private static WireMockServer wireMockServer;
|
||||
|
||||
|
||||
@@ -80,28 +80,54 @@ class KubernetesClientSecretsPropertySourceTests {
|
||||
|
||||
private static final String LIST_API_WITH_LABEL = "/api/v1/namespaces/default/secrets";
|
||||
|
||||
private static final String LIST_BODY = "{\n" + "\t\"kind\": \"SecretList\",\n" + "\t\"apiVersion\": \"v1\",\n"
|
||||
+ "\t\"metadata\": {\n" + "\t\t\"selfLink\": \"/api/v1/secrets\",\n"
|
||||
+ "\t\t\"resourceVersion\": \"163035\"\n" + "\t},\n" + "\t\"items\": [{\n" + "\t\t\t\"metadata\": {\n"
|
||||
+ "\t\t\t\t\"name\": \"db-secret\",\n" + "\t\t\t\t\"namespace\": \"default\",\n"
|
||||
+ "\t\t\t\t\"selfLink\": \"/api/v1/namespaces/default/secrets/db-secret\",\n"
|
||||
+ "\t\t\t\t\"uid\": \"59ba8e6a-a2d4-416c-b016-22597c193f23\",\n"
|
||||
+ "\t\t\t\t\"resourceVersion\": \"1462\",\n" + "\t\t\t\t\"creationTimestamp\": \"2020-10-28T14:45:02Z\",\n"
|
||||
+ "\t\t\t\t\"labels\": {\n" + "\t\t\t\t\t\"spring.cloud.kubernetes.secret\": \"true\"\n" + "\t\t\t\t}\n"
|
||||
+ "\t\t\t},\n" + "\t\t\t\"data\": {\n" + "\t\t\t\t\"password\": \"cDQ1NXcwcmQ=\",\n"
|
||||
+ "\t\t\t\t\"username\": \"dXNlcg==\"\n" + "\t\t\t},\n" + "\t\t\t\"type\": \"Opaque\"\n" + "\t\t},\n"
|
||||
+ "\t\t{\n" + "\t\t\t\"metadata\": {\n" + "\t\t\t\t\"name\": \"rabbit-password\",\n"
|
||||
+ "\t\t\t\t\"namespace\": \"default\",\n"
|
||||
+ "\t\t\t\t\"selfLink\": \"/api/v1/namespaces/default/secrets/rabbit-password\",\n"
|
||||
+ "\t\t\t\t\"uid\": \"bc211cb4-e7ff-4556-b26e-c54911301740\",\n"
|
||||
+ "\t\t\t\t\"resourceVersion\": \"162708\",\n"
|
||||
+ "\t\t\t\t\"creationTimestamp\": \"2020-10-29T19:47:36Z\",\n" + "\t\t\t\t\"labels\": {\n"
|
||||
+ "\t\t\t\t\t\"spring.cloud.kubernetes.secret\": \"true\"\n" + "\t\t\t\t},\n"
|
||||
+ "\t\t\t\t\"annotations\": {\n"
|
||||
+ "\t\t\t\t\t\"kubectl.kubernetes.io/last-applied-configuration\": \"{\\\"apiVersion\\\":\\\"v1\\\",\\\"data\\\":{\\\"spring.rabbitmq.password\\\":\\\"password\\\"},\\\"kind\\\":\\\"Secret\\\",\\\"metadata\\\":{\\\"annotations\\\":{},\\\"labels\\\":{\\\"spring.cloud.kubernetes.secret\\\":\\\"true\\\"},\\\"name\\\":\\\"rabbit-password\\\",\\\"namespace\\\":\\\"default\\\"},\\\"type\\\":\\\"Opaque\\\"}\\n\"\n"
|
||||
+ "\t\t\t\t}\n" + "\t\t\t},\n" + "\t\t\t\"data\": {\n"
|
||||
+ "\t\t\t\t\"spring.rabbitmq.password\": \"cGFzc3dvcmQ=\"\n" + "\t\t\t},\n" + "\t\t\t\"type\": \"Opaque\"\n"
|
||||
+ "\t\t}\n" + "\t]\n" + "}";
|
||||
private static final String LIST_BODY = """
|
||||
{
|
||||
\t"kind": "SecretList",
|
||||
\t"apiVersion": "v1",
|
||||
\t"metadata": {
|
||||
\t\t"selfLink": "/api/v1/secrets",
|
||||
\t\t"resourceVersion": "163035"
|
||||
\t},
|
||||
\t"items": [{
|
||||
\t\t\t"metadata": {
|
||||
\t\t\t\t"name": "db-secret",
|
||||
\t\t\t\t"namespace": "default",
|
||||
\t\t\t\t"selfLink": "/api/v1/namespaces/default/secrets/db-secret",
|
||||
\t\t\t\t"uid": "59ba8e6a-a2d4-416c-b016-22597c193f23",
|
||||
\t\t\t\t"resourceVersion": "1462",
|
||||
\t\t\t\t"creationTimestamp": "2020-10-28T14:45:02Z",
|
||||
\t\t\t\t"labels": {
|
||||
\t\t\t\t\t"spring.cloud.kubernetes.secret": "true"
|
||||
\t\t\t\t}
|
||||
\t\t\t},
|
||||
\t\t\t"data": {
|
||||
\t\t\t\t"password": "cDQ1NXcwcmQ=",
|
||||
\t\t\t\t"username": "dXNlcg=="
|
||||
\t\t\t},
|
||||
\t\t\t"type": "Opaque"
|
||||
\t\t},
|
||||
\t\t{
|
||||
\t\t\t"metadata": {
|
||||
\t\t\t\t"name": "rabbit-password",
|
||||
\t\t\t\t"namespace": "default",
|
||||
\t\t\t\t"selfLink": "/api/v1/namespaces/default/secrets/rabbit-password",
|
||||
\t\t\t\t"uid": "bc211cb4-e7ff-4556-b26e-c54911301740",
|
||||
\t\t\t\t"resourceVersion": "162708",
|
||||
\t\t\t\t"creationTimestamp": "2020-10-29T19:47:36Z",
|
||||
\t\t\t\t"labels": {
|
||||
\t\t\t\t\t"spring.cloud.kubernetes.secret": "true"
|
||||
\t\t\t\t},
|
||||
\t\t\t\t"annotations": {
|
||||
\t\t\t\t\t"kubectl.kubernetes.io/last-applied-configuration": "{\\"apiVersion\\":\\"v1\\",\\"data\\":{\\"spring.rabbitmq.password\\":\\"password\\"},\\"kind\\":\\"Secret\\",\\"metadata\\":{\\"annotations\\":{},\\"labels\\":{\\"spring.cloud.kubernetes.secret\\":\\"true\\"},\\"name\\":\\"rabbit-password\\",\\"namespace\\":\\"default\\"},\\"type\\":\\"Opaque\\"}\\n"
|
||||
\t\t\t\t}
|
||||
\t\t\t},
|
||||
\t\t\t"data": {
|
||||
\t\t\t\t"spring.rabbitmq.password": "cGFzc3dvcmQ="
|
||||
\t\t\t},
|
||||
\t\t\t"type": "Opaque"
|
||||
\t\t}
|
||||
\t]
|
||||
}""";
|
||||
|
||||
private static WireMockServer wireMockServer;
|
||||
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
* Copyright 2013-2022 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.client.config;
|
||||
|
||||
import io.kubernetes.client.common.KubernetesObject;
|
||||
import io.kubernetes.client.openapi.apis.CoreV1Api;
|
||||
|
||||
import org.springframework.cloud.kubernetes.client.config.reload.KubernetesClientEventBasedConfigMapChangeDetector;
|
||||
import org.springframework.cloud.kubernetes.commons.KubernetesNamespaceProvider;
|
||||
import org.springframework.cloud.kubernetes.commons.config.reload.ConfigReloadProperties;
|
||||
import org.springframework.cloud.kubernetes.commons.config.reload.ConfigurationUpdateStrategy;
|
||||
import org.springframework.core.env.ConfigurableEnvironment;
|
||||
|
||||
/**
|
||||
* @author wind57
|
||||
*/
|
||||
public class VisibleKubernetesClientEventBasedConfigMapChangeDetector
|
||||
extends KubernetesClientEventBasedConfigMapChangeDetector {
|
||||
|
||||
public VisibleKubernetesClientEventBasedConfigMapChangeDetector(CoreV1Api coreV1Api,
|
||||
ConfigurableEnvironment environment, ConfigReloadProperties properties,
|
||||
ConfigurationUpdateStrategy strategy, KubernetesClientConfigMapPropertySourceLocator propertySourceLocator,
|
||||
KubernetesNamespaceProvider kubernetesNamespaceProvider) {
|
||||
super(coreV1Api, environment, properties, strategy, propertySourceLocator, kubernetesNamespaceProvider);
|
||||
}
|
||||
|
||||
public void onEvent(KubernetesObject kubernetesObject) {
|
||||
super.onEvent(kubernetesObject);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
* Copyright 2013-2022 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.client.config;
|
||||
|
||||
import io.kubernetes.client.common.KubernetesObject;
|
||||
import io.kubernetes.client.openapi.apis.CoreV1Api;
|
||||
|
||||
import org.springframework.cloud.kubernetes.client.config.reload.KubernetesClientEventBasedSecretsChangeDetector;
|
||||
import org.springframework.cloud.kubernetes.commons.KubernetesNamespaceProvider;
|
||||
import org.springframework.cloud.kubernetes.commons.config.reload.ConfigReloadProperties;
|
||||
import org.springframework.cloud.kubernetes.commons.config.reload.ConfigurationUpdateStrategy;
|
||||
import org.springframework.core.env.ConfigurableEnvironment;
|
||||
|
||||
/**
|
||||
* @author wind57
|
||||
*/
|
||||
public class VisibleKubernetesClientEventBasedSecretsChangeDetector
|
||||
extends KubernetesClientEventBasedSecretsChangeDetector {
|
||||
|
||||
public VisibleKubernetesClientEventBasedSecretsChangeDetector(CoreV1Api coreV1Api,
|
||||
ConfigurableEnvironment environment, ConfigReloadProperties properties,
|
||||
ConfigurationUpdateStrategy strategy, KubernetesClientSecretsPropertySourceLocator propertySourceLocator,
|
||||
KubernetesNamespaceProvider kubernetesNamespaceProvider) {
|
||||
super(coreV1Api, environment, properties, strategy, propertySourceLocator, kubernetesNamespaceProvider);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEvent(KubernetesObject kubernetesObject) {
|
||||
super.onEvent(kubernetesObject);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,255 @@
|
||||
/*
|
||||
* Copyright 2013-2024 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.client.config.reload_it;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import com.github.tomakehurst.wiremock.WireMockServer;
|
||||
import com.github.tomakehurst.wiremock.client.WireMock;
|
||||
import io.kubernetes.client.openapi.ApiClient;
|
||||
import io.kubernetes.client.openapi.Configuration;
|
||||
import io.kubernetes.client.openapi.JSON;
|
||||
import io.kubernetes.client.openapi.apis.CoreV1Api;
|
||||
import io.kubernetes.client.openapi.models.V1ConfigMap;
|
||||
import io.kubernetes.client.openapi.models.V1ConfigMapBuilder;
|
||||
import io.kubernetes.client.openapi.models.V1ConfigMapList;
|
||||
import io.kubernetes.client.util.ClientBuilder;
|
||||
import org.awaitility.Awaitility;
|
||||
import org.junit.jupiter.api.AfterAll;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.MockedStatic;
|
||||
import org.mockito.Mockito;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.boot.test.context.TestConfiguration;
|
||||
import org.springframework.boot.test.system.CapturedOutput;
|
||||
import org.springframework.boot.test.system.OutputCaptureExtension;
|
||||
import org.springframework.cloud.kubernetes.client.KubernetesClientUtils;
|
||||
import org.springframework.cloud.kubernetes.client.config.KubernetesClientConfigMapPropertySourceLocator;
|
||||
import org.springframework.cloud.kubernetes.client.config.VisibleKubernetesClientEventBasedConfigMapChangeDetector;
|
||||
import org.springframework.cloud.kubernetes.commons.KubernetesNamespaceProvider;
|
||||
import org.springframework.cloud.kubernetes.commons.config.ConfigMapConfigProperties;
|
||||
import org.springframework.cloud.kubernetes.commons.config.RetryProperties;
|
||||
import org.springframework.cloud.kubernetes.commons.config.reload.ConfigReloadProperties;
|
||||
import org.springframework.cloud.kubernetes.commons.config.reload.ConfigurationUpdateStrategy;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Primary;
|
||||
import org.springframework.core.env.AbstractEnvironment;
|
||||
import org.springframework.core.env.PropertySource;
|
||||
import org.springframework.mock.env.MockEnvironment;
|
||||
|
||||
import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
|
||||
import static com.github.tomakehurst.wiremock.client.WireMock.get;
|
||||
import static com.github.tomakehurst.wiremock.client.WireMock.stubFor;
|
||||
import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.options;
|
||||
|
||||
/**
|
||||
* @author wind57
|
||||
*/
|
||||
@SpringBootTest(
|
||||
properties = { "spring.main.allow-bean-definition-overriding=true",
|
||||
"logging.level.org.springframework.cloud.kubernetes.commons.config=debug" },
|
||||
classes = { EventReloadConfigMapTest.TestConfig.class })
|
||||
@ExtendWith(OutputCaptureExtension.class)
|
||||
class EventReloadConfigMapTest {
|
||||
|
||||
private static final boolean FAIL_FAST = false;
|
||||
|
||||
private static WireMockServer wireMockServer;
|
||||
|
||||
private static final String CONFIG_MAP_NAME = "mine";
|
||||
|
||||
private static final String NAMESPACE = "spring-k8s";
|
||||
|
||||
private static final boolean[] strategyCalled = new boolean[] { false };
|
||||
|
||||
private static CoreV1Api coreV1Api;
|
||||
|
||||
private static final MockedStatic<KubernetesClientUtils> MOCK_STATIC = Mockito
|
||||
.mockStatic(KubernetesClientUtils.class);
|
||||
|
||||
@Autowired
|
||||
private VisibleKubernetesClientEventBasedConfigMapChangeDetector kubernetesClientEventBasedConfigMapChangeDetector;
|
||||
|
||||
@BeforeAll
|
||||
static void setup() {
|
||||
wireMockServer = new WireMockServer(options().dynamicPort());
|
||||
|
||||
wireMockServer.start();
|
||||
WireMock.configureFor("localhost", wireMockServer.port());
|
||||
|
||||
ApiClient client = new ClientBuilder().setBasePath("http://localhost:" + wireMockServer.port()).build();
|
||||
client.setDebugging(true);
|
||||
MOCK_STATIC.when(KubernetesClientUtils::createApiClientForInformerClient).thenReturn(client);
|
||||
MOCK_STATIC
|
||||
.when(() -> KubernetesClientUtils.getApplicationNamespace(Mockito.anyString(), Mockito.anyString(),
|
||||
Mockito.any()))
|
||||
.thenReturn(NAMESPACE);
|
||||
Configuration.setDefaultApiClient(client);
|
||||
coreV1Api = new CoreV1Api();
|
||||
|
||||
String path = "/api/v1/namespaces/spring-k8s/configmaps";
|
||||
V1ConfigMap configMapOne = configMap(CONFIG_MAP_NAME, Map.of());
|
||||
V1ConfigMapList listOne = new V1ConfigMapList().addItemsItem(configMapOne);
|
||||
|
||||
// needed so that our environment is populated with 'something'
|
||||
// this call is done in the method that returns the AbstractEnvironment
|
||||
stubFor(get(path).willReturn(aResponse().withStatus(200).withBody(new JSON().serialize(listOne)))
|
||||
.inScenario("mine-test")
|
||||
.willSetStateTo("go-to-fail"));
|
||||
|
||||
// first call will fail
|
||||
stubFor(get(path).willReturn(aResponse().withStatus(500).withBody("Internal Server Error"))
|
||||
.inScenario("mine-test")
|
||||
.whenScenarioStateIs("go-to-fail")
|
||||
.willSetStateTo("go-to-ok"));
|
||||
|
||||
// second call passes (change data so that reload is triggered)
|
||||
configMapOne = configMap(CONFIG_MAP_NAME, Map.of("a", "b"));
|
||||
listOne = new V1ConfigMapList().addItemsItem(configMapOne);
|
||||
stubFor(get(path).willReturn(aResponse().withStatus(200).withBody(new JSON().serialize(listOne)))
|
||||
.inScenario("mine-test")
|
||||
.whenScenarioStateIs("go-to-ok")
|
||||
.willSetStateTo("done"));
|
||||
}
|
||||
|
||||
@AfterAll
|
||||
static void after() {
|
||||
MOCK_STATIC.close();
|
||||
wireMockServer.stop();
|
||||
}
|
||||
|
||||
/**
|
||||
* <pre>
|
||||
* - 'configmap.mine.spring-k8s' already exists in the environment
|
||||
* - we simulate that another configmap is created, so a request goes to k8s to find any potential
|
||||
* differences. This request is mocked to fail.
|
||||
* - then our configmap is changed and the request passes
|
||||
* </pre>
|
||||
*/
|
||||
@Test
|
||||
void test(CapturedOutput output) {
|
||||
V1ConfigMap configMapNotMine = configMap("not" + CONFIG_MAP_NAME, Map.of());
|
||||
kubernetesClientEventBasedConfigMapChangeDetector.onEvent(configMapNotMine);
|
||||
|
||||
// we fail while reading 'configMapOne'
|
||||
Awaitility.await().atMost(Duration.ofSeconds(10)).pollInterval(Duration.ofSeconds(1)).until(() -> {
|
||||
boolean one = output.getOut().contains("failure in reading named sources");
|
||||
boolean two = output.getOut()
|
||||
.contains("there was an error while reading config maps/secrets, no reload will happen");
|
||||
boolean three = output.getOut()
|
||||
.contains("reloadable condition was not satisfied, reload will not be triggered");
|
||||
boolean updateStrategyNotCalled = !strategyCalled[0];
|
||||
return one && two && three && updateStrategyNotCalled;
|
||||
});
|
||||
|
||||
// trigger the call again
|
||||
V1ConfigMap configMapMine = configMap(CONFIG_MAP_NAME, Map.of());
|
||||
kubernetesClientEventBasedConfigMapChangeDetector.onEvent(configMapMine);
|
||||
Awaitility.await()
|
||||
.atMost(Duration.ofSeconds(10))
|
||||
.pollInterval(Duration.ofSeconds(1))
|
||||
.until(() -> strategyCalled[0]);
|
||||
}
|
||||
|
||||
private static V1ConfigMap configMap(String name, Map<String, String> data) {
|
||||
return new V1ConfigMapBuilder().withNewMetadata().withName(name).endMetadata().withData(data).build();
|
||||
}
|
||||
|
||||
@TestConfiguration
|
||||
static class TestConfig {
|
||||
|
||||
@Bean
|
||||
@Primary
|
||||
VisibleKubernetesClientEventBasedConfigMapChangeDetector kubernetesClientEventBasedConfigMapChangeDetector(
|
||||
AbstractEnvironment environment, ConfigReloadProperties configReloadProperties,
|
||||
ConfigurationUpdateStrategy configurationUpdateStrategy,
|
||||
KubernetesClientConfigMapPropertySourceLocator kubernetesClientConfigMapPropertySourceLocator,
|
||||
KubernetesNamespaceProvider namespaceProvider) {
|
||||
return new VisibleKubernetesClientEventBasedConfigMapChangeDetector(coreV1Api, environment,
|
||||
configReloadProperties, configurationUpdateStrategy, kubernetesClientConfigMapPropertySourceLocator,
|
||||
namespaceProvider);
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Primary
|
||||
AbstractEnvironment environment() {
|
||||
MockEnvironment mockEnvironment = new MockEnvironment();
|
||||
mockEnvironment.setProperty("spring.cloud.kubernetes.client.namespace", NAMESPACE);
|
||||
|
||||
// simulate that environment already has a
|
||||
// KubernetesClientConfigMapPropertySource,
|
||||
// otherwise we can't properly test reload functionality
|
||||
ConfigMapConfigProperties configMapConfigProperties = new ConfigMapConfigProperties(true, List.of(),
|
||||
List.of(), Map.of(), true, CONFIG_MAP_NAME, NAMESPACE, false, true, FAIL_FAST,
|
||||
RetryProperties.DEFAULT);
|
||||
KubernetesNamespaceProvider namespaceProvider = new KubernetesNamespaceProvider(mockEnvironment);
|
||||
|
||||
PropertySource<?> propertySource = new KubernetesClientConfigMapPropertySourceLocator(coreV1Api,
|
||||
configMapConfigProperties, namespaceProvider)
|
||||
.locate(mockEnvironment);
|
||||
|
||||
mockEnvironment.getPropertySources().addFirst(propertySource);
|
||||
return mockEnvironment;
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Primary
|
||||
ConfigReloadProperties configReloadProperties() {
|
||||
return new ConfigReloadProperties(true, true, false, ConfigReloadProperties.ReloadStrategy.REFRESH,
|
||||
ConfigReloadProperties.ReloadDetectionMode.POLLING, Duration.ofMillis(2000), Set.of("non-default"),
|
||||
false, Duration.ofSeconds(2));
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Primary
|
||||
ConfigMapConfigProperties configMapConfigProperties() {
|
||||
return new ConfigMapConfigProperties(true, List.of(), List.of(), Map.of(), true, CONFIG_MAP_NAME, NAMESPACE,
|
||||
false, true, FAIL_FAST, RetryProperties.DEFAULT);
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Primary
|
||||
KubernetesNamespaceProvider namespaceProvider(AbstractEnvironment environment) {
|
||||
return new KubernetesNamespaceProvider(environment);
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Primary
|
||||
ConfigurationUpdateStrategy configurationUpdateStrategy() {
|
||||
return new ConfigurationUpdateStrategy("to-console", () -> {
|
||||
strategyCalled[0] = true;
|
||||
});
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Primary
|
||||
KubernetesClientConfigMapPropertySourceLocator kubernetesClientConfigMapPropertySourceLocator(
|
||||
ConfigMapConfigProperties configMapConfigProperties, KubernetesNamespaceProvider namespaceProvider) {
|
||||
return new KubernetesClientConfigMapPropertySourceLocator(coreV1Api, configMapConfigProperties,
|
||||
namespaceProvider);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,260 @@
|
||||
/*
|
||||
* Copyright 2013-2024 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.client.config.reload_it;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.Base64;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import com.github.tomakehurst.wiremock.WireMockServer;
|
||||
import com.github.tomakehurst.wiremock.client.WireMock;
|
||||
import io.kubernetes.client.openapi.ApiClient;
|
||||
import io.kubernetes.client.openapi.Configuration;
|
||||
import io.kubernetes.client.openapi.JSON;
|
||||
import io.kubernetes.client.openapi.apis.CoreV1Api;
|
||||
import io.kubernetes.client.openapi.models.V1Secret;
|
||||
import io.kubernetes.client.openapi.models.V1SecretBuilder;
|
||||
import io.kubernetes.client.openapi.models.V1SecretList;
|
||||
import io.kubernetes.client.util.ClientBuilder;
|
||||
import org.awaitility.Awaitility;
|
||||
import org.junit.jupiter.api.AfterAll;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.MockedStatic;
|
||||
import org.mockito.Mockito;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.boot.test.context.TestConfiguration;
|
||||
import org.springframework.boot.test.system.CapturedOutput;
|
||||
import org.springframework.boot.test.system.OutputCaptureExtension;
|
||||
import org.springframework.cloud.kubernetes.client.KubernetesClientUtils;
|
||||
import org.springframework.cloud.kubernetes.client.config.KubernetesClientSecretsPropertySourceLocator;
|
||||
import org.springframework.cloud.kubernetes.client.config.VisibleKubernetesClientEventBasedSecretsChangeDetector;
|
||||
import org.springframework.cloud.kubernetes.commons.KubernetesNamespaceProvider;
|
||||
import org.springframework.cloud.kubernetes.commons.config.RetryProperties;
|
||||
import org.springframework.cloud.kubernetes.commons.config.SecretsConfigProperties;
|
||||
import org.springframework.cloud.kubernetes.commons.config.reload.ConfigReloadProperties;
|
||||
import org.springframework.cloud.kubernetes.commons.config.reload.ConfigurationUpdateStrategy;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Primary;
|
||||
import org.springframework.core.env.AbstractEnvironment;
|
||||
import org.springframework.core.env.PropertySource;
|
||||
import org.springframework.mock.env.MockEnvironment;
|
||||
|
||||
import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
|
||||
import static com.github.tomakehurst.wiremock.client.WireMock.get;
|
||||
import static com.github.tomakehurst.wiremock.client.WireMock.stubFor;
|
||||
import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.options;
|
||||
|
||||
/**
|
||||
* @author wind57
|
||||
*/
|
||||
@SpringBootTest(
|
||||
properties = { "spring.main.allow-bean-definition-overriding=true",
|
||||
"logging.level.org.springframework.cloud.kubernetes.commons.config=debug" },
|
||||
classes = { EventReloadSecretTest.TestConfig.class })
|
||||
@ExtendWith(OutputCaptureExtension.class)
|
||||
class EventReloadSecretTest {
|
||||
|
||||
private static final boolean FAIL_FAST = false;
|
||||
|
||||
private static WireMockServer wireMockServer;
|
||||
|
||||
private static final String SECRET_NAME = "mine";
|
||||
|
||||
private static final String NAMESPACE = "spring-k8s";
|
||||
|
||||
private static final boolean[] strategyCalled = new boolean[] { false };
|
||||
|
||||
private static CoreV1Api coreV1Api;
|
||||
|
||||
private static final MockedStatic<KubernetesClientUtils> MOCK_STATIC = Mockito
|
||||
.mockStatic(KubernetesClientUtils.class);
|
||||
|
||||
@Autowired
|
||||
private VisibleKubernetesClientEventBasedSecretsChangeDetector kubernetesClientEventBasedSecretsChangeDetector;
|
||||
|
||||
@BeforeAll
|
||||
static void setup() {
|
||||
wireMockServer = new WireMockServer(options().dynamicPort());
|
||||
|
||||
wireMockServer.start();
|
||||
WireMock.configureFor("localhost", wireMockServer.port());
|
||||
|
||||
ApiClient client = new ClientBuilder().setBasePath("http://localhost:" + wireMockServer.port()).build();
|
||||
client.setDebugging(true);
|
||||
MOCK_STATIC.when(KubernetesClientUtils::createApiClientForInformerClient).thenReturn(client);
|
||||
MOCK_STATIC
|
||||
.when(() -> KubernetesClientUtils.getApplicationNamespace(Mockito.anyString(), Mockito.anyString(),
|
||||
Mockito.any()))
|
||||
.thenReturn(NAMESPACE);
|
||||
Configuration.setDefaultApiClient(client);
|
||||
coreV1Api = new CoreV1Api();
|
||||
|
||||
String path = "/api/v1/namespaces/spring-k8s/secrets";
|
||||
V1Secret secretOne = secret(SECRET_NAME, Map.of());
|
||||
V1SecretList listOne = new V1SecretList().addItemsItem(secretOne);
|
||||
|
||||
// needed so that our environment is populated with 'something'
|
||||
// this call is done in the method that returns the AbstractEnvironment
|
||||
stubFor(get(path).willReturn(aResponse().withStatus(200).withBody(new JSON().serialize(listOne)))
|
||||
.inScenario("mine-test")
|
||||
.willSetStateTo("go-to-fail"));
|
||||
|
||||
// first call will fail
|
||||
stubFor(get(path).willReturn(aResponse().withStatus(500).withBody("Internal Server Error"))
|
||||
.inScenario("mine-test")
|
||||
.whenScenarioStateIs("go-to-fail")
|
||||
.willSetStateTo("go-to-ok"));
|
||||
|
||||
// second call passes (change data so that reload is triggered)
|
||||
secretOne = secret(SECRET_NAME, Map.of("a", "b"));
|
||||
listOne = new V1SecretList().addItemsItem(secretOne);
|
||||
stubFor(get(path).willReturn(aResponse().withStatus(200).withBody(new JSON().serialize(listOne)))
|
||||
.inScenario("mine-test")
|
||||
.whenScenarioStateIs("go-to-ok")
|
||||
.willSetStateTo("done"));
|
||||
}
|
||||
|
||||
@AfterAll
|
||||
static void after() {
|
||||
MOCK_STATIC.close();
|
||||
wireMockServer.stop();
|
||||
}
|
||||
|
||||
/**
|
||||
* <pre>
|
||||
* - 'secret.mine.spring-k8s' already exists in the environment
|
||||
* - we simulate that another secret is created, so a request goes to k8s to find any potential
|
||||
* differences. This request is mocked to fail.
|
||||
* - then our secret is changed and the request passes
|
||||
* </pre>
|
||||
*/
|
||||
@Test
|
||||
void test(CapturedOutput output) {
|
||||
V1Secret secretNotMine = secret("not" + SECRET_NAME, Map.of());
|
||||
kubernetesClientEventBasedSecretsChangeDetector.onEvent(secretNotMine);
|
||||
|
||||
// we fail while reading 'configMapOne'
|
||||
Awaitility.await().atMost(Duration.ofSeconds(10)).pollInterval(Duration.ofSeconds(1)).until(() -> {
|
||||
boolean one = output.getOut().contains("failure in reading named sources");
|
||||
boolean two = output.getOut()
|
||||
.contains("there was an error while reading config maps/secrets, no reload will happen");
|
||||
boolean three = output.getOut()
|
||||
.contains("reloadable condition was not satisfied, reload will not be triggered");
|
||||
boolean updateStrategyNotCalled = !strategyCalled[0];
|
||||
return one && two && three && updateStrategyNotCalled;
|
||||
});
|
||||
|
||||
// trigger the call again
|
||||
V1Secret secretMine = secret(SECRET_NAME, Map.of());
|
||||
kubernetesClientEventBasedSecretsChangeDetector.onEvent(secretMine);
|
||||
Awaitility.await()
|
||||
.atMost(Duration.ofSeconds(10))
|
||||
.pollInterval(Duration.ofSeconds(1))
|
||||
.until(() -> strategyCalled[0]);
|
||||
}
|
||||
|
||||
private static V1Secret secret(String name, Map<String, String> data) {
|
||||
Map<String, byte[]> encoded = data.entrySet()
|
||||
.stream()
|
||||
.collect(Collectors.toMap(Map.Entry::getKey, e -> Base64.getEncoder().encode(e.getValue().getBytes())));
|
||||
|
||||
return new V1SecretBuilder().withNewMetadata().withName(name).endMetadata().withData(encoded).build();
|
||||
}
|
||||
|
||||
@TestConfiguration
|
||||
static class TestConfig {
|
||||
|
||||
@Bean
|
||||
@Primary
|
||||
VisibleKubernetesClientEventBasedSecretsChangeDetector kubernetesClientEventBasedSecretsChangeDetector(
|
||||
AbstractEnvironment environment, ConfigReloadProperties configReloadProperties,
|
||||
ConfigurationUpdateStrategy configurationUpdateStrategy,
|
||||
KubernetesClientSecretsPropertySourceLocator kubernetesClientSecretsPropertySourceLocator,
|
||||
KubernetesNamespaceProvider namespaceProvider) {
|
||||
return new VisibleKubernetesClientEventBasedSecretsChangeDetector(coreV1Api, environment,
|
||||
configReloadProperties, configurationUpdateStrategy, kubernetesClientSecretsPropertySourceLocator,
|
||||
namespaceProvider);
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Primary
|
||||
AbstractEnvironment environment() {
|
||||
MockEnvironment mockEnvironment = new MockEnvironment();
|
||||
mockEnvironment.setProperty("spring.cloud.kubernetes.client.namespace", NAMESPACE);
|
||||
|
||||
// simulate that environment already has a
|
||||
// KubernetesClientConfigMapPropertySource,
|
||||
// otherwise we can't properly test reload functionality
|
||||
SecretsConfigProperties secretsConfigProperties = new SecretsConfigProperties(true, Map.of(), List.of(),
|
||||
List.of(), true, SECRET_NAME, NAMESPACE, false, true, FAIL_FAST, RetryProperties.DEFAULT);
|
||||
KubernetesNamespaceProvider namespaceProvider = new KubernetesNamespaceProvider(mockEnvironment);
|
||||
|
||||
PropertySource<?> propertySource = new KubernetesClientSecretsPropertySourceLocator(coreV1Api,
|
||||
namespaceProvider, secretsConfigProperties)
|
||||
.locate(mockEnvironment);
|
||||
|
||||
mockEnvironment.getPropertySources().addFirst(propertySource);
|
||||
return mockEnvironment;
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Primary
|
||||
ConfigReloadProperties configReloadProperties() {
|
||||
return new ConfigReloadProperties(true, true, false, ConfigReloadProperties.ReloadStrategy.REFRESH,
|
||||
ConfigReloadProperties.ReloadDetectionMode.POLLING, Duration.ofMillis(2000), Set.of("non-default"),
|
||||
false, Duration.ofSeconds(2));
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Primary
|
||||
SecretsConfigProperties secretsConfigProperties() {
|
||||
return new SecretsConfigProperties(true, Map.of(), List.of(), List.of(), true, SECRET_NAME, NAMESPACE,
|
||||
false, true, FAIL_FAST, RetryProperties.DEFAULT);
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Primary
|
||||
KubernetesNamespaceProvider namespaceProvider(AbstractEnvironment environment) {
|
||||
return new KubernetesNamespaceProvider(environment);
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Primary
|
||||
ConfigurationUpdateStrategy configurationUpdateStrategy() {
|
||||
return new ConfigurationUpdateStrategy("to-console", () -> {
|
||||
strategyCalled[0] = true;
|
||||
});
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Primary
|
||||
KubernetesClientSecretsPropertySourceLocator kubernetesClientSecretsPropertySourceLocator(
|
||||
SecretsConfigProperties secretsConfigProperties, KubernetesNamespaceProvider namespaceProvider) {
|
||||
return new KubernetesClientSecretsPropertySourceLocator(coreV1Api, namespaceProvider,
|
||||
secretsConfigProperties);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,234 @@
|
||||
/*
|
||||
* Copyright 2013-2024 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.client.config.reload_it;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import com.github.tomakehurst.wiremock.WireMockServer;
|
||||
import com.github.tomakehurst.wiremock.client.WireMock;
|
||||
import io.kubernetes.client.openapi.ApiClient;
|
||||
import io.kubernetes.client.openapi.Configuration;
|
||||
import io.kubernetes.client.openapi.JSON;
|
||||
import io.kubernetes.client.openapi.apis.CoreV1Api;
|
||||
import io.kubernetes.client.openapi.models.V1ConfigMap;
|
||||
import io.kubernetes.client.openapi.models.V1ConfigMapBuilder;
|
||||
import io.kubernetes.client.openapi.models.V1ConfigMapList;
|
||||
import io.kubernetes.client.util.ClientBuilder;
|
||||
import org.awaitility.Awaitility;
|
||||
import org.junit.jupiter.api.AfterAll;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.boot.test.context.TestConfiguration;
|
||||
import org.springframework.boot.test.system.CapturedOutput;
|
||||
import org.springframework.boot.test.system.OutputCaptureExtension;
|
||||
import org.springframework.cloud.kubernetes.client.config.KubernetesClientConfigMapPropertySource;
|
||||
import org.springframework.cloud.kubernetes.client.config.KubernetesClientConfigMapPropertySourceLocator;
|
||||
import org.springframework.cloud.kubernetes.commons.KubernetesNamespaceProvider;
|
||||
import org.springframework.cloud.kubernetes.commons.config.ConfigMapConfigProperties;
|
||||
import org.springframework.cloud.kubernetes.commons.config.RetryProperties;
|
||||
import org.springframework.cloud.kubernetes.commons.config.reload.ConfigReloadProperties;
|
||||
import org.springframework.cloud.kubernetes.commons.config.reload.ConfigurationUpdateStrategy;
|
||||
import org.springframework.cloud.kubernetes.commons.config.reload.PollingConfigMapChangeDetector;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Primary;
|
||||
import org.springframework.core.env.AbstractEnvironment;
|
||||
import org.springframework.core.env.PropertySource;
|
||||
import org.springframework.mock.env.MockEnvironment;
|
||||
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
|
||||
|
||||
import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
|
||||
import static com.github.tomakehurst.wiremock.client.WireMock.get;
|
||||
import static com.github.tomakehurst.wiremock.client.WireMock.stubFor;
|
||||
import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.options;
|
||||
|
||||
/**
|
||||
* @author wind57
|
||||
*/
|
||||
@SpringBootTest(
|
||||
properties = { "spring.main.allow-bean-definition-overriding=true",
|
||||
"logging.level.org.springframework.cloud.kubernetes.commons.config=debug" },
|
||||
classes = { PollingReloadConfigMapTest.TestConfig.class })
|
||||
@ExtendWith(OutputCaptureExtension.class)
|
||||
class PollingReloadConfigMapTest {
|
||||
|
||||
private static WireMockServer wireMockServer;
|
||||
|
||||
private static final boolean FAIL_FAST = false;
|
||||
|
||||
private static final String CONFIG_MAP_NAME = "mine";
|
||||
|
||||
private static final String NAMESPACE = "spring-k8s";
|
||||
|
||||
private static final boolean[] strategyCalled = new boolean[] { false };
|
||||
|
||||
private static CoreV1Api coreV1Api;
|
||||
|
||||
@BeforeAll
|
||||
static void setup() {
|
||||
wireMockServer = new WireMockServer(options().dynamicPort());
|
||||
|
||||
wireMockServer.start();
|
||||
WireMock.configureFor("localhost", wireMockServer.port());
|
||||
|
||||
ApiClient client = new ClientBuilder().setBasePath("http://localhost:" + wireMockServer.port()).build();
|
||||
client.setDebugging(true);
|
||||
Configuration.setDefaultApiClient(client);
|
||||
coreV1Api = new CoreV1Api();
|
||||
|
||||
String path = "/api/v1/namespaces/spring-k8s/configmaps";
|
||||
V1ConfigMap configMapOne = configMap(CONFIG_MAP_NAME, Map.of());
|
||||
V1ConfigMapList listOne = new V1ConfigMapList().addItemsItem(configMapOne);
|
||||
|
||||
// needed so that our environment is populated with 'something'
|
||||
// this call is done in the method that returns the AbstractEnvironment
|
||||
stubFor(get(path).willReturn(aResponse().withStatus(200).withBody(new JSON().serialize(listOne)))
|
||||
.inScenario("my-test")
|
||||
.willSetStateTo("go-to-fail"));
|
||||
|
||||
// first reload call fails
|
||||
stubFor(get(path).willReturn(aResponse().withStatus(500).withBody("Internal Server Error"))
|
||||
.inScenario("my-test")
|
||||
.whenScenarioStateIs("go-to-fail")
|
||||
.willSetStateTo("go-to-ok"));
|
||||
|
||||
// second reload call passes
|
||||
V1ConfigMap configMapTwo = configMap(CONFIG_MAP_NAME, Map.of("a", "b"));
|
||||
V1ConfigMapList listTwo = new V1ConfigMapList().addItemsItem(configMapTwo);
|
||||
stubFor(get(path).willReturn(aResponse().withStatus(200).withBody(new JSON().serialize(listTwo)))
|
||||
.inScenario("my-test")
|
||||
.whenScenarioStateIs("go-to-ok"));
|
||||
|
||||
}
|
||||
|
||||
@AfterAll
|
||||
static void after() {
|
||||
wireMockServer.stop();
|
||||
}
|
||||
|
||||
/**
|
||||
* <pre>
|
||||
* - we have a PropertySource in the environment
|
||||
* - first polling cycle tries to read the sources from k8s and fails
|
||||
* - second polling cycle reads sources from k8s and finds a change
|
||||
* </pre>
|
||||
*/
|
||||
@Test
|
||||
void test(CapturedOutput output) {
|
||||
// we fail while reading 'configMapOne'
|
||||
Awaitility.await().atMost(Duration.ofSeconds(10)).pollInterval(Duration.ofSeconds(1)).until(() -> {
|
||||
boolean one = output.getOut().contains("failure in reading named sources");
|
||||
boolean two = output.getOut()
|
||||
.contains("there was an error while reading config maps/secrets, no reload will happen");
|
||||
boolean three = output.getOut()
|
||||
.contains("reloadable condition was not satisfied, reload will not be triggered");
|
||||
boolean updateStrategyNotCalled = !strategyCalled[0];
|
||||
return one && two && three && updateStrategyNotCalled;
|
||||
});
|
||||
|
||||
// it passes while reading 'configMapTwo'
|
||||
Awaitility.await()
|
||||
.atMost(Duration.ofSeconds(10))
|
||||
.pollInterval(Duration.ofSeconds(1))
|
||||
.until(() -> strategyCalled[0]);
|
||||
}
|
||||
|
||||
private static V1ConfigMap configMap(String name, Map<String, String> data) {
|
||||
return new V1ConfigMapBuilder().withNewMetadata().withName(name).endMetadata().withData(data).build();
|
||||
}
|
||||
|
||||
@TestConfiguration
|
||||
static class TestConfig {
|
||||
|
||||
@Bean
|
||||
@Primary
|
||||
PollingConfigMapChangeDetector pollingConfigMapChangeDetector(AbstractEnvironment environment,
|
||||
ConfigReloadProperties configReloadProperties, ConfigurationUpdateStrategy configurationUpdateStrategy,
|
||||
KubernetesClientConfigMapPropertySourceLocator kubernetesClientConfigMapPropertySourceLocator) {
|
||||
ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
|
||||
scheduler.initialize();
|
||||
return new PollingConfigMapChangeDetector(environment, configReloadProperties, configurationUpdateStrategy,
|
||||
KubernetesClientConfigMapPropertySource.class, kubernetesClientConfigMapPropertySourceLocator,
|
||||
scheduler);
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Primary
|
||||
AbstractEnvironment environment() {
|
||||
MockEnvironment mockEnvironment = new MockEnvironment();
|
||||
mockEnvironment.setProperty("spring.cloud.kubernetes.client.namespace", NAMESPACE);
|
||||
|
||||
// simulate that environment already has a
|
||||
// KubernetesClientConfigMapPropertySource,
|
||||
// otherwise we can't properly test reload functionality
|
||||
ConfigMapConfigProperties configMapConfigProperties = new ConfigMapConfigProperties(true, List.of(),
|
||||
List.of(), Map.of(), true, CONFIG_MAP_NAME, NAMESPACE, false, true, true, RetryProperties.DEFAULT);
|
||||
KubernetesNamespaceProvider namespaceProvider = new KubernetesNamespaceProvider(mockEnvironment);
|
||||
|
||||
PropertySource<?> propertySource = new KubernetesClientConfigMapPropertySourceLocator(coreV1Api,
|
||||
configMapConfigProperties, namespaceProvider)
|
||||
.locate(mockEnvironment);
|
||||
|
||||
mockEnvironment.getPropertySources().addFirst(propertySource);
|
||||
return mockEnvironment;
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Primary
|
||||
ConfigReloadProperties configReloadProperties() {
|
||||
return new ConfigReloadProperties(true, true, false, ConfigReloadProperties.ReloadStrategy.REFRESH,
|
||||
ConfigReloadProperties.ReloadDetectionMode.POLLING, Duration.ofMillis(2000), Set.of("non-default"),
|
||||
false, Duration.ofSeconds(2));
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Primary
|
||||
ConfigMapConfigProperties configMapConfigProperties() {
|
||||
return new ConfigMapConfigProperties(true, List.of(), List.of(), Map.of(), true, CONFIG_MAP_NAME, NAMESPACE,
|
||||
false, true, FAIL_FAST, RetryProperties.DEFAULT);
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Primary
|
||||
KubernetesNamespaceProvider namespaceProvider(AbstractEnvironment environment) {
|
||||
return new KubernetesNamespaceProvider(environment);
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Primary
|
||||
ConfigurationUpdateStrategy configurationUpdateStrategy() {
|
||||
return new ConfigurationUpdateStrategy("to-console", () -> {
|
||||
strategyCalled[0] = true;
|
||||
});
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Primary
|
||||
KubernetesClientConfigMapPropertySourceLocator kubernetesClientConfigMapPropertySourceLocator(
|
||||
ConfigMapConfigProperties configMapConfigProperties, KubernetesNamespaceProvider namespaceProvider) {
|
||||
return new KubernetesClientConfigMapPropertySourceLocator(coreV1Api, configMapConfigProperties,
|
||||
namespaceProvider);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,240 @@
|
||||
/*
|
||||
* Copyright 2013-2024 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.client.config.reload_it;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.Base64;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import com.github.tomakehurst.wiremock.WireMockServer;
|
||||
import com.github.tomakehurst.wiremock.client.WireMock;
|
||||
import io.kubernetes.client.openapi.ApiClient;
|
||||
import io.kubernetes.client.openapi.Configuration;
|
||||
import io.kubernetes.client.openapi.JSON;
|
||||
import io.kubernetes.client.openapi.apis.CoreV1Api;
|
||||
import io.kubernetes.client.openapi.models.V1Secret;
|
||||
import io.kubernetes.client.openapi.models.V1SecretBuilder;
|
||||
import io.kubernetes.client.openapi.models.V1SecretList;
|
||||
import io.kubernetes.client.util.ClientBuilder;
|
||||
import org.awaitility.Awaitility;
|
||||
import org.junit.jupiter.api.AfterAll;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.boot.test.context.TestConfiguration;
|
||||
import org.springframework.boot.test.system.CapturedOutput;
|
||||
import org.springframework.boot.test.system.OutputCaptureExtension;
|
||||
import org.springframework.cloud.kubernetes.client.config.KubernetesClientSecretsPropertySource;
|
||||
import org.springframework.cloud.kubernetes.client.config.KubernetesClientSecretsPropertySourceLocator;
|
||||
import org.springframework.cloud.kubernetes.commons.KubernetesNamespaceProvider;
|
||||
import org.springframework.cloud.kubernetes.commons.config.RetryProperties;
|
||||
import org.springframework.cloud.kubernetes.commons.config.SecretsConfigProperties;
|
||||
import org.springframework.cloud.kubernetes.commons.config.reload.ConfigReloadProperties;
|
||||
import org.springframework.cloud.kubernetes.commons.config.reload.ConfigurationUpdateStrategy;
|
||||
import org.springframework.cloud.kubernetes.commons.config.reload.PollingSecretsChangeDetector;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Primary;
|
||||
import org.springframework.core.env.AbstractEnvironment;
|
||||
import org.springframework.core.env.PropertySource;
|
||||
import org.springframework.mock.env.MockEnvironment;
|
||||
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
|
||||
|
||||
import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
|
||||
import static com.github.tomakehurst.wiremock.client.WireMock.get;
|
||||
import static com.github.tomakehurst.wiremock.client.WireMock.stubFor;
|
||||
import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.options;
|
||||
|
||||
/**
|
||||
* @author wind57
|
||||
*/
|
||||
@SpringBootTest(
|
||||
properties = { "spring.main.allow-bean-definition-overriding=true",
|
||||
"logging.level.org.springframework.cloud.kubernetes.commons.config=debug" },
|
||||
classes = { PollingReloadSecretTest.TestConfig.class })
|
||||
@ExtendWith(OutputCaptureExtension.class)
|
||||
class PollingReloadSecretTest {
|
||||
|
||||
private static WireMockServer wireMockServer;
|
||||
|
||||
private static final boolean FAIL_FAST = false;
|
||||
|
||||
private static final String SECRET_NAME = "mine";
|
||||
|
||||
private static final String NAMESPACE = "spring-k8s";
|
||||
|
||||
private static final boolean[] strategyCalled = new boolean[] { false };
|
||||
|
||||
private static CoreV1Api coreV1Api;
|
||||
|
||||
@BeforeAll
|
||||
static void setup() {
|
||||
wireMockServer = new WireMockServer(options().dynamicPort());
|
||||
|
||||
wireMockServer.start();
|
||||
WireMock.configureFor("localhost", wireMockServer.port());
|
||||
|
||||
ApiClient client = new ClientBuilder().setBasePath("http://localhost:" + wireMockServer.port()).build();
|
||||
client.setDebugging(true);
|
||||
Configuration.setDefaultApiClient(client);
|
||||
coreV1Api = new CoreV1Api();
|
||||
|
||||
String path = "/api/v1/namespaces/spring-k8s/secrets";
|
||||
V1Secret secretOne = secret(SECRET_NAME, Map.of());
|
||||
V1SecretList listOne = new V1SecretList().addItemsItem(secretOne);
|
||||
|
||||
// needed so that our environment is populated with 'something'
|
||||
// this call is done in the method that returns the AbstractEnvironment
|
||||
stubFor(get(path).willReturn(aResponse().withStatus(200).withBody(new JSON().serialize(listOne)))
|
||||
.inScenario("my-test")
|
||||
.willSetStateTo("go-to-fail"));
|
||||
|
||||
// first reload call fails
|
||||
stubFor(get(path).willReturn(aResponse().withStatus(500).withBody("Internal Server Error"))
|
||||
.inScenario("my-test")
|
||||
.whenScenarioStateIs("go-to-fail")
|
||||
.willSetStateTo("go-to-ok"));
|
||||
|
||||
V1Secret secretTwo = secret(SECRET_NAME, Map.of("a", "b"));
|
||||
V1SecretList listTwo = new V1SecretList().addItemsItem(secretTwo);
|
||||
stubFor(get(path).willReturn(aResponse().withStatus(200).withBody(new JSON().serialize(listTwo)))
|
||||
.inScenario("my-test")
|
||||
.whenScenarioStateIs("go-to-ok"));
|
||||
|
||||
}
|
||||
|
||||
@AfterAll
|
||||
static void after() {
|
||||
wireMockServer.stop();
|
||||
}
|
||||
|
||||
/**
|
||||
* <pre>
|
||||
* - we have a PropertySource in the environment
|
||||
* - first polling cycle tries to read the sources from k8s and fails
|
||||
* - second polling cycle reads sources from k8s and finds a change
|
||||
* </pre>
|
||||
*/
|
||||
@Test
|
||||
void test(CapturedOutput output) {
|
||||
// we fail while reading 'secretOne'
|
||||
Awaitility.await().atMost(Duration.ofSeconds(10)).pollInterval(Duration.ofSeconds(1)).until(() -> {
|
||||
boolean one = output.getOut().contains("failure in reading named sources");
|
||||
boolean two = output.getOut()
|
||||
.contains("there was an error while reading config maps/secrets, no reload will happen");
|
||||
boolean three = output.getOut()
|
||||
.contains("reloadable condition was not satisfied, reload will not be triggered");
|
||||
boolean updateStrategyNotCalled = !strategyCalled[0];
|
||||
return one && two && three && updateStrategyNotCalled;
|
||||
});
|
||||
|
||||
// it passes while reading 'secretTwo'
|
||||
Awaitility.await()
|
||||
.atMost(Duration.ofSeconds(10))
|
||||
.pollInterval(Duration.ofSeconds(1))
|
||||
.until(() -> strategyCalled[0]);
|
||||
}
|
||||
|
||||
private static V1Secret secret(String name, Map<String, String> data) {
|
||||
|
||||
Map<String, byte[]> encoded = data.entrySet()
|
||||
.stream()
|
||||
.collect(Collectors.toMap(Map.Entry::getKey, e -> Base64.getEncoder().encode(e.getValue().getBytes())));
|
||||
|
||||
return new V1SecretBuilder().withNewMetadata().withName(name).endMetadata().withData(encoded).build();
|
||||
}
|
||||
|
||||
@TestConfiguration
|
||||
static class TestConfig {
|
||||
|
||||
@Bean
|
||||
@Primary
|
||||
PollingSecretsChangeDetector pollingSecretsChangeDetector(AbstractEnvironment environment,
|
||||
ConfigReloadProperties configReloadProperties, ConfigurationUpdateStrategy configurationUpdateStrategy,
|
||||
KubernetesClientSecretsPropertySourceLocator kubernetesClientSecretsPropertySourceLocator) {
|
||||
ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
|
||||
scheduler.initialize();
|
||||
return new PollingSecretsChangeDetector(environment, configReloadProperties, configurationUpdateStrategy,
|
||||
KubernetesClientSecretsPropertySource.class, kubernetesClientSecretsPropertySourceLocator,
|
||||
scheduler);
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Primary
|
||||
AbstractEnvironment environment() {
|
||||
MockEnvironment mockEnvironment = new MockEnvironment();
|
||||
mockEnvironment.setProperty("spring.cloud.kubernetes.client.namespace", NAMESPACE);
|
||||
|
||||
// simulate that environment already has a
|
||||
// KubernetesClientSecretPropertySource,
|
||||
// otherwise we can't properly test reload functionality
|
||||
SecretsConfigProperties secretsConfigProperties = new SecretsConfigProperties(true, Map.of(), List.of(),
|
||||
List.of(), true, SECRET_NAME, NAMESPACE, false, true, false, RetryProperties.DEFAULT);
|
||||
KubernetesNamespaceProvider namespaceProvider = new KubernetesNamespaceProvider(mockEnvironment);
|
||||
|
||||
PropertySource<?> propertySource = new KubernetesClientSecretsPropertySourceLocator(coreV1Api,
|
||||
namespaceProvider, secretsConfigProperties)
|
||||
.locate(mockEnvironment);
|
||||
|
||||
mockEnvironment.getPropertySources().addFirst(propertySource);
|
||||
return mockEnvironment;
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Primary
|
||||
ConfigReloadProperties configReloadProperties() {
|
||||
return new ConfigReloadProperties(true, false, true, ConfigReloadProperties.ReloadStrategy.REFRESH,
|
||||
ConfigReloadProperties.ReloadDetectionMode.POLLING, Duration.ofMillis(2000), Set.of("non-default"),
|
||||
false, Duration.ofSeconds(2));
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Primary
|
||||
SecretsConfigProperties secretsConfigProperties() {
|
||||
return new SecretsConfigProperties(true, Map.of(), List.of(), List.of(), true, SECRET_NAME, NAMESPACE,
|
||||
false, true, FAIL_FAST, RetryProperties.DEFAULT);
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Primary
|
||||
KubernetesNamespaceProvider namespaceProvider(AbstractEnvironment environment) {
|
||||
return new KubernetesNamespaceProvider(environment);
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Primary
|
||||
ConfigurationUpdateStrategy configurationUpdateStrategy() {
|
||||
return new ConfigurationUpdateStrategy("to-console", () -> {
|
||||
strategyCalled[0] = true;
|
||||
});
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Primary
|
||||
KubernetesClientSecretsPropertySourceLocator kubernetesClientSecretsPropertySourceLocator(
|
||||
SecretsConfigProperties secretsConfigProperties, KubernetesNamespaceProvider namespaceProvider) {
|
||||
return new KubernetesClientSecretsPropertySourceLocator(coreV1Api, namespaceProvider,
|
||||
secretsConfigProperties);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -63,6 +63,12 @@ public final class Constants {
|
||||
*/
|
||||
public static final String RELOAD_MODE = "spring.cloud.kubernetes.reload.mode";
|
||||
|
||||
/**
|
||||
* property set to true when there was an error reading config maps or secrets, when
|
||||
* generating a property source.
|
||||
*/
|
||||
public static final String ERROR_PROPERTY = "spring.cloud.k8s.error.reading.property.source";
|
||||
|
||||
private Constants() {
|
||||
}
|
||||
|
||||
|
||||
@@ -21,7 +21,11 @@ import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
|
||||
import static org.springframework.cloud.kubernetes.commons.config.ConfigUtils.onException;
|
||||
import static org.springframework.cloud.kubernetes.commons.config.Constants.ERROR_PROPERTY;
|
||||
import static org.springframework.cloud.kubernetes.commons.config.Constants.PROPERTY_SOURCE_NAME_SEPARATOR;
|
||||
|
||||
/**
|
||||
@@ -32,6 +36,8 @@ import static org.springframework.cloud.kubernetes.commons.config.Constants.PROP
|
||||
*/
|
||||
public abstract class LabeledSourceData {
|
||||
|
||||
private static final Log LOG = LogFactory.getLog(LabeledSourceData.class);
|
||||
|
||||
public final SourceData compute(Map<String, String> labels, ConfigUtils.Prefix prefix, String target,
|
||||
boolean profileSources, boolean failFast, String namespace, String[] activeProfiles) {
|
||||
|
||||
@@ -73,7 +79,9 @@ public abstract class LabeledSourceData {
|
||||
}
|
||||
}
|
||||
catch (Exception e) {
|
||||
LOG.warn("failure in reading labeled sources");
|
||||
onException(failFast, e);
|
||||
data = new MultipleSourcesContainer(data.names(), Map.of(ERROR_PROPERTY, "true"));
|
||||
}
|
||||
|
||||
String names = data.names().stream().sorted().collect(Collectors.joining(PROPERTY_SOURCE_NAME_SEPARATOR));
|
||||
|
||||
@@ -17,12 +17,14 @@
|
||||
package org.springframework.cloud.kubernetes.commons.config;
|
||||
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
|
||||
import static org.springframework.cloud.kubernetes.commons.config.ConfigUtils.onException;
|
||||
import static org.springframework.cloud.kubernetes.commons.config.Constants.ERROR_PROPERTY;
|
||||
import static org.springframework.cloud.kubernetes.commons.config.Constants.PROPERTY_SOURCE_NAME_SEPARATOR;
|
||||
|
||||
/**
|
||||
@@ -69,7 +71,9 @@ public abstract class NamedSourceData {
|
||||
|
||||
}
|
||||
catch (Exception e) {
|
||||
LOG.warn("failure in reading named sources");
|
||||
onException(failFast, e);
|
||||
data = new MultipleSourcesContainer(data.names(), Map.of(ERROR_PROPERTY, "true"));
|
||||
}
|
||||
|
||||
String names = data.names().stream().sorted().collect(Collectors.joining(PROPERTY_SOURCE_NAME_SEPARATOR));
|
||||
|
||||
@@ -33,6 +33,8 @@ import org.springframework.core.env.MapPropertySource;
|
||||
import org.springframework.core.env.PropertySource;
|
||||
import org.springframework.core.log.LogAccessor;
|
||||
|
||||
import static org.springframework.cloud.kubernetes.commons.config.Constants.ERROR_PROPERTY;
|
||||
|
||||
/**
|
||||
* @author wind57
|
||||
*/
|
||||
@@ -71,11 +73,11 @@ public final class ConfigReloadUtil {
|
||||
|
||||
boolean changed = changed(sourceFromK8s, existingSources);
|
||||
if (changed) {
|
||||
LOG.info("Detected change in config maps/secrets");
|
||||
LOG.info("Detected change in config maps/secrets, reload will ne triggered");
|
||||
return true;
|
||||
}
|
||||
else {
|
||||
LOG.debug("No change detected in config maps/secrets, reload will not happen");
|
||||
LOG.debug("reloadable condition was not satisfied, reload will not be triggered");
|
||||
}
|
||||
|
||||
return false;
|
||||
@@ -162,6 +164,12 @@ public final class ConfigReloadUtil {
|
||||
}
|
||||
|
||||
static boolean changed(List<? extends MapPropertySource> k8sSources, List<? extends MapPropertySource> appSources) {
|
||||
|
||||
if (k8sSources.stream().anyMatch(source -> "true".equals(source.getProperty(ERROR_PROPERTY)))) {
|
||||
LOG.info(() -> "there was an error while reading config maps/secrets, no reload will happen");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (k8sSources.size() != appSources.size()) {
|
||||
if (LOG.isDebugEnabled()) {
|
||||
LOG.debug("k8s property sources size: " + k8sSources.size());
|
||||
|
||||
@@ -26,6 +26,7 @@ import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.cloud.bootstrap.config.BootstrapPropertySource;
|
||||
import org.springframework.cloud.kubernetes.commons.config.Constants;
|
||||
import org.springframework.cloud.kubernetes.commons.config.MountConfigMapPropertySource;
|
||||
import org.springframework.core.env.CompositePropertySource;
|
||||
import org.springframework.core.env.EnumerablePropertySource;
|
||||
@@ -150,6 +151,16 @@ class ConfigReloadUtilTests {
|
||||
Assertions.assertEquals("from-inner-two-composite", result.get(3).getProperty(""));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testEmptySourceNameOnError() {
|
||||
Object value = new Object();
|
||||
Map<String, Object> rightMap = Map.of("key", value);
|
||||
MapPropertySource left = new MapPropertySource("on-error", Map.of(Constants.ERROR_PROPERTY, "true"));
|
||||
MapPropertySource right = new MapPropertySource("right", rightMap);
|
||||
boolean changed = ConfigReloadUtil.changed(List.of(left), List.of(right));
|
||||
assertThat(changed).isFalse();
|
||||
}
|
||||
|
||||
private static final class OneComposite extends CompositePropertySource {
|
||||
|
||||
private OneComposite() {
|
||||
|
||||
@@ -0,0 +1,291 @@
|
||||
/*
|
||||
* Copyright 2013-2024 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.fabric8.config;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import io.fabric8.kubernetes.api.model.ConfigMap;
|
||||
import io.fabric8.kubernetes.api.model.ConfigMapBuilder;
|
||||
import io.fabric8.kubernetes.api.model.ConfigMapListBuilder;
|
||||
import io.fabric8.kubernetes.client.KubernetesClient;
|
||||
import io.fabric8.kubernetes.client.server.mock.EnableKubernetesMockClient;
|
||||
import io.fabric8.kubernetes.client.server.mock.KubernetesMockServer;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
|
||||
import org.springframework.boot.test.system.CapturedOutput;
|
||||
import org.springframework.boot.test.system.OutputCaptureExtension;
|
||||
import org.springframework.cloud.kubernetes.commons.KubernetesNamespaceProvider;
|
||||
import org.springframework.cloud.kubernetes.commons.config.ConfigMapConfigProperties;
|
||||
import org.springframework.cloud.kubernetes.commons.config.Constants;
|
||||
import org.springframework.cloud.kubernetes.commons.config.RetryProperties;
|
||||
import org.springframework.core.env.CompositePropertySource;
|
||||
import org.springframework.core.env.MapPropertySource;
|
||||
import org.springframework.core.env.PropertySource;
|
||||
import org.springframework.mock.env.MockEnvironment;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.springframework.cloud.kubernetes.commons.config.ConfigMapConfigProperties.Source;
|
||||
|
||||
/**
|
||||
* @author wind57
|
||||
*/
|
||||
@EnableKubernetesMockClient
|
||||
@ExtendWith(OutputCaptureExtension.class)
|
||||
class Fabric8ConfigMapErrorOnReadingSourceTests {
|
||||
|
||||
private static KubernetesMockServer mockServer;
|
||||
|
||||
private static KubernetesClient mockClient;
|
||||
|
||||
@BeforeAll
|
||||
static void beforeAll() {
|
||||
mockClient.getConfiguration().setRequestRetryBackoffLimit(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* <pre>
|
||||
* we try to read all config maps in a namespace and fail,
|
||||
* thus generate a well defined name for the source.
|
||||
* </pre>
|
||||
*/
|
||||
@Test
|
||||
void namedSingleConfigMapFails() {
|
||||
String name = "my-config";
|
||||
String namespace = "spring-k8s";
|
||||
String path = "/api/v1/namespaces/" + namespace + "/configmaps";
|
||||
|
||||
mockServer.expect().withPath(path).andReturn(500, "Internal Server Error").once();
|
||||
|
||||
ConfigMapConfigProperties configMapConfigProperties = new ConfigMapConfigProperties(true, List.of(), List.of(),
|
||||
Map.of(), true, name, namespace, false, true, false, RetryProperties.DEFAULT);
|
||||
|
||||
Fabric8ConfigMapPropertySourceLocator locator = new Fabric8ConfigMapPropertySourceLocator(mockClient,
|
||||
configMapConfigProperties, new KubernetesNamespaceProvider(new MockEnvironment()));
|
||||
|
||||
CompositePropertySource propertySource = (CompositePropertySource) locator.locate(new MockEnvironment());
|
||||
MapPropertySource mapPropertySource = (MapPropertySource) propertySource.getPropertySources()
|
||||
.stream()
|
||||
.findAny()
|
||||
.orElseThrow();
|
||||
|
||||
assertThat(mapPropertySource.getProperty(Constants.ERROR_PROPERTY)).isEqualTo("true");
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* <pre>
|
||||
* there are two sources and we try to read them.
|
||||
* one fails and one passes.
|
||||
* </pre>
|
||||
*/
|
||||
@Test
|
||||
void namedTwoConfigMapsOneFails() {
|
||||
String configMapNameOne = "one";
|
||||
String configMapNameTwo = "two";
|
||||
String namespace = "default";
|
||||
String path = "/api/v1/namespaces/default/configmaps";
|
||||
|
||||
ConfigMap configMapTwo = configMap(configMapNameTwo, Map.of());
|
||||
|
||||
mockServer.expect().withPath(path).andReturn(500, "Internal Server Error").once();
|
||||
mockServer.expect()
|
||||
.withPath(path)
|
||||
.andReturn(200, new ConfigMapListBuilder().withItems(configMapTwo).build())
|
||||
.once();
|
||||
|
||||
Source sourceOne = new Source(configMapNameOne, namespace, Map.of(), null, null, null);
|
||||
Source sourceTwo = new Source(configMapNameTwo, namespace, Map.of(), null, null, null);
|
||||
|
||||
ConfigMapConfigProperties configMapConfigProperties = new ConfigMapConfigProperties(true, List.of(),
|
||||
List.of(sourceOne, sourceTwo), Map.of(), true, null, namespace, false, true, false,
|
||||
RetryProperties.DEFAULT);
|
||||
|
||||
Fabric8ConfigMapPropertySourceLocator locator = new Fabric8ConfigMapPropertySourceLocator(mockClient,
|
||||
configMapConfigProperties, new KubernetesNamespaceProvider(new MockEnvironment()));
|
||||
|
||||
CompositePropertySource propertySource = (CompositePropertySource) locator.locate(new MockEnvironment());
|
||||
List<String> names = propertySource.getPropertySources().stream().map(PropertySource::getName).toList();
|
||||
|
||||
// two sources are present
|
||||
assertThat(names).containsExactly("configmap.two.default", "configmap..default");
|
||||
assertThat(propertySource.getProperty(Constants.ERROR_PROPERTY)).isEqualTo("true");
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* <pre>
|
||||
* there are two sources and we try to read them.
|
||||
* both fail.
|
||||
* </pre>
|
||||
*/
|
||||
@Test
|
||||
void namedTwoConfigMapsBothFail() {
|
||||
String configMapNameOne = "one";
|
||||
String configMapNameTwo = "two";
|
||||
String namespace = "default";
|
||||
String path = "/api/v1/namespaces/default/configmaps";
|
||||
|
||||
mockServer.expect().withPath(path).andReturn(500, "Internal Server Error").once();
|
||||
mockServer.expect().withPath(path).andReturn(500, "Internal Server Error").once();
|
||||
|
||||
Source sourceOne = new Source(configMapNameOne, namespace, Map.of(), null, null, null);
|
||||
Source sourceTwo = new Source(configMapNameTwo, namespace, Map.of(), null, null, null);
|
||||
|
||||
ConfigMapConfigProperties configMapConfigProperties = new ConfigMapConfigProperties(true, List.of(),
|
||||
List.of(sourceOne, sourceTwo), Map.of(), true, null, namespace, false, true, false,
|
||||
RetryProperties.DEFAULT);
|
||||
|
||||
Fabric8ConfigMapPropertySourceLocator locator = new Fabric8ConfigMapPropertySourceLocator(mockClient,
|
||||
configMapConfigProperties, new KubernetesNamespaceProvider(new MockEnvironment()));
|
||||
|
||||
CompositePropertySource propertySource = (CompositePropertySource) locator.locate(new MockEnvironment());
|
||||
List<String> names = propertySource.getPropertySources().stream().map(PropertySource::getName).toList();
|
||||
|
||||
assertThat(names).containsExactly("configmap..default");
|
||||
assertThat(propertySource.getProperty(Constants.ERROR_PROPERTY)).isEqualTo("true");
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* <pre>
|
||||
* we try to read all config maps in a namespace and fail,
|
||||
* thus generate a well defined name for the source.
|
||||
* </pre>
|
||||
*/
|
||||
@Test
|
||||
void labeledSingleConfigMapFails(CapturedOutput output) {
|
||||
Map<String, String> labels = Map.of("a", "b");
|
||||
String namespace = "spring-k8s";
|
||||
String path = "/api/v1/namespaces/" + namespace + "/configmaps";
|
||||
|
||||
// one for the 'application' named configmap
|
||||
// the other for the labeled config map
|
||||
mockServer.expect().withPath(path).andReturn(500, "Internal Server Error").times(2);
|
||||
|
||||
Source configMapSource = new Source(null, namespace, labels, null, null, null);
|
||||
|
||||
ConfigMapConfigProperties configMapConfigProperties = new ConfigMapConfigProperties(true, List.of(),
|
||||
List.of(configMapSource), labels, true, null, namespace, false, true, false, RetryProperties.DEFAULT);
|
||||
|
||||
Fabric8ConfigMapPropertySourceLocator locator = new Fabric8ConfigMapPropertySourceLocator(mockClient,
|
||||
configMapConfigProperties, new KubernetesNamespaceProvider(new MockEnvironment()));
|
||||
|
||||
CompositePropertySource propertySource = (CompositePropertySource) locator.locate(new MockEnvironment());
|
||||
List<String> sourceNames = propertySource.getPropertySources().stream().map(PropertySource::getName).toList();
|
||||
|
||||
assertThat(sourceNames).containsExactly("configmap..spring-k8s");
|
||||
assertThat(propertySource.getProperty(Constants.ERROR_PROPERTY)).isEqualTo("true");
|
||||
assertThat(output).contains("failure in reading labeled sources");
|
||||
assertThat(output).contains("failure in reading named sources");
|
||||
}
|
||||
|
||||
/**
|
||||
* <pre>
|
||||
* there are two sources and we try to read them.
|
||||
* one fails and one passes.
|
||||
* </pre>
|
||||
*/
|
||||
@Test
|
||||
void labeledTwoConfigMapsOneFails(CapturedOutput output) {
|
||||
String configMapNameOne = "one";
|
||||
String configMapNameTwo = "two";
|
||||
|
||||
Map<String, String> configMapOneLabels = Map.of("one", "1");
|
||||
Map<String, String> configMapTwoLabels = Map.of("two", "2");
|
||||
|
||||
String namespace = "default";
|
||||
String path = "/api/v1/namespaces/default/configmaps";
|
||||
|
||||
ConfigMap configMapOne = configMap(configMapNameOne, configMapOneLabels);
|
||||
ConfigMap configMapTwo = configMap(configMapNameTwo, configMapTwoLabels);
|
||||
|
||||
// one for 'application' named configmap and one for the first labeled configmap
|
||||
mockServer.expect().withPath(path).andReturn(500, "Internal Server Error").times(2);
|
||||
mockServer.expect()
|
||||
.withPath(path)
|
||||
.andReturn(200, new ConfigMapListBuilder().withItems(configMapOne, configMapTwo).build())
|
||||
.once();
|
||||
|
||||
Source sourceOne = new Source(null, namespace, configMapOneLabels, null, null, null);
|
||||
Source sourceTwo = new Source(null, namespace, configMapTwoLabels, null, null, null);
|
||||
|
||||
ConfigMapConfigProperties configMapConfigProperties = new ConfigMapConfigProperties(true, List.of(),
|
||||
List.of(sourceOne, sourceTwo), Map.of("one", "1", "two", "2"), true, null, namespace, false, true,
|
||||
false, RetryProperties.DEFAULT);
|
||||
|
||||
Fabric8ConfigMapPropertySourceLocator locator = new Fabric8ConfigMapPropertySourceLocator(mockClient,
|
||||
configMapConfigProperties, new KubernetesNamespaceProvider(new MockEnvironment()));
|
||||
|
||||
CompositePropertySource propertySource = (CompositePropertySource) locator.locate(new MockEnvironment());
|
||||
List<String> names = propertySource.getPropertySources().stream().map(PropertySource::getName).toList();
|
||||
|
||||
// two sources are present, one being empty
|
||||
assertThat(names).containsExactly("configmap.two.default", "configmap..default");
|
||||
assertThat(propertySource.getProperty(Constants.ERROR_PROPERTY)).isEqualTo("true");
|
||||
|
||||
assertThat(output).contains("failure in reading labeled sources");
|
||||
assertThat(output).contains("failure in reading named sources");
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* <pre>
|
||||
* there are two sources and we try to read them.
|
||||
* both fail.
|
||||
* </pre>
|
||||
*/
|
||||
@Test
|
||||
void labeledTwoConfigMapsBothFail(CapturedOutput output) {
|
||||
|
||||
Map<String, String> configMapOneLabels = Map.of("one", "1");
|
||||
Map<String, String> configMapTwoLabels = Map.of("two", "2");
|
||||
|
||||
String namespace = "default";
|
||||
String path = "/api/v1/namespaces/default/configmaps";
|
||||
|
||||
// one for 'application' named configmap and two for the labeled configmaps
|
||||
mockServer.expect().withPath(path).andReturn(500, "Internal Server Error").times(3);
|
||||
|
||||
Source sourceOne = new Source(null, namespace, configMapOneLabels, null, null, null);
|
||||
Source sourceTwo = new Source(null, namespace, configMapTwoLabels, null, null, null);
|
||||
|
||||
ConfigMapConfigProperties configMapConfigProperties = new ConfigMapConfigProperties(true, List.of(),
|
||||
List.of(sourceOne, sourceTwo), Map.of("one", "1", "two", "2"), true, null, namespace, false, true,
|
||||
false, RetryProperties.DEFAULT);
|
||||
|
||||
Fabric8ConfigMapPropertySourceLocator locator = new Fabric8ConfigMapPropertySourceLocator(mockClient,
|
||||
configMapConfigProperties, new KubernetesNamespaceProvider(new MockEnvironment()));
|
||||
|
||||
CompositePropertySource propertySource = (CompositePropertySource) locator.locate(new MockEnvironment());
|
||||
List<String> names = propertySource.getPropertySources().stream().map(PropertySource::getName).toList();
|
||||
|
||||
assertThat(names).containsExactly("configmap..default");
|
||||
assertThat(propertySource.getProperty(Constants.ERROR_PROPERTY)).isEqualTo("true");
|
||||
|
||||
assertThat(output).contains("failure in reading labeled sources");
|
||||
assertThat(output).contains("failure in reading named sources");
|
||||
|
||||
}
|
||||
|
||||
private ConfigMap configMap(String name, Map<String, String> labels) {
|
||||
return new ConfigMapBuilder().withNewMetadata().withName(name).withLabels(labels).endMetadata().build();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,288 @@
|
||||
/*
|
||||
* Copyright 2013-2024 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.fabric8.config;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import io.fabric8.kubernetes.api.model.Secret;
|
||||
import io.fabric8.kubernetes.api.model.SecretBuilder;
|
||||
import io.fabric8.kubernetes.api.model.SecretListBuilder;
|
||||
import io.fabric8.kubernetes.client.KubernetesClient;
|
||||
import io.fabric8.kubernetes.client.server.mock.EnableKubernetesMockClient;
|
||||
import io.fabric8.kubernetes.client.server.mock.KubernetesMockServer;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
|
||||
import org.springframework.boot.test.system.CapturedOutput;
|
||||
import org.springframework.boot.test.system.OutputCaptureExtension;
|
||||
import org.springframework.cloud.kubernetes.commons.KubernetesNamespaceProvider;
|
||||
import org.springframework.cloud.kubernetes.commons.config.Constants;
|
||||
import org.springframework.cloud.kubernetes.commons.config.RetryProperties;
|
||||
import org.springframework.cloud.kubernetes.commons.config.SecretsConfigProperties;
|
||||
import org.springframework.core.env.CompositePropertySource;
|
||||
import org.springframework.core.env.MapPropertySource;
|
||||
import org.springframework.core.env.PropertySource;
|
||||
import org.springframework.mock.env.MockEnvironment;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.springframework.cloud.kubernetes.commons.config.SecretsConfigProperties.Source;
|
||||
|
||||
/**
|
||||
* @author wind57
|
||||
*/
|
||||
@EnableKubernetesMockClient
|
||||
@ExtendWith(OutputCaptureExtension.class)
|
||||
class Fabric8SecretErrorOnReadingSourceTests {
|
||||
|
||||
private static KubernetesMockServer mockServer;
|
||||
|
||||
private static KubernetesClient mockClient;
|
||||
|
||||
@BeforeAll
|
||||
static void beforeAll() {
|
||||
mockClient.getConfiguration().setRequestRetryBackoffLimit(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* <pre>
|
||||
* we try to read all secrets in a namespace and fail,
|
||||
* thus generate a well defined name for the source.
|
||||
* </pre>
|
||||
*/
|
||||
@Test
|
||||
void namedSingleSecretFails(CapturedOutput output) {
|
||||
String name = "my-secret";
|
||||
String namespace = "spring-k8s";
|
||||
String path = "/api/v1/namespaces/" + namespace + "/secrets";
|
||||
|
||||
mockServer.expect().withPath(path).andReturn(500, "Internal Server Error").once();
|
||||
|
||||
SecretsConfigProperties secretsConfigProperties = new SecretsConfigProperties(true, Map.of(), List.of(),
|
||||
List.of(), true, name, namespace, false, true, false, RetryProperties.DEFAULT);
|
||||
|
||||
Fabric8SecretsPropertySourceLocator locator = new Fabric8SecretsPropertySourceLocator(mockClient,
|
||||
secretsConfigProperties, new KubernetesNamespaceProvider(new MockEnvironment()));
|
||||
|
||||
CompositePropertySource propertySource = (CompositePropertySource) locator.locate(new MockEnvironment());
|
||||
MapPropertySource mapPropertySource = (MapPropertySource) propertySource.getPropertySources()
|
||||
.stream()
|
||||
.findAny()
|
||||
.orElseThrow();
|
||||
|
||||
assertThat(mapPropertySource.getName()).isEqualTo("secret..spring-k8s");
|
||||
assertThat(propertySource.getProperty(Constants.ERROR_PROPERTY)).isEqualTo("true");
|
||||
assertThat(output).contains("failure in reading named sources");
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* <pre>
|
||||
* there are two sources and we try to read them.
|
||||
* one fails and one passes.
|
||||
* </pre>
|
||||
*/
|
||||
@Test
|
||||
void namedTwoSecretsOneFails() {
|
||||
String secretNameOne = "one";
|
||||
String secretNameTwo = "two";
|
||||
String namespace = "default";
|
||||
String path = "/api/v1/namespaces/default/secrets";
|
||||
|
||||
Secret secretTwo = secret(secretNameTwo, Map.of());
|
||||
|
||||
mockServer.expect().withPath(path).andReturn(500, "Internal Server Error").once();
|
||||
mockServer.expect().withPath(path).andReturn(200, new SecretListBuilder().withItems(secretTwo).build()).once();
|
||||
|
||||
Source sourceOne = new Source(secretNameOne, namespace, Map.of(), null, null, null);
|
||||
Source sourceTwo = new Source(secretNameTwo, namespace, Map.of(), null, null, null);
|
||||
|
||||
SecretsConfigProperties secretsConfigProperties = new SecretsConfigProperties(true, Map.of(), List.of(),
|
||||
List.of(sourceOne, sourceTwo), true, null, namespace, false, true, false, RetryProperties.DEFAULT);
|
||||
|
||||
Fabric8SecretsPropertySourceLocator locator = new Fabric8SecretsPropertySourceLocator(mockClient,
|
||||
secretsConfigProperties, new KubernetesNamespaceProvider(new MockEnvironment()));
|
||||
|
||||
CompositePropertySource propertySource = (CompositePropertySource) locator.locate(new MockEnvironment());
|
||||
List<String> names = propertySource.getPropertySources().stream().map(PropertySource::getName).toList();
|
||||
|
||||
// two sources are present, one being empty
|
||||
assertThat(names).containsExactly("secret.two.default", "secret..default");
|
||||
assertThat(propertySource.getProperty(Constants.ERROR_PROPERTY)).isEqualTo("true");
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* <pre>
|
||||
* there are two sources and we try to read them.
|
||||
* both fail.
|
||||
* </pre>
|
||||
*/
|
||||
@Test
|
||||
void namedTwoSecretsBothFail() {
|
||||
String secretNameOne = "one";
|
||||
String secretNameTwo = "two";
|
||||
String namespace = "default";
|
||||
String path = "/api/v1/namespaces/default/secrets";
|
||||
|
||||
mockServer.expect().withPath(path).andReturn(500, "Internal Server Error").once();
|
||||
mockServer.expect().withPath(path).andReturn(500, "Internal Server Error").once();
|
||||
|
||||
Source sourceOne = new Source(secretNameOne, namespace, Map.of(), null, null, null);
|
||||
Source sourceTwo = new Source(secretNameTwo, namespace, Map.of(), null, null, null);
|
||||
|
||||
SecretsConfigProperties secretsConfigProperties = new SecretsConfigProperties(true, Map.of(), List.of(),
|
||||
List.of(sourceOne, sourceTwo), true, null, namespace, false, true, false, RetryProperties.DEFAULT);
|
||||
|
||||
Fabric8SecretsPropertySourceLocator locator = new Fabric8SecretsPropertySourceLocator(mockClient,
|
||||
secretsConfigProperties, new KubernetesNamespaceProvider(new MockEnvironment()));
|
||||
|
||||
CompositePropertySource propertySource = (CompositePropertySource) locator.locate(new MockEnvironment());
|
||||
List<String> names = propertySource.getPropertySources().stream().map(PropertySource::getName).toList();
|
||||
|
||||
assertThat(names).containsExactly("secret..default");
|
||||
assertThat(propertySource.getProperty(Constants.ERROR_PROPERTY)).isEqualTo("true");
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* <pre>
|
||||
* we try to read all secrets in a namespace and fail,
|
||||
* thus generate a well defined name for the source.
|
||||
* </pre>
|
||||
*/
|
||||
@Test
|
||||
void labeledSingleSecretFails(CapturedOutput output) {
|
||||
Map<String, String> labels = Map.of("a", "b");
|
||||
String namespace = "spring-k8s";
|
||||
String path = "/api/v1/namespaces/" + namespace + "/secrets";
|
||||
|
||||
// one for the 'application' named secret
|
||||
// the other for the labeled secret
|
||||
mockServer.expect().withPath(path).andReturn(500, "Internal Server Error").times(2);
|
||||
|
||||
Source secretSource = new Source(null, namespace, labels, null, null, null);
|
||||
|
||||
SecretsConfigProperties secretsConfigProperties = new SecretsConfigProperties(true, labels, List.of(),
|
||||
List.of(secretSource), true, null, namespace, false, true, false, RetryProperties.DEFAULT);
|
||||
|
||||
Fabric8SecretsPropertySourceLocator locator = new Fabric8SecretsPropertySourceLocator(mockClient,
|
||||
secretsConfigProperties, new KubernetesNamespaceProvider(new MockEnvironment()));
|
||||
|
||||
CompositePropertySource propertySource = (CompositePropertySource) locator.locate(new MockEnvironment());
|
||||
List<String> sourceNames = propertySource.getPropertySources().stream().map(PropertySource::getName).toList();
|
||||
|
||||
assertThat(sourceNames).containsExactly("secret..spring-k8s");
|
||||
assertThat(propertySource.getProperty(Constants.ERROR_PROPERTY)).isEqualTo("true");
|
||||
assertThat(output).contains("failure in reading labeled sources");
|
||||
assertThat(output).contains("failure in reading named sources");
|
||||
}
|
||||
|
||||
/**
|
||||
* <pre>
|
||||
* there are two sources and we try to read them.
|
||||
* one fails and one passes.
|
||||
* </pre>
|
||||
*/
|
||||
@Test
|
||||
void labeledTwoSecretsOneFails(CapturedOutput output) {
|
||||
String secretNameOne = "one";
|
||||
String secretNameTwo = "two";
|
||||
|
||||
Map<String, String> secretOneLabels = Map.of("one", "1");
|
||||
Map<String, String> secretTwoLabels = Map.of("two", "2");
|
||||
|
||||
String namespace = "default";
|
||||
String path = "/api/v1/namespaces/default/secrets";
|
||||
|
||||
Secret secretOne = secret(secretNameOne, secretOneLabels);
|
||||
Secret secretTwo = secret(secretNameTwo, secretTwoLabels);
|
||||
|
||||
// one for 'application' named secret and one for the first labeled secret
|
||||
mockServer.expect().withPath(path).andReturn(500, "Internal Server Error").times(2);
|
||||
mockServer.expect()
|
||||
.withPath(path)
|
||||
.andReturn(200, new SecretListBuilder().withItems(secretOne, secretTwo).build())
|
||||
.once();
|
||||
|
||||
Source sourceOne = new Source(null, namespace, secretOneLabels, null, null, null);
|
||||
Source sourceTwo = new Source(null, namespace, secretTwoLabels, null, null, null);
|
||||
|
||||
SecretsConfigProperties secretsConfigProperties = new SecretsConfigProperties(true,
|
||||
Map.of("one", "1", "two", "2"), List.of(), List.of(sourceOne, sourceTwo), true, null, namespace, false,
|
||||
true, false, RetryProperties.DEFAULT);
|
||||
|
||||
Fabric8SecretsPropertySourceLocator locator = new Fabric8SecretsPropertySourceLocator(mockClient,
|
||||
secretsConfigProperties, new KubernetesNamespaceProvider(new MockEnvironment()));
|
||||
|
||||
CompositePropertySource propertySource = (CompositePropertySource) locator.locate(new MockEnvironment());
|
||||
List<String> names = propertySource.getPropertySources().stream().map(PropertySource::getName).toList();
|
||||
|
||||
// two sources are present, one being empty
|
||||
assertThat(names).containsExactly("secret.two.default", "secret..default");
|
||||
assertThat(propertySource.getProperty(Constants.ERROR_PROPERTY)).isEqualTo("true");
|
||||
|
||||
assertThat(output).contains("failure in reading labeled sources");
|
||||
assertThat(output).contains("failure in reading named sources");
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* <pre>
|
||||
* there are two sources and we try to read them.
|
||||
* both fail.
|
||||
* </pre>
|
||||
*/
|
||||
@Test
|
||||
void labeledTwoConfigMapsBothFail(CapturedOutput output) {
|
||||
|
||||
Map<String, String> secretOneLabels = Map.of("one", "1");
|
||||
Map<String, String> secretTwoLabels = Map.of("two", "2");
|
||||
|
||||
String namespace = "default";
|
||||
String path = "/api/v1/namespaces/default/secrets";
|
||||
|
||||
// one for 'application' named configmap and two for the labeled configmaps
|
||||
mockServer.expect().withPath(path).andReturn(500, "Internal Server Error").times(3);
|
||||
|
||||
Source sourceOne = new Source(null, namespace, secretOneLabels, null, null, null);
|
||||
Source sourceTwo = new Source(null, namespace, secretTwoLabels, null, null, null);
|
||||
|
||||
SecretsConfigProperties secretsConfigProperties = new SecretsConfigProperties(true,
|
||||
Map.of("one", "1", "two", "2"), List.of(), List.of(sourceOne, sourceTwo), true, null, namespace, false,
|
||||
true, false, RetryProperties.DEFAULT);
|
||||
|
||||
Fabric8SecretsPropertySourceLocator locator = new Fabric8SecretsPropertySourceLocator(mockClient,
|
||||
secretsConfigProperties, new KubernetesNamespaceProvider(new MockEnvironment()));
|
||||
|
||||
CompositePropertySource propertySource = (CompositePropertySource) locator.locate(new MockEnvironment());
|
||||
List<String> names = propertySource.getPropertySources().stream().map(PropertySource::getName).toList();
|
||||
|
||||
assertThat(names).containsExactly("secret..default");
|
||||
assertThat(propertySource.getProperty(Constants.ERROR_PROPERTY)).isEqualTo("true");
|
||||
|
||||
assertThat(output).contains("failure in reading labeled sources");
|
||||
assertThat(output).contains("failure in reading named sources");
|
||||
|
||||
}
|
||||
|
||||
private Secret secret(String name, Map<String, String> labels) {
|
||||
return new SecretBuilder().withNewMetadata().withName(name).withLabels(labels).endMetadata().build();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
* Copyright 2013-2024 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.fabric8.config;
|
||||
|
||||
import io.fabric8.kubernetes.client.KubernetesClient;
|
||||
|
||||
import org.springframework.cloud.kubernetes.commons.KubernetesNamespaceProvider;
|
||||
import org.springframework.cloud.kubernetes.commons.config.ConfigMapConfigProperties;
|
||||
|
||||
/**
|
||||
* Only needed to make Fabric8ConfigMapPropertySourceLocator visible for testing purposes
|
||||
*
|
||||
* @author wind57
|
||||
*/
|
||||
public class VisibleFabric8ConfigMapPropertySourceLocator extends Fabric8ConfigMapPropertySourceLocator {
|
||||
|
||||
public VisibleFabric8ConfigMapPropertySourceLocator(KubernetesClient client, ConfigMapConfigProperties properties,
|
||||
KubernetesNamespaceProvider provider) {
|
||||
super(client, properties, provider);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
* Copyright 2013-2024 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.fabric8.config;
|
||||
|
||||
import io.fabric8.kubernetes.client.KubernetesClient;
|
||||
|
||||
import org.springframework.cloud.kubernetes.commons.KubernetesNamespaceProvider;
|
||||
import org.springframework.cloud.kubernetes.commons.config.SecretsConfigProperties;
|
||||
|
||||
/**
|
||||
* Only needed to make Fabric8SecretsPropertySourceLocator visible for testing purposes
|
||||
*
|
||||
* @author wind57
|
||||
*/
|
||||
public class VisibleFabric8SecretsPropertySourceLocator extends Fabric8SecretsPropertySourceLocator {
|
||||
|
||||
public VisibleFabric8SecretsPropertySourceLocator(KubernetesClient client, SecretsConfigProperties properties,
|
||||
KubernetesNamespaceProvider provider) {
|
||||
super(client, properties, provider);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,215 @@
|
||||
/*
|
||||
* Copyright 2012-2024 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.fabric8.config.reload_it;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import io.fabric8.kubernetes.api.model.ConfigMap;
|
||||
import io.fabric8.kubernetes.api.model.ConfigMapBuilder;
|
||||
import io.fabric8.kubernetes.api.model.ConfigMapList;
|
||||
import io.fabric8.kubernetes.client.KubernetesClient;
|
||||
import io.fabric8.kubernetes.client.dsl.MixedOperation;
|
||||
import io.fabric8.kubernetes.client.dsl.NonNamespaceOperation;
|
||||
import io.fabric8.kubernetes.client.dsl.Resource;
|
||||
import io.fabric8.kubernetes.client.server.mock.EnableKubernetesMockClient;
|
||||
import org.awaitility.Awaitility;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mockito;
|
||||
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.boot.test.context.TestConfiguration;
|
||||
import org.springframework.boot.test.system.CapturedOutput;
|
||||
import org.springframework.boot.test.system.OutputCaptureExtension;
|
||||
import org.springframework.cloud.kubernetes.commons.KubernetesNamespaceProvider;
|
||||
import org.springframework.cloud.kubernetes.commons.config.ConfigMapConfigProperties;
|
||||
import org.springframework.cloud.kubernetes.commons.config.RetryProperties;
|
||||
import org.springframework.cloud.kubernetes.commons.config.reload.ConfigReloadProperties;
|
||||
import org.springframework.cloud.kubernetes.commons.config.reload.ConfigurationUpdateStrategy;
|
||||
import org.springframework.cloud.kubernetes.fabric8.config.Fabric8ConfigMapPropertySourceLocator;
|
||||
import org.springframework.cloud.kubernetes.fabric8.config.VisibleFabric8ConfigMapPropertySourceLocator;
|
||||
import org.springframework.cloud.kubernetes.fabric8.config.reload.Fabric8EventBasedConfigMapChangeDetector;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Primary;
|
||||
import org.springframework.core.env.AbstractEnvironment;
|
||||
import org.springframework.core.env.PropertySource;
|
||||
import org.springframework.mock.env.MockEnvironment;
|
||||
|
||||
/**
|
||||
* @author wind57
|
||||
*/
|
||||
@SpringBootTest(
|
||||
properties = { "spring.main.allow-bean-definition-overriding=true",
|
||||
"logging.level.org.springframework.cloud.kubernetes.commons.config=debug" },
|
||||
classes = { EventReloadConfigMapTest.TestConfig.class })
|
||||
@EnableKubernetesMockClient(crud = true)
|
||||
@ExtendWith(OutputCaptureExtension.class)
|
||||
public class EventReloadConfigMapTest {
|
||||
|
||||
private static final boolean FAIL_FAST = false;
|
||||
|
||||
private static final String CONFIG_MAP_NAME = "mine";
|
||||
|
||||
private static final String NAMESPACE = "spring-k8s";
|
||||
|
||||
private static KubernetesClient kubernetesClient;
|
||||
|
||||
private static final boolean[] strategyCalled = new boolean[] { false };
|
||||
|
||||
@BeforeAll
|
||||
static void beforeAll() {
|
||||
|
||||
kubernetesClient = Mockito.spy(kubernetesClient);
|
||||
kubernetesClient.getConfiguration().setRequestRetryBackoffLimit(0);
|
||||
|
||||
ConfigMap configMapOne = configMap(CONFIG_MAP_NAME, Map.of());
|
||||
|
||||
// for the informer, when it starts
|
||||
kubernetesClient.configMaps().inNamespace(NAMESPACE).resource(configMapOne).create();
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings({ "unchecked" })
|
||||
void test(CapturedOutput output) {
|
||||
|
||||
// we need to create this one before mocking calls
|
||||
NonNamespaceOperation<ConfigMap, ConfigMapList, Resource<ConfigMap>> operation = kubernetesClient.configMaps()
|
||||
.inNamespace(NAMESPACE);
|
||||
|
||||
// makes sure that when 'onEvent' is triggered (because we added a config map)
|
||||
// the call to /api/v1/namespaces/spring-k8s/configmaps will fail with an
|
||||
// Exception
|
||||
MixedOperation<ConfigMap, ConfigMapList, Resource<ConfigMap>> mixedOperation = Mockito
|
||||
.mock(MixedOperation.class);
|
||||
NonNamespaceOperation<ConfigMap, ConfigMapList, Resource<ConfigMap>> mockedOperation = Mockito
|
||||
.mock(NonNamespaceOperation.class);
|
||||
Mockito.when(kubernetesClient.configMaps()).thenReturn(mixedOperation);
|
||||
Mockito.when(mixedOperation.inNamespace(NAMESPACE)).thenReturn(mockedOperation);
|
||||
Mockito.when(mockedOperation.list()).thenThrow(new RuntimeException("failed in reading configmap"));
|
||||
|
||||
// create a different configmap that triggers even based reloading.
|
||||
// the one we create, will trigger a call to
|
||||
// /api/v1/namespaces/spring-k8s/configmaps
|
||||
// that we mocked above to fail.
|
||||
ConfigMap configMapTwo = configMap("not" + CONFIG_MAP_NAME, Map.of("a", "b"));
|
||||
operation.resource(configMapTwo).create();
|
||||
|
||||
// we fail while reading 'configMapTwo'
|
||||
Awaitility.await().atMost(Duration.ofSeconds(10)).pollInterval(Duration.ofSeconds(1)).until(() -> {
|
||||
boolean one = output.getOut().contains("failure in reading named sources");
|
||||
boolean two = output.getOut()
|
||||
.contains("there was an error while reading config maps/secrets, no reload will happen");
|
||||
boolean three = output.getOut()
|
||||
.contains("reloadable condition was not satisfied, reload will not be triggered");
|
||||
boolean updateStrategyNotCalled = !strategyCalled[0];
|
||||
return one && two && three && updateStrategyNotCalled;
|
||||
});
|
||||
|
||||
// reset the mock and replace our configmap with some data, so that reload
|
||||
// is triggered
|
||||
Mockito.reset(kubernetesClient);
|
||||
ConfigMap configMapOne = configMap(CONFIG_MAP_NAME, Map.of("a", "b"));
|
||||
operation.resource(configMapOne).replace();
|
||||
|
||||
// it passes while reading 'configMapThatWillPass'
|
||||
Awaitility.await()
|
||||
.atMost(Duration.ofSeconds(10))
|
||||
.pollInterval(Duration.ofSeconds(1))
|
||||
.until(() -> strategyCalled[0]);
|
||||
}
|
||||
|
||||
private static ConfigMap configMap(String name, Map<String, String> data) {
|
||||
return new ConfigMapBuilder().withNewMetadata().withName(name).endMetadata().withData(data).build();
|
||||
}
|
||||
|
||||
@TestConfiguration
|
||||
static class TestConfig {
|
||||
|
||||
@Bean
|
||||
@Primary
|
||||
Fabric8EventBasedConfigMapChangeDetector fabric8EventBasedSecretsChangeDetector(AbstractEnvironment environment,
|
||||
ConfigReloadProperties configReloadProperties, ConfigurationUpdateStrategy configurationUpdateStrategy,
|
||||
Fabric8ConfigMapPropertySourceLocator fabric8ConfigMapPropertySourceLocator,
|
||||
KubernetesNamespaceProvider namespaceProvider) {
|
||||
return new Fabric8EventBasedConfigMapChangeDetector(environment, configReloadProperties, kubernetesClient,
|
||||
configurationUpdateStrategy, fabric8ConfigMapPropertySourceLocator, namespaceProvider);
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Primary
|
||||
AbstractEnvironment environment() {
|
||||
MockEnvironment mockEnvironment = new MockEnvironment();
|
||||
mockEnvironment.setProperty("spring.cloud.kubernetes.client.namespace", NAMESPACE);
|
||||
|
||||
// simulate that environment already has a Fabric8ConfigMapPropertySource,
|
||||
// otherwise we can't properly test reload functionality
|
||||
ConfigMapConfigProperties configMapConfigProperties = new ConfigMapConfigProperties(true, List.of(),
|
||||
List.of(), Map.of(), true, CONFIG_MAP_NAME, NAMESPACE, false, true, true, RetryProperties.DEFAULT);
|
||||
KubernetesNamespaceProvider namespaceProvider = new KubernetesNamespaceProvider(mockEnvironment);
|
||||
|
||||
PropertySource<?> propertySource = new VisibleFabric8ConfigMapPropertySourceLocator(kubernetesClient,
|
||||
configMapConfigProperties, namespaceProvider)
|
||||
.locate(mockEnvironment);
|
||||
|
||||
mockEnvironment.getPropertySources().addFirst(propertySource);
|
||||
return mockEnvironment;
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Primary
|
||||
ConfigReloadProperties configReloadProperties() {
|
||||
return new ConfigReloadProperties(true, true, false, ConfigReloadProperties.ReloadStrategy.REFRESH,
|
||||
ConfigReloadProperties.ReloadDetectionMode.EVENT, Duration.ofMillis(2000), Set.of(NAMESPACE), false,
|
||||
Duration.ofSeconds(2));
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Primary
|
||||
ConfigMapConfigProperties configMapConfigProperties() {
|
||||
return new ConfigMapConfigProperties(true, List.of(), List.of(), Map.of(), true, CONFIG_MAP_NAME, NAMESPACE,
|
||||
false, true, FAIL_FAST, RetryProperties.DEFAULT);
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Primary
|
||||
KubernetesNamespaceProvider namespaceProvider(AbstractEnvironment environment) {
|
||||
return new KubernetesNamespaceProvider(environment);
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Primary
|
||||
ConfigurationUpdateStrategy configurationUpdateStrategy() {
|
||||
return new ConfigurationUpdateStrategy("to-console", () -> {
|
||||
strategyCalled[0] = true;
|
||||
});
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Primary
|
||||
Fabric8ConfigMapPropertySourceLocator fabric8ConfigMapPropertySourceLocator(
|
||||
ConfigMapConfigProperties configMapConfigProperties, KubernetesNamespaceProvider namespaceProvider) {
|
||||
return new VisibleFabric8ConfigMapPropertySourceLocator(kubernetesClient, configMapConfigProperties,
|
||||
namespaceProvider);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,222 @@
|
||||
/*
|
||||
* Copyright 2012-2024 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.fabric8.config.reload_it;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.Base64;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import io.fabric8.kubernetes.api.model.Secret;
|
||||
import io.fabric8.kubernetes.api.model.SecretBuilder;
|
||||
import io.fabric8.kubernetes.api.model.SecretList;
|
||||
import io.fabric8.kubernetes.client.KubernetesClient;
|
||||
import io.fabric8.kubernetes.client.dsl.MixedOperation;
|
||||
import io.fabric8.kubernetes.client.dsl.NonNamespaceOperation;
|
||||
import io.fabric8.kubernetes.client.dsl.Resource;
|
||||
import io.fabric8.kubernetes.client.server.mock.EnableKubernetesMockClient;
|
||||
import org.awaitility.Awaitility;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mockito;
|
||||
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.boot.test.context.TestConfiguration;
|
||||
import org.springframework.boot.test.system.CapturedOutput;
|
||||
import org.springframework.boot.test.system.OutputCaptureExtension;
|
||||
import org.springframework.cloud.kubernetes.commons.KubernetesNamespaceProvider;
|
||||
import org.springframework.cloud.kubernetes.commons.config.RetryProperties;
|
||||
import org.springframework.cloud.kubernetes.commons.config.SecretsConfigProperties;
|
||||
import org.springframework.cloud.kubernetes.commons.config.reload.ConfigReloadProperties;
|
||||
import org.springframework.cloud.kubernetes.commons.config.reload.ConfigurationUpdateStrategy;
|
||||
import org.springframework.cloud.kubernetes.fabric8.config.Fabric8SecretsPropertySourceLocator;
|
||||
import org.springframework.cloud.kubernetes.fabric8.config.VisibleFabric8SecretsPropertySourceLocator;
|
||||
import org.springframework.cloud.kubernetes.fabric8.config.reload.Fabric8EventBasedSecretsChangeDetector;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Primary;
|
||||
import org.springframework.core.env.AbstractEnvironment;
|
||||
import org.springframework.core.env.PropertySource;
|
||||
import org.springframework.mock.env.MockEnvironment;
|
||||
|
||||
/**
|
||||
* @author wind57
|
||||
*/
|
||||
@SpringBootTest(
|
||||
properties = { "spring.main.allow-bean-definition-overriding=true",
|
||||
"logging.level.org.springframework.cloud.kubernetes.commons.config=debug" },
|
||||
classes = { EventReloadSecretTest.TestConfig.class })
|
||||
@EnableKubernetesMockClient(crud = true)
|
||||
@ExtendWith(OutputCaptureExtension.class)
|
||||
|
||||
class EventReloadSecretTest {
|
||||
|
||||
private static final boolean FAIL_FAST = false;
|
||||
|
||||
private static final String SECRET_NAME = "mine";
|
||||
|
||||
private static final String NAMESPACE = "spring-k8s";
|
||||
|
||||
private static KubernetesClient kubernetesClient;
|
||||
|
||||
private static final boolean[] strategyCalled = new boolean[] { false };
|
||||
|
||||
@BeforeAll
|
||||
static void beforeAll() {
|
||||
|
||||
kubernetesClient = Mockito.spy(kubernetesClient);
|
||||
kubernetesClient.getConfiguration().setRequestRetryBackoffLimit(0);
|
||||
|
||||
Secret secretOne = secret(SECRET_NAME, Map.of());
|
||||
|
||||
// for the informer, when it starts
|
||||
kubernetesClient.secrets().inNamespace(NAMESPACE).resource(secretOne).create();
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings({ "unchecked" })
|
||||
void test(CapturedOutput output) {
|
||||
|
||||
// we need to create this one before mocking calls
|
||||
NonNamespaceOperation<Secret, SecretList, Resource<Secret>> operation = kubernetesClient.secrets()
|
||||
.inNamespace(NAMESPACE);
|
||||
|
||||
// makes sure that when 'onEvent' is triggered (because we added a config map)
|
||||
// the call to /api/v1/namespaces/spring-k8s/secrets will fail with an
|
||||
// Exception
|
||||
MixedOperation<Secret, SecretList, Resource<Secret>> mixedOperation = Mockito.mock(MixedOperation.class);
|
||||
NonNamespaceOperation<Secret, SecretList, Resource<Secret>> mockedOperation = Mockito
|
||||
.mock(NonNamespaceOperation.class);
|
||||
Mockito.when(kubernetesClient.secrets()).thenReturn(mixedOperation);
|
||||
Mockito.when(mixedOperation.inNamespace(NAMESPACE)).thenReturn(mockedOperation);
|
||||
Mockito.when(mockedOperation.list()).thenThrow(new RuntimeException("failed in reading secret"));
|
||||
|
||||
// create a different secret that triggers even based reloading.
|
||||
// the one we create, will trigger a call to
|
||||
// /api/v1/namespaces/spring-k8s/secrets
|
||||
// that we mocked above to fail.
|
||||
Secret secretTwo = secret("not" + SECRET_NAME, Map.of("a", "b"));
|
||||
operation.resource(secretTwo).create();
|
||||
|
||||
// we fail while reading 'secretTwo'
|
||||
Awaitility.await().atMost(Duration.ofSeconds(10)).pollInterval(Duration.ofSeconds(1)).until(() -> {
|
||||
boolean one = output.getOut().contains("failure in reading named sources");
|
||||
boolean two = output.getOut()
|
||||
.contains("there was an error while reading config maps/secrets, no reload will happen");
|
||||
boolean three = output.getOut()
|
||||
.contains("reloadable condition was not satisfied, reload will not be triggered");
|
||||
boolean updateStrategyNotCalled = !strategyCalled[0];
|
||||
return one && two && three && updateStrategyNotCalled;
|
||||
});
|
||||
|
||||
// reset the mock and replace our secret with some data, so that reload
|
||||
// is triggered
|
||||
Mockito.reset(kubernetesClient);
|
||||
Secret secretOne = secret(SECRET_NAME, Map.of("a", "b"));
|
||||
operation.resource(secretOne).replace();
|
||||
|
||||
// it passes while reading 'secretOne'
|
||||
Awaitility.await()
|
||||
.atMost(Duration.ofSeconds(10))
|
||||
.pollInterval(Duration.ofSeconds(1))
|
||||
.until(() -> strategyCalled[0]);
|
||||
}
|
||||
|
||||
private static Secret secret(String name, Map<String, String> data) {
|
||||
Map<String, String> encoded = data.entrySet()
|
||||
.stream()
|
||||
.collect(Collectors.toMap(Map.Entry::getKey,
|
||||
e -> new String(Base64.getEncoder().encode(e.getValue().getBytes()))));
|
||||
return new SecretBuilder().withNewMetadata().withName(name).endMetadata().withData(encoded).build();
|
||||
}
|
||||
|
||||
@TestConfiguration
|
||||
static class TestConfig {
|
||||
|
||||
@Bean
|
||||
@Primary
|
||||
Fabric8EventBasedSecretsChangeDetector fabric8EventBasedSecretsChangeDetector(AbstractEnvironment environment,
|
||||
ConfigReloadProperties configReloadProperties, ConfigurationUpdateStrategy configurationUpdateStrategy,
|
||||
Fabric8SecretsPropertySourceLocator fabric8SecretsPropertySourceLocator,
|
||||
KubernetesNamespaceProvider namespaceProvider) {
|
||||
return new Fabric8EventBasedSecretsChangeDetector(environment, configReloadProperties, kubernetesClient,
|
||||
configurationUpdateStrategy, fabric8SecretsPropertySourceLocator, namespaceProvider);
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Primary
|
||||
AbstractEnvironment environment() {
|
||||
MockEnvironment mockEnvironment = new MockEnvironment();
|
||||
mockEnvironment.setProperty("spring.cloud.kubernetes.client.namespace", NAMESPACE);
|
||||
|
||||
// simulate that environment already has a
|
||||
// Fabric8SecretsPropertySourceLocator,
|
||||
// otherwise we can't properly test reload functionality
|
||||
SecretsConfigProperties secretsConfigProperties = new SecretsConfigProperties(true, Map.of(), List.of(),
|
||||
List.of(), true, SECRET_NAME, NAMESPACE, false, true, true, RetryProperties.DEFAULT);
|
||||
KubernetesNamespaceProvider namespaceProvider = new KubernetesNamespaceProvider(mockEnvironment);
|
||||
|
||||
PropertySource<?> propertySource = new VisibleFabric8SecretsPropertySourceLocator(kubernetesClient,
|
||||
secretsConfigProperties, namespaceProvider)
|
||||
.locate(mockEnvironment);
|
||||
|
||||
mockEnvironment.getPropertySources().addFirst(propertySource);
|
||||
return mockEnvironment;
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Primary
|
||||
ConfigReloadProperties configReloadProperties() {
|
||||
return new ConfigReloadProperties(true, true, false, ConfigReloadProperties.ReloadStrategy.REFRESH,
|
||||
ConfigReloadProperties.ReloadDetectionMode.EVENT, Duration.ofMillis(2000), Set.of(NAMESPACE), false,
|
||||
Duration.ofSeconds(2));
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Primary
|
||||
SecretsConfigProperties secretsConfigProperties() {
|
||||
return new SecretsConfigProperties(true, Map.of(), List.of(), List.of(), true, SECRET_NAME, NAMESPACE,
|
||||
false, true, FAIL_FAST, RetryProperties.DEFAULT);
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Primary
|
||||
KubernetesNamespaceProvider namespaceProvider(AbstractEnvironment environment) {
|
||||
return new KubernetesNamespaceProvider(environment);
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Primary
|
||||
ConfigurationUpdateStrategy configurationUpdateStrategy() {
|
||||
return new ConfigurationUpdateStrategy("to-console", () -> {
|
||||
strategyCalled[0] = true;
|
||||
});
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Primary
|
||||
Fabric8SecretsPropertySourceLocator fabric8SecretsPropertySourceLocator(
|
||||
SecretsConfigProperties secretsConfigProperties, KubernetesNamespaceProvider namespaceProvider) {
|
||||
return new VisibleFabric8SecretsPropertySourceLocator(kubernetesClient, secretsConfigProperties,
|
||||
namespaceProvider);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,205 @@
|
||||
/*
|
||||
* Copyright 2012-2024 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.fabric8.config.reload_it;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import io.fabric8.kubernetes.api.model.ConfigMap;
|
||||
import io.fabric8.kubernetes.api.model.ConfigMapBuilder;
|
||||
import io.fabric8.kubernetes.api.model.ConfigMapListBuilder;
|
||||
import io.fabric8.kubernetes.client.KubernetesClient;
|
||||
import io.fabric8.kubernetes.client.server.mock.EnableKubernetesMockClient;
|
||||
import io.fabric8.kubernetes.client.server.mock.KubernetesMockServer;
|
||||
import org.awaitility.Awaitility;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.boot.test.context.TestConfiguration;
|
||||
import org.springframework.boot.test.system.CapturedOutput;
|
||||
import org.springframework.boot.test.system.OutputCaptureExtension;
|
||||
import org.springframework.cloud.kubernetes.commons.KubernetesNamespaceProvider;
|
||||
import org.springframework.cloud.kubernetes.commons.config.ConfigMapConfigProperties;
|
||||
import org.springframework.cloud.kubernetes.commons.config.RetryProperties;
|
||||
import org.springframework.cloud.kubernetes.commons.config.reload.ConfigReloadProperties;
|
||||
import org.springframework.cloud.kubernetes.commons.config.reload.ConfigurationUpdateStrategy;
|
||||
import org.springframework.cloud.kubernetes.commons.config.reload.PollingConfigMapChangeDetector;
|
||||
import org.springframework.cloud.kubernetes.fabric8.config.Fabric8ConfigMapPropertySource;
|
||||
import org.springframework.cloud.kubernetes.fabric8.config.Fabric8ConfigMapPropertySourceLocator;
|
||||
import org.springframework.cloud.kubernetes.fabric8.config.VisibleFabric8ConfigMapPropertySourceLocator;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Primary;
|
||||
import org.springframework.core.env.AbstractEnvironment;
|
||||
import org.springframework.core.env.PropertySource;
|
||||
import org.springframework.mock.env.MockEnvironment;
|
||||
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
|
||||
|
||||
/**
|
||||
* @author wind57
|
||||
*/
|
||||
@SpringBootTest(
|
||||
properties = { "spring.main.allow-bean-definition-overriding=true",
|
||||
"logging.level.org.springframework.cloud.kubernetes.commons.config=debug" },
|
||||
classes = { PollingReloadConfigMapTest.TestConfig.class })
|
||||
@EnableKubernetesMockClient
|
||||
@ExtendWith(OutputCaptureExtension.class)
|
||||
class PollingReloadConfigMapTest {
|
||||
|
||||
private static final boolean FAIL_FAST = false;
|
||||
|
||||
private static final String CONFIG_MAP_NAME = "mine";
|
||||
|
||||
private static final String NAMESPACE = "spring-k8s";
|
||||
|
||||
private static KubernetesMockServer kubernetesMockServer;
|
||||
|
||||
private static KubernetesClient kubernetesClient;
|
||||
|
||||
private static final boolean[] strategyCalled = new boolean[] { false };
|
||||
|
||||
@BeforeAll
|
||||
static void beforeAll() {
|
||||
|
||||
kubernetesClient.getConfiguration().setRequestRetryBackoffLimit(0);
|
||||
|
||||
// needed so that our environment is populated with 'something'
|
||||
// this call is done in the method that returns the AbstractEnvironment
|
||||
ConfigMap configMapOne = configMap(CONFIG_MAP_NAME, Map.of());
|
||||
ConfigMap configMapTwo = configMap(CONFIG_MAP_NAME, Map.of("a", "b"));
|
||||
String path = "/api/v1/namespaces/spring-k8s/configmaps";
|
||||
kubernetesMockServer.expect()
|
||||
.withPath(path)
|
||||
.andReturn(200, new ConfigMapListBuilder().withItems(configMapOne).build())
|
||||
.once();
|
||||
|
||||
kubernetesMockServer.expect().withPath(path).andReturn(500, "Internal Server Error").once();
|
||||
|
||||
kubernetesMockServer.expect()
|
||||
.withPath(path)
|
||||
.andReturn(200, new ConfigMapListBuilder().withItems(configMapTwo).build())
|
||||
.once();
|
||||
}
|
||||
|
||||
/**
|
||||
* <pre>
|
||||
* - we have a PropertySource in the environment
|
||||
* - first polling cycle tries to read the sources from k8s and fails
|
||||
* - second polling cycle reads sources from k8s and finds a change
|
||||
* </pre>
|
||||
*/
|
||||
@Test
|
||||
void test(CapturedOutput output) {
|
||||
// we fail while reading 'configMapOne'
|
||||
Awaitility.await().atMost(Duration.ofSeconds(10)).pollInterval(Duration.ofSeconds(1)).until(() -> {
|
||||
boolean one = output.getOut().contains("failure in reading named sources");
|
||||
boolean two = output.getOut()
|
||||
.contains("there was an error while reading config maps/secrets, no reload will happen");
|
||||
boolean three = output.getOut()
|
||||
.contains("reloadable condition was not satisfied, reload will not be triggered");
|
||||
boolean updateStrategyNotCalled = !strategyCalled[0];
|
||||
return one && two && three && updateStrategyNotCalled;
|
||||
});
|
||||
|
||||
// it passes while reading 'configMapTwo'
|
||||
Awaitility.await()
|
||||
.atMost(Duration.ofSeconds(10))
|
||||
.pollInterval(Duration.ofSeconds(1))
|
||||
.until(() -> strategyCalled[0]);
|
||||
}
|
||||
|
||||
private static ConfigMap configMap(String name, Map<String, String> data) {
|
||||
return new ConfigMapBuilder().withNewMetadata().withName(name).endMetadata().withData(data).build();
|
||||
}
|
||||
|
||||
@TestConfiguration
|
||||
static class TestConfig {
|
||||
|
||||
@Bean
|
||||
@Primary
|
||||
PollingConfigMapChangeDetector pollingConfigMapChangeDetector(AbstractEnvironment environment,
|
||||
ConfigReloadProperties configReloadProperties, ConfigurationUpdateStrategy configurationUpdateStrategy,
|
||||
Fabric8ConfigMapPropertySourceLocator fabric8ConfigMapPropertySourceLocator) {
|
||||
ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
|
||||
scheduler.initialize();
|
||||
return new PollingConfigMapChangeDetector(environment, configReloadProperties, configurationUpdateStrategy,
|
||||
Fabric8ConfigMapPropertySource.class, fabric8ConfigMapPropertySourceLocator, scheduler);
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Primary
|
||||
AbstractEnvironment environment() {
|
||||
MockEnvironment mockEnvironment = new MockEnvironment();
|
||||
mockEnvironment.setProperty("spring.cloud.kubernetes.client.namespace", NAMESPACE);
|
||||
|
||||
// simulate that environment already has a Fabric8ConfigMapPropertySource,
|
||||
// otherwise we can't properly test reload functionality
|
||||
ConfigMapConfigProperties configMapConfigProperties = new ConfigMapConfigProperties(true, List.of(),
|
||||
List.of(), Map.of(), true, CONFIG_MAP_NAME, NAMESPACE, false, true, true, RetryProperties.DEFAULT);
|
||||
KubernetesNamespaceProvider namespaceProvider = new KubernetesNamespaceProvider(mockEnvironment);
|
||||
|
||||
PropertySource<?> propertySource = new VisibleFabric8ConfigMapPropertySourceLocator(kubernetesClient,
|
||||
configMapConfigProperties, namespaceProvider)
|
||||
.locate(mockEnvironment);
|
||||
|
||||
mockEnvironment.getPropertySources().addFirst(propertySource);
|
||||
return mockEnvironment;
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Primary
|
||||
ConfigReloadProperties configReloadProperties() {
|
||||
return new ConfigReloadProperties(true, true, false, ConfigReloadProperties.ReloadStrategy.REFRESH,
|
||||
ConfigReloadProperties.ReloadDetectionMode.POLLING, Duration.ofMillis(2000), Set.of("non-default"),
|
||||
false, Duration.ofSeconds(2));
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Primary
|
||||
ConfigMapConfigProperties configMapConfigProperties() {
|
||||
return new ConfigMapConfigProperties(true, List.of(), List.of(), Map.of(), true, CONFIG_MAP_NAME, NAMESPACE,
|
||||
false, true, FAIL_FAST, RetryProperties.DEFAULT);
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Primary
|
||||
KubernetesNamespaceProvider namespaceProvider(AbstractEnvironment environment) {
|
||||
return new KubernetesNamespaceProvider(environment);
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Primary
|
||||
ConfigurationUpdateStrategy configurationUpdateStrategy() {
|
||||
return new ConfigurationUpdateStrategy("to-console", () -> {
|
||||
strategyCalled[0] = true;
|
||||
});
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Primary
|
||||
Fabric8ConfigMapPropertySourceLocator fabric8ConfigMapPropertySourceLocator(
|
||||
ConfigMapConfigProperties configMapConfigProperties, KubernetesNamespaceProvider namespaceProvider) {
|
||||
return new VisibleFabric8ConfigMapPropertySourceLocator(kubernetesClient, configMapConfigProperties,
|
||||
namespaceProvider);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,212 @@
|
||||
/*
|
||||
* Copyright 2012-2024 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.fabric8.config.reload_it;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.Base64;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import io.fabric8.kubernetes.api.model.Secret;
|
||||
import io.fabric8.kubernetes.api.model.SecretBuilder;
|
||||
import io.fabric8.kubernetes.api.model.SecretListBuilder;
|
||||
import io.fabric8.kubernetes.client.KubernetesClient;
|
||||
import io.fabric8.kubernetes.client.server.mock.EnableKubernetesMockClient;
|
||||
import io.fabric8.kubernetes.client.server.mock.KubernetesMockServer;
|
||||
import org.awaitility.Awaitility;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.boot.test.context.TestConfiguration;
|
||||
import org.springframework.boot.test.system.CapturedOutput;
|
||||
import org.springframework.boot.test.system.OutputCaptureExtension;
|
||||
import org.springframework.cloud.kubernetes.commons.KubernetesNamespaceProvider;
|
||||
import org.springframework.cloud.kubernetes.commons.config.RetryProperties;
|
||||
import org.springframework.cloud.kubernetes.commons.config.SecretsConfigProperties;
|
||||
import org.springframework.cloud.kubernetes.commons.config.reload.ConfigReloadProperties;
|
||||
import org.springframework.cloud.kubernetes.commons.config.reload.ConfigurationUpdateStrategy;
|
||||
import org.springframework.cloud.kubernetes.commons.config.reload.PollingSecretsChangeDetector;
|
||||
import org.springframework.cloud.kubernetes.fabric8.config.Fabric8SecretsPropertySource;
|
||||
import org.springframework.cloud.kubernetes.fabric8.config.Fabric8SecretsPropertySourceLocator;
|
||||
import org.springframework.cloud.kubernetes.fabric8.config.VisibleFabric8SecretsPropertySourceLocator;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Primary;
|
||||
import org.springframework.core.env.AbstractEnvironment;
|
||||
import org.springframework.core.env.PropertySource;
|
||||
import org.springframework.mock.env.MockEnvironment;
|
||||
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
|
||||
|
||||
/**
|
||||
* @author wind57
|
||||
*/
|
||||
@SpringBootTest(
|
||||
properties = { "spring.main.allow-bean-definition-overriding=true",
|
||||
"logging.level.org.springframework.cloud.kubernetes.commons.config=debug" },
|
||||
classes = { PollingReloadSecretTest.TestConfig.class })
|
||||
@EnableKubernetesMockClient
|
||||
@ExtendWith(OutputCaptureExtension.class)
|
||||
|
||||
public class PollingReloadSecretTest {
|
||||
|
||||
private static final boolean FAIL_FAST = false;
|
||||
|
||||
private static final String SECRET_NAME = "mine";
|
||||
|
||||
private static final String NAMESPACE = "spring-k8s";
|
||||
|
||||
private static KubernetesMockServer kubernetesMockServer;
|
||||
|
||||
private static KubernetesClient kubernetesClient;
|
||||
|
||||
private static final boolean[] strategyCalled = new boolean[] { false };
|
||||
|
||||
@BeforeAll
|
||||
static void beforeAll() {
|
||||
|
||||
kubernetesClient.getConfiguration().setRequestRetryBackoffLimit(0);
|
||||
|
||||
// needed so that our environment is populated with 'something'
|
||||
// this call is done in the method that returns the AbstractEnvironment
|
||||
Secret secretOne = secret(SECRET_NAME, Map.of());
|
||||
Secret secretTwo = secret(SECRET_NAME, Map.of("a", "b"));
|
||||
String path = "/api/v1/namespaces/spring-k8s/secrets";
|
||||
kubernetesMockServer.expect()
|
||||
.withPath(path)
|
||||
.andReturn(200, new SecretListBuilder().withItems(secretOne).build())
|
||||
.once();
|
||||
|
||||
kubernetesMockServer.expect().withPath(path).andReturn(500, "Internal Server Error").once();
|
||||
|
||||
kubernetesMockServer.expect()
|
||||
.withPath(path)
|
||||
.andReturn(200, new SecretListBuilder().withItems(secretTwo).build())
|
||||
.once();
|
||||
}
|
||||
|
||||
/**
|
||||
* <pre>
|
||||
* - we have a PropertySource in the environment
|
||||
* - first polling cycle tries to read the sources from k8s and fails
|
||||
* - second polling cycle reads sources from k8s and finds a change
|
||||
* </pre>
|
||||
*/
|
||||
@Test
|
||||
void test(CapturedOutput output) {
|
||||
// we fail while reading 'secretOne'
|
||||
Awaitility.await().atMost(Duration.ofSeconds(10)).pollInterval(Duration.ofSeconds(1)).until(() -> {
|
||||
boolean one = output.getOut().contains("failure in reading named sources");
|
||||
boolean two = output.getOut()
|
||||
.contains("there was an error while reading config maps/secrets, no reload will happen");
|
||||
boolean three = output.getOut()
|
||||
.contains("reloadable condition was not satisfied, reload will not be triggered");
|
||||
boolean updateStrategyNotCalled = !strategyCalled[0];
|
||||
return one && two && three && updateStrategyNotCalled;
|
||||
});
|
||||
|
||||
// it passes while reading 'secretTwo'
|
||||
Awaitility.await()
|
||||
.atMost(Duration.ofSeconds(10))
|
||||
.pollInterval(Duration.ofSeconds(1))
|
||||
.until(() -> strategyCalled[0]);
|
||||
}
|
||||
|
||||
private static Secret secret(String name, Map<String, String> data) {
|
||||
Map<String, String> encoded = data.entrySet()
|
||||
.stream()
|
||||
.collect(Collectors.toMap(Map.Entry::getKey,
|
||||
e -> new String(Base64.getEncoder().encode(e.getValue().getBytes()))));
|
||||
return new SecretBuilder().withNewMetadata().withName(name).endMetadata().withData(encoded).build();
|
||||
}
|
||||
|
||||
@TestConfiguration
|
||||
static class TestConfig {
|
||||
|
||||
@Bean
|
||||
@Primary
|
||||
PollingSecretsChangeDetector pollingSecretsChangeDetector(AbstractEnvironment environment,
|
||||
ConfigReloadProperties configReloadProperties, ConfigurationUpdateStrategy configurationUpdateStrategy,
|
||||
Fabric8SecretsPropertySourceLocator fabric8SecretsPropertySourceLocator) {
|
||||
ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
|
||||
scheduler.initialize();
|
||||
return new PollingSecretsChangeDetector(environment, configReloadProperties, configurationUpdateStrategy,
|
||||
Fabric8SecretsPropertySource.class, fabric8SecretsPropertySourceLocator, scheduler);
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Primary
|
||||
AbstractEnvironment environment() {
|
||||
MockEnvironment mockEnvironment = new MockEnvironment();
|
||||
mockEnvironment.setProperty("spring.cloud.kubernetes.client.namespace", NAMESPACE);
|
||||
|
||||
// simulate that environment already has a Fabric8SecretsPropertySource,
|
||||
// otherwise we can't properly test reload functionality
|
||||
SecretsConfigProperties secretsConfigProperties = new SecretsConfigProperties(true, Map.of(), List.of(),
|
||||
List.of(), true, SECRET_NAME, NAMESPACE, false, true, true, RetryProperties.DEFAULT);
|
||||
KubernetesNamespaceProvider namespaceProvider = new KubernetesNamespaceProvider(mockEnvironment);
|
||||
|
||||
PropertySource<?> propertySource = new VisibleFabric8SecretsPropertySourceLocator(kubernetesClient,
|
||||
secretsConfigProperties, namespaceProvider)
|
||||
.locate(mockEnvironment);
|
||||
|
||||
mockEnvironment.getPropertySources().addFirst(propertySource);
|
||||
return mockEnvironment;
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Primary
|
||||
ConfigReloadProperties configReloadProperties() {
|
||||
return new ConfigReloadProperties(true, true, true, ConfigReloadProperties.ReloadStrategy.REFRESH,
|
||||
ConfigReloadProperties.ReloadDetectionMode.POLLING, Duration.ofMillis(2000), Set.of("non-default"),
|
||||
false, Duration.ofSeconds(2));
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Primary
|
||||
SecretsConfigProperties secretsConfigProperties() {
|
||||
return new SecretsConfigProperties(true, Map.of(), List.of(), List.of(), true, SECRET_NAME, NAMESPACE,
|
||||
false, true, FAIL_FAST, RetryProperties.DEFAULT);
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Primary
|
||||
KubernetesNamespaceProvider namespaceProvider(AbstractEnvironment environment) {
|
||||
return new KubernetesNamespaceProvider(environment);
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Primary
|
||||
ConfigurationUpdateStrategy configurationUpdateStrategy() {
|
||||
return new ConfigurationUpdateStrategy("to-console", () -> {
|
||||
strategyCalled[0] = true;
|
||||
});
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Primary
|
||||
Fabric8SecretsPropertySourceLocator fabric8SecretsPropertySourceLocator(
|
||||
SecretsConfigProperties secretsConfigProperties, KubernetesNamespaceProvider namespaceProvider) {
|
||||
return new VisibleFabric8SecretsPropertySourceLocator(kubernetesClient, secretsConfigProperties,
|
||||
namespaceProvider);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user