Refactor fabric8 catalog watch integration tests (1) (#1755)

This commit is contained in:
erabii
2024-10-15 17:21:50 +03:00
committed by GitHub
parent 7b62798d91
commit a27bf65c55
16 changed files with 617 additions and 624 deletions

View File

@@ -77,6 +77,7 @@
</execution>
</executions>
<configuration>
<classesDirectory>${project.build.outputDirectory}</classesDirectory>
<includes>
<include>${testsToRun}</include>
</includes>

View File

@@ -42,6 +42,22 @@
<filtering>true</filtering>
</resource>
</resources>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<executions>
<execution>
<id>build-image</id>
<configuration>
<skip>true</skip>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

View File

@@ -0,0 +1,75 @@
/*
* 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.catalog.watch;
import java.util.Map;
import java.util.Set;
import io.fabric8.kubernetes.client.Config;
import io.fabric8.kubernetes.client.KubernetesClient;
import io.fabric8.kubernetes.client.KubernetesClientBuilder;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.extension.ExtendWith;
import org.testcontainers.k3s.K3sContainer;
import org.springframework.boot.test.system.OutputCaptureExtension;
import org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscoveryProperties;
import org.springframework.cloud.kubernetes.integration.tests.commons.Commons;
import org.springframework.cloud.kubernetes.integration.tests.commons.fabric8_client.Util;
import org.springframework.test.context.TestPropertySource;
/**
* @author wind57
*/
@TestPropertySource(
properties = { "spring.main.cloud-platform=kubernetes", "spring.cloud.config.import-check.enabled=false",
"spring.cloud.kubernetes.discovery.catalogServicesWatchDelay=2000",
"spring.cloud.kubernetes.client.namespace=default",
"logging.level.org.springframework.cloud.kubernetes.fabric8.discovery=DEBUG" })
@ExtendWith(OutputCaptureExtension.class)
abstract class Fabric8CatalogWatchBase {
protected static final String NAMESPACE = "default";
protected static final String NAMESPACE_A = "a";
protected static final String NAMESPACE_B = "b";
protected static final K3sContainer K3S = Commons.container();
protected static Util util;
@BeforeAll
protected static void beforeAll() {
K3S.start();
util = new Util(K3S);
}
protected static KubernetesDiscoveryProperties discoveryProperties(boolean useEndpointSlices) {
return new KubernetesDiscoveryProperties(true, false, Set.of(NAMESPACE, NAMESPACE_A), true, 60, false, null,
Set.of(443, 8443), Map.of(), null, KubernetesDiscoveryProperties.Metadata.DEFAULT, 0, useEndpointSlices,
false, null);
}
protected static KubernetesClient client() {
String kubeConfigYaml = K3S.getKubeConfigYaml();
Config config = Config.fromKubeconfig(kubeConfigYaml);
return new KubernetesClientBuilder().withConfig(config).build();
}
}

View File

@@ -1,254 +0,0 @@
/*
* 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.fabric8.catalog.watch;
import java.io.InputStream;
import java.time.Duration;
import java.util.List;
import java.util.Set;
import io.fabric8.kubernetes.api.model.Service;
import io.fabric8.kubernetes.api.model.apps.Deployment;
import io.fabric8.kubernetes.api.model.networking.v1.Ingress;
import io.fabric8.kubernetes.client.utils.Serialization;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.testcontainers.k3s.K3sContainer;
import org.springframework.cloud.kubernetes.commons.discovery.EndpointNameAndNamespace;
import org.springframework.cloud.kubernetes.integration.tests.commons.Commons;
import org.springframework.cloud.kubernetes.integration.tests.commons.Images;
import org.springframework.cloud.kubernetes.integration.tests.commons.Phase;
import org.springframework.cloud.kubernetes.integration.tests.commons.fabric8_client.Util;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.core.ResolvableType;
import org.springframework.http.HttpMethod;
import org.springframework.web.reactive.function.client.WebClient;
import static org.awaitility.Awaitility.await;
import static org.springframework.cloud.kubernetes.fabric8.catalog.watch.Fabric8CatalogWatchUtil.builder;
import static org.springframework.cloud.kubernetes.fabric8.catalog.watch.Fabric8CatalogWatchUtil.patchForEndpointSlices;
import static org.springframework.cloud.kubernetes.fabric8.catalog.watch.Fabric8CatalogWatchUtil.patchForNamespaceFilterAndEndpointSlices;
import static org.springframework.cloud.kubernetes.fabric8.catalog.watch.Fabric8CatalogWatchUtil.patchForNamespaceFilterAndEndpoints;
import static org.springframework.cloud.kubernetes.fabric8.catalog.watch.Fabric8CatalogWatchUtil.retrySpec;
import static org.springframework.cloud.kubernetes.integration.tests.commons.Commons.pomVersion;
/**
* @author wind57
*/
class Fabric8CatalogWatchIT {
private static final String NAMESPACE = "default";
public static final String NAMESPACE_A = "namespacea";
public static final String NAMESPACE_B = "namespaceb";
private static final String IMAGE_NAME = "spring-cloud-kubernetes-fabric8-client-catalog-watcher";
private static final String DOCKER_IMAGE = "docker.io/springcloud/" + IMAGE_NAME + ":" + pomVersion();
private static final K3sContainer K3S = Commons.container();
private static Util util;
@BeforeAll
static void beforeAll() throws Exception {
K3S.start();
Commons.validateImage(IMAGE_NAME, K3S);
Commons.loadSpringCloudKubernetesImage(IMAGE_NAME, K3S);
Images.loadBusybox(K3S);
util = new Util(K3S);
util.createNamespace(NAMESPACE_A);
util.createNamespace(NAMESPACE_B);
util.setUp(NAMESPACE);
util.setUpClusterWide(NAMESPACE, Set.of(NAMESPACE, NAMESPACE_A, NAMESPACE_B));
util.busybox(NAMESPACE, Phase.CREATE);
app(Phase.CREATE);
}
@AfterAll
static void afterAll() {
util.deleteNamespace(NAMESPACE_A);
util.deleteNamespace(NAMESPACE_B);
app(Phase.DELETE);
Commons.systemPrune();
}
/**
* <pre>
* - we deploy a busybox service with 2 replica pods
* - we receive an event from KubernetesCatalogWatcher, assert what is inside it
* - delete the busybox service
* - assert that we receive only spring-cloud-kubernetes-fabric8-client-catalog-watcher pod
* </pre>
*/
@Test
void testCatalogWatchWithEndpoints() throws Exception {
assertLogStatement();
test();
testCatalogWatchWithEndpointSlices();
testCatalogWatchWithNamespaceFilterAndEndpoints();
testCatalogWatchWithNamespaceFilterAndEndpointSlices();
}
void testCatalogWatchWithEndpointSlices() {
util.busybox(NAMESPACE, Phase.CREATE);
patchForEndpointSlices(util, DOCKER_IMAGE, IMAGE_NAME, NAMESPACE);
Commons.waitForLogStatement("stateGenerator is of type: Fabric8EndpointSliceV1CatalogWatch", K3S, IMAGE_NAME);
test();
}
void testCatalogWatchWithNamespaceFilterAndEndpoints() {
util.busybox(NAMESPACE_A, Phase.CREATE);
util.busybox(NAMESPACE_B, Phase.CREATE);
patchForNamespaceFilterAndEndpoints(util, DOCKER_IMAGE, IMAGE_NAME, NAMESPACE);
Fabric8CatalogWatchWithNamespacesDelegate.testCatalogWatchWithNamespaceFilterAndEndpoints(K3S, IMAGE_NAME,
util);
}
void testCatalogWatchWithNamespaceFilterAndEndpointSlices() {
util.busybox(NAMESPACE_A, Phase.CREATE);
util.busybox(NAMESPACE_B, Phase.CREATE);
patchForNamespaceFilterAndEndpointSlices(util, DOCKER_IMAGE, IMAGE_NAME, NAMESPACE);
Fabric8CatalogWatchWithNamespacesDelegate.testCatalogWatchWithNamespaceFilterAndEndpointSlices(K3S, IMAGE_NAME,
util);
}
/**
* we log in debug mode the type of the StateGenerator we use, be that Endpoints or
* EndpointSlices. Here we make sure that in the test we actually use the correct
* type.
*/
private void assertLogStatement() throws Exception {
String appPodName = K3S
.execInContainer("kubectl", "get", "pods", "-l",
"app=spring-cloud-kubernetes-fabric8-client-catalog-watcher", "-o=name", "--no-headers")
.getStdout();
String allLogs = K3S.execInContainer("kubectl", "logs", appPodName.trim()).getStdout();
Assertions.assertTrue(allLogs.contains("stateGenerator is of type: Fabric8EndpointsCatalogWatch"));
}
/**
* the test is the same for both endpoints and endpoint slices, the set-up for them is
* different.
*/
@SuppressWarnings("unchecked")
private void test() {
WebClient client = builder().baseUrl("http://localhost/result").build();
EndpointNameAndNamespace[] holder = new EndpointNameAndNamespace[2];
ResolvableType resolvableType = ResolvableType.forClassWithGenerics(List.class, EndpointNameAndNamespace.class);
await().pollInterval(Duration.ofSeconds(1)).atMost(Duration.ofSeconds(240)).until(() -> {
List<EndpointNameAndNamespace> result = (List<EndpointNameAndNamespace>) client.method(HttpMethod.GET)
.retrieve()
.bodyToMono(ParameterizedTypeReference.forType(resolvableType.getType()))
.retryWhen(retrySpec())
.block();
// we get 3 pods as input, but because they are sorted by name in the catalog
// watcher implementation
// we will get the first busybox instances here.
if (result != null) {
if (result.size() != 3) {
return false;
}
holder[0] = result.get(0);
holder[1] = result.get(1);
return true;
}
return false;
});
EndpointNameAndNamespace resultOne = holder[0];
EndpointNameAndNamespace resultTwo = holder[1];
Assertions.assertNotNull(resultOne);
Assertions.assertNotNull(resultTwo);
Assertions.assertTrue(resultOne.endpointName().contains("busybox"));
Assertions.assertTrue(resultTwo.endpointName().contains("busybox"));
Assertions.assertEquals("default", resultOne.namespace());
Assertions.assertEquals("default", resultTwo.namespace());
util.busybox(NAMESPACE, Phase.DELETE);
// what we get after delete
EndpointNameAndNamespace[] afterDelete = new EndpointNameAndNamespace[1];
await().pollInterval(Duration.ofSeconds(1)).atMost(Duration.ofSeconds(240)).until(() -> {
List<EndpointNameAndNamespace> result = (List<EndpointNameAndNamespace>) client.method(HttpMethod.GET)
.retrieve()
.bodyToMono(ParameterizedTypeReference.forType(resolvableType.getType()))
.retryWhen(retrySpec())
.block();
// we need to get the event from KubernetesCatalogWatch, but that happens
// on periodic bases. So in order to be sure we got the event we care about
// we wait until the result has a single entry, which means busybox was
// deleted
// + KubernetesCatalogWatch received the new update.
if (result != null && result.size() != 1) {
return false;
}
// we will only receive one pod here, our own
if (result != null) {
afterDelete[0] = result.get(0);
return true;
}
return false;
});
Assertions.assertTrue(afterDelete[0].endpointName().contains(IMAGE_NAME));
Assertions.assertEquals("default", afterDelete[0].namespace());
}
private static void app(Phase phase) {
InputStream endpointsDeploymentStream = util.inputStream("app/watcher-endpoints-deployment.yaml");
InputStream serviceStream = util.inputStream("app/watcher-service.yaml");
InputStream ingressStream = util.inputStream("app/watcher-ingress.yaml");
Deployment deployment = Serialization.unmarshal(endpointsDeploymentStream, Deployment.class);
Service service = Serialization.unmarshal(serviceStream, Service.class);
Ingress ingress = Serialization.unmarshal(ingressStream, Ingress.class);
if (phase.equals(Phase.CREATE)) {
util.createAndWait(Fabric8CatalogWatchIT.NAMESPACE, null, deployment, service, ingress, true);
}
else {
util.deleteAndWait(Fabric8CatalogWatchIT.NAMESPACE, deployment, service, ingress);
}
}
}

View File

@@ -1,156 +0,0 @@
/*
* Copyright 2013-2023 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.catalog.watch;
import java.time.Duration;
import java.util.Map;
import java.util.Objects;
import reactor.netty.http.client.HttpClient;
import reactor.util.retry.Retry;
import reactor.util.retry.RetryBackoffSpec;
import org.springframework.cloud.kubernetes.integration.tests.commons.fabric8_client.Util;
import org.springframework.http.client.reactive.ReactorClientHttpConnector;
import org.springframework.web.reactive.function.client.WebClient;
/**
* @author wind57
*/
final class Fabric8CatalogWatchUtil {
private static final Map<String, String> POD_LABELS = Map.of("app",
"spring-cloud-kubernetes-fabric8-client-catalog-watcher");
private Fabric8CatalogWatchUtil() {
}
static final String BODY_ONE = """
{
"spec": {
"template": {
"spec": {
"containers": [{
"name": "spring-cloud-kubernetes-fabric8-client-catalog-watcher",
"image": "image_name_here",
"env": [
{
"name": "LOGGING_LEVEL_ORG_SPRINGFRAMEWORK_CLOUD_KUBERNETES_FABRIC8_DISCOVERY",
"value": "DEBUG"
},
{
"name": "SPRING_CLOUD_KUBERNETES_DISCOVERY_USE_ENDPOINT_SLICES",
"value": "TRUE"
}
]
}]
}
}
}
}
""";
static final String BODY_TWO = """
{
"spec": {
"template": {
"spec": {
"containers": [{
"name": "spring-cloud-kubernetes-fabric8-client-catalog-watcher",
"image": "image_name_here",
"env": [
{
"name": "LOGGING_LEVEL_ORG_SPRINGFRAMEWORK_CLOUD_KUBERNETES_FABRIC8_DISCOVERY",
"value": "DEBUG"
},
{
"name": "SPRING_CLOUD_KUBERNETES_DISCOVERY_USE_ENDPOINT_SLICES",
"value": "FALSE"
},
{
"name": "SPRING_CLOUD_KUBERNETES_DISCOVERY_NAMESPACES_0",
"value": "namespacea"
},
{
"name": "SPRING_CLOUD_KUBERNETES_DISCOVERY_NAMESPACES_1",
"value": "default"
}
]
}]
}
}
}
}
""";
static final String BODY_THREE = """
{
"spec": {
"template": {
"spec": {
"containers": [{
"name": "spring-cloud-kubernetes-fabric8-client-catalog-watcher",
"image": "image_name_here",
"env": [
{
"name": "LOGGING_LEVEL_ORG_SPRINGFRAMEWORK_CLOUD_KUBERNETES_FABRIC8_DISCOVERY",
"value": "DEBUG"
},
{
"name": "SPRING_CLOUD_KUBERNETES_DISCOVERY_USE_ENDPOINT_SLICES",
"value": "TRUE"
},
{
"name": "SPRING_CLOUD_KUBERNETES_DISCOVERY_NAMESPACES_0",
"value": "namespacea"
},
{
"name": "SPRING_CLOUD_KUBERNETES_DISCOVERY_NAMESPACES_1",
"value": "default"
}
]
}]
}
}
}
}
""";
static void patchForEndpointSlices(Util util, String dockerImage, String deploymentName, String namespace) {
util.patchWithReplace(dockerImage, deploymentName, namespace, BODY_ONE, POD_LABELS);
}
static void patchForNamespaceFilterAndEndpoints(Util util, String dockerImage, String deploymentName,
String namespace) {
util.patchWithReplace(dockerImage, deploymentName, namespace, BODY_TWO, POD_LABELS);
}
static void patchForNamespaceFilterAndEndpointSlices(Util util, String dockerImage, String deploymentName,
String namespace) {
util.patchWithReplace(dockerImage, deploymentName, namespace, BODY_THREE, POD_LABELS);
}
static WebClient.Builder builder() {
return WebClient.builder().clientConnector(new ReactorClientHttpConnector(HttpClient.create()));
}
static RetryBackoffSpec retrySpec() {
return Retry.fixedDelay(15, Duration.ofSeconds(1)).filter(Objects::nonNull);
}
}

View File

@@ -0,0 +1,101 @@
/*
* 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.catalog.watch;
import java.util.Set;
import io.fabric8.kubernetes.client.KubernetesClient;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
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.web.server.LocalServerPort;
import org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscoveryProperties;
import org.springframework.cloud.kubernetes.fabric8.discovery.KubernetesCatalogWatchAutoConfiguration;
import org.springframework.cloud.kubernetes.integration.tests.commons.Images;
import org.springframework.cloud.kubernetes.integration.tests.commons.Phase;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Primary;
import static org.springframework.cloud.kubernetes.fabric8.catalog.watch.Fabric8CatalogWatchWithEndpointSlicesAndNamespaceFilterIT.TestConfig;
/**
* @author wind57
*/
@SpringBootTest(classes = { KubernetesCatalogWatchAutoConfiguration.class, TestConfig.class, Application.class },
webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class Fabric8CatalogWatchWithEndpointSlicesAndNamespaceFilterIT extends Fabric8CatalogWatchBase {
@LocalServerPort
private int port;
@BeforeEach
void beforeEach() {
util.createNamespace(NAMESPACE_A);
util.createNamespace(NAMESPACE_B);
Images.loadBusybox(K3S);
util.busybox(NAMESPACE_A, Phase.CREATE);
util.busybox(NAMESPACE_B, Phase.CREATE);
}
@AfterEach
void afterEach() {
util.deleteNamespace(NAMESPACE_A);
util.deleteNamespace(NAMESPACE_B);
}
/**
* <pre>
* - we deploy a busybox service with 2 replica pods in two namespaces : a, b
* - we use endpoint slices
* - we enable namespace filtering for 'default' and 'a'
* - we receive an event from KubernetesCatalogWatcher, assert what is inside it
* - delete the busybox service in 'a' and 'b'
* - assert that we receive an empty response
* </pre>
*/
@Test
void test(CapturedOutput output) {
TestAssertions.assertLogStatement(output, "stateGenerator is of type: Fabric8EndpointSliceV1CatalogWatch");
TestAssertions.invokeAndAssert(util, Set.of(NAMESPACE_A, NAMESPACE_B), port, NAMESPACE_A);
}
@TestConfiguration
static class TestConfig {
@Bean
@Primary
KubernetesClient kubernetesClient() {
return client();
}
@Bean
@Primary
KubernetesDiscoveryProperties kubernetesDiscoveryProperties() {
return discoveryProperties(true);
}
}
}

View File

@@ -0,0 +1,86 @@
/*
* 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.catalog.watch;
import java.util.Set;
import io.fabric8.kubernetes.client.KubernetesClient;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
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.web.server.LocalServerPort;
import org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscoveryProperties;
import org.springframework.cloud.kubernetes.fabric8.discovery.KubernetesCatalogWatchAutoConfiguration;
import org.springframework.cloud.kubernetes.integration.tests.commons.Images;
import org.springframework.cloud.kubernetes.integration.tests.commons.Phase;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Primary;
import static org.springframework.cloud.kubernetes.fabric8.catalog.watch.Fabric8CatalogWatchWithEndpointSlicesIT.TestConfig;
/**
* @author wind57
*/
@SpringBootTest(classes = { KubernetesCatalogWatchAutoConfiguration.class, TestConfig.class, Application.class },
webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class Fabric8CatalogWatchWithEndpointSlicesIT extends Fabric8CatalogWatchBase {
@LocalServerPort
private int port;
@BeforeEach
void beforeEach() {
Images.loadBusybox(K3S);
util.busybox(NAMESPACE, Phase.CREATE);
}
/**
* <pre>
* - we deploy a busybox service with 2 replica pods
* - we use endpoint slices
* - we receive an event from KubernetesCatalogWatcher, assert what is inside it
* - delete the busybox service
* - assert that we receive an empty response
* </pre>
*/
@Test
void test(CapturedOutput output) {
TestAssertions.assertLogStatement(output, "stateGenerator is of type: Fabric8EndpointSliceV1CatalogWatch");
TestAssertions.invokeAndAssert(util, Set.of(NAMESPACE), port, NAMESPACE);
}
@TestConfiguration
static class TestConfig {
@Bean
@Primary
KubernetesClient kubernetesClient() {
return client();
}
@Bean
@Primary
KubernetesDiscoveryProperties kubernetesDiscoveryProperties() {
return discoveryProperties(true);
}
}
}

View File

@@ -0,0 +1,101 @@
/*
* 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.catalog.watch;
import java.util.Set;
import io.fabric8.kubernetes.client.KubernetesClient;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
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.web.server.LocalServerPort;
import org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscoveryProperties;
import org.springframework.cloud.kubernetes.fabric8.discovery.KubernetesCatalogWatchAutoConfiguration;
import org.springframework.cloud.kubernetes.integration.tests.commons.Images;
import org.springframework.cloud.kubernetes.integration.tests.commons.Phase;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Primary;
import static org.springframework.cloud.kubernetes.fabric8.catalog.watch.Fabric8CatalogWatchWithEndpointsAndNamespaceFilterIT.TestConfig;
/**
* @author wind57
*/
@SpringBootTest(classes = { KubernetesCatalogWatchAutoConfiguration.class, TestConfig.class, Application.class },
webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class Fabric8CatalogWatchWithEndpointsAndNamespaceFilterIT extends Fabric8CatalogWatchBase {
@LocalServerPort
private int port;
@BeforeEach
void beforeEach() {
util.createNamespace(NAMESPACE_A);
util.createNamespace(NAMESPACE_B);
Images.loadBusybox(K3S);
util.busybox(NAMESPACE_A, Phase.CREATE);
util.busybox(NAMESPACE_B, Phase.CREATE);
}
@AfterEach
void afterEach() {
util.deleteNamespace(NAMESPACE_A);
util.deleteNamespace(NAMESPACE_B);
}
/**
* <pre>
* - we deploy a busybox service with 2 replica pods in two namespaces : a, b
* - we use endpoints
* - we enable namespace filtering for 'default' and 'a'
* - we receive an event from KubernetesCatalogWatcher, assert what is inside it
* - delete the busybox service in 'a' and 'b'
* - assert that we receive an empty response
* </pre>
*/
@Test
void test(CapturedOutput output) {
TestAssertions.assertLogStatement(output, "stateGenerator is of type: Fabric8EndpointsCatalogWatch");
TestAssertions.invokeAndAssert(util, Set.of(NAMESPACE_A, NAMESPACE_B), port, NAMESPACE_A);
}
@TestConfiguration
static class TestConfig {
@Bean
@Primary
KubernetesClient kubernetesClient() {
return client();
}
@Bean
@Primary
KubernetesDiscoveryProperties kubernetesDiscoveryProperties() {
return discoveryProperties(false);
}
}
}

View File

@@ -0,0 +1,86 @@
/*
* 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.catalog.watch;
import java.util.Set;
import io.fabric8.kubernetes.client.KubernetesClient;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
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.web.server.LocalServerPort;
import org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscoveryProperties;
import org.springframework.cloud.kubernetes.fabric8.discovery.KubernetesCatalogWatchAutoConfiguration;
import org.springframework.cloud.kubernetes.integration.tests.commons.Images;
import org.springframework.cloud.kubernetes.integration.tests.commons.Phase;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Primary;
import static org.springframework.cloud.kubernetes.fabric8.catalog.watch.Fabric8CatalogWatchWithEndpointsIT.TestConfig;
/**
* @author wind57
*/
@SpringBootTest(classes = { KubernetesCatalogWatchAutoConfiguration.class, TestConfig.class, Application.class },
webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class Fabric8CatalogWatchWithEndpointsIT extends Fabric8CatalogWatchBase {
@LocalServerPort
private int port;
@BeforeEach
void beforeEach() {
Images.loadBusybox(K3S);
util.busybox(NAMESPACE, Phase.CREATE);
}
/**
* <pre>
* - we deploy a busybox service with 2 replica pods
* - we use endpoints
* - we receive an event from KubernetesCatalogWatcher, assert what is inside it
* - delete the busybox service
* - assert that we receive an empty response
* </pre>
*/
@Test
void test(CapturedOutput output) {
TestAssertions.assertLogStatement(output, "stateGenerator is of type: Fabric8EndpointsCatalogWatch");
TestAssertions.invokeAndAssert(util, Set.of(NAMESPACE), port, NAMESPACE);
}
@TestConfiguration
static class TestConfig {
@Bean
@Primary
KubernetesClient kubernetesClient() {
return client();
}
@Bean
@Primary
KubernetesDiscoveryProperties kubernetesDiscoveryProperties() {
return discoveryProperties(false);
}
}
}

View File

@@ -1,150 +0,0 @@
/*
* 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.fabric8.catalog.watch;
import java.time.Duration;
import java.util.List;
import org.junit.jupiter.api.Assertions;
import org.testcontainers.k3s.K3sContainer;
import org.springframework.cloud.kubernetes.commons.discovery.EndpointNameAndNamespace;
import org.springframework.cloud.kubernetes.integration.tests.commons.Commons;
import org.springframework.cloud.kubernetes.integration.tests.commons.Phase;
import org.springframework.cloud.kubernetes.integration.tests.commons.fabric8_client.Util;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.core.ResolvableType;
import org.springframework.http.HttpMethod;
import org.springframework.web.reactive.function.client.WebClient;
import static org.awaitility.Awaitility.await;
import static org.springframework.cloud.kubernetes.fabric8.catalog.watch.Fabric8CatalogWatchIT.NAMESPACE_A;
import static org.springframework.cloud.kubernetes.fabric8.catalog.watch.Fabric8CatalogWatchIT.NAMESPACE_B;
import static org.springframework.cloud.kubernetes.fabric8.catalog.watch.Fabric8CatalogWatchUtil.builder;
import static org.springframework.cloud.kubernetes.fabric8.catalog.watch.Fabric8CatalogWatchUtil.retrySpec;
/**
* @author wind57
*/
final class Fabric8CatalogWatchWithNamespacesDelegate {
private Fabric8CatalogWatchWithNamespacesDelegate() {
}
private static final String APP_NAME = "spring-cloud-kubernetes-fabric8-client-catalog-watcher";
/**
* <pre>
* - we deploy one busybox service with 2 replica pods in namespace namespacea
* - we deploy one busybox service with 2 replica pods in namespace namespaceb
* - we enable the search to be made in namespacea and default ones
* - we receive an event from KubernetesCatalogWatcher, assert what is inside it
* - delete both busybox services in namespacea and namespaceb
* - assert that we receive only spring-cloud-kubernetes-fabric8-client-catalog-watcher pod
* </pre>
*/
static void testCatalogWatchWithNamespaceFilterAndEndpoints(K3sContainer container, String imageName, Util util) {
Commons.waitForLogStatement("stateGenerator is of type: Fabric8EndpointsCatalogWatch", container, imageName);
test(util);
}
static void testCatalogWatchWithNamespaceFilterAndEndpointSlices(K3sContainer container, String imageName,
Util util) {
Commons.waitForLogStatement("stateGenerator is of type: Fabric8EndpointSliceV1CatalogWatch", container,
imageName);
test(util);
}
/**
* the test is the same for both endpoints and endpoint slices, the set-up for them is
* different.
*/
@SuppressWarnings("unchecked")
private static void test(Util util) {
WebClient client = builder().baseUrl("http://localhost/result").build();
EndpointNameAndNamespace[] holder = new EndpointNameAndNamespace[2];
ResolvableType resolvableType = ResolvableType.forClassWithGenerics(List.class, EndpointNameAndNamespace.class);
await().pollInterval(Duration.ofSeconds(1)).atMost(Duration.ofSeconds(240)).until(() -> {
List<EndpointNameAndNamespace> result = (List<EndpointNameAndNamespace>) client.method(HttpMethod.GET)
.retrieve()
.bodyToMono(ParameterizedTypeReference.forType(resolvableType.getType()))
.retryWhen(retrySpec())
.block();
// we get 3 pods as input, but because they are sorted by name in the catalog
// watcher implementation
// we will get the first busybox instances here.
if (result != null) {
holder[0] = result.get(0);
holder[1] = result.get(1);
return true;
}
return false;
});
EndpointNameAndNamespace resultOne = holder[0];
EndpointNameAndNamespace resultTwo = holder[1];
Assertions.assertNotNull(resultOne);
Assertions.assertNotNull(resultTwo);
Assertions.assertTrue(resultOne.endpointName().contains("busybox"));
Assertions.assertTrue(resultTwo.endpointName().contains("busybox"));
Assertions.assertEquals(NAMESPACE_A, resultOne.namespace());
Assertions.assertEquals(NAMESPACE_A, resultTwo.namespace());
util.busybox(NAMESPACE_A, Phase.DELETE);
util.busybox(NAMESPACE_B, Phase.DELETE);
// what we get after delete
EndpointNameAndNamespace[] afterDelete = new EndpointNameAndNamespace[1];
await().pollInterval(Duration.ofSeconds(1)).atMost(Duration.ofSeconds(240)).until(() -> {
List<EndpointNameAndNamespace> result = (List<EndpointNameAndNamespace>) client.method(HttpMethod.GET)
.retrieve()
.bodyToMono(ParameterizedTypeReference.forType(resolvableType.getType()))
.retryWhen(retrySpec())
.block();
// we need to get the event from KubernetesCatalogWatch, but that happens
// on periodic bases. So in order to be sure we got the event we care about
// we wait until the result has a single entry, which means busybox was
// deleted
// + KubernetesCatalogWatch received the new update.
if (result != null && result.size() != 1) {
return false;
}
// we will only receive one pod here, our own
if (result != null) {
afterDelete[0] = result.get(0);
return true;
}
return false;
});
Assertions.assertTrue(afterDelete[0].endpointName().contains(APP_NAME));
Assertions.assertEquals("default", afterDelete[0].namespace());
}
}

View File

@@ -0,0 +1,127 @@
/*
* 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.catalog.watch;
import java.time.Duration;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import org.assertj.core.api.Assertions;
import org.awaitility.Awaitility;
import reactor.netty.http.client.HttpClient;
import reactor.util.retry.Retry;
import reactor.util.retry.RetryBackoffSpec;
import org.springframework.boot.test.system.CapturedOutput;
import org.springframework.cloud.kubernetes.commons.discovery.EndpointNameAndNamespace;
import org.springframework.cloud.kubernetes.integration.tests.commons.Phase;
import org.springframework.cloud.kubernetes.integration.tests.commons.fabric8_client.Util;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.core.ResolvableType;
import org.springframework.http.HttpMethod;
import org.springframework.http.client.reactive.ReactorClientHttpConnector;
import org.springframework.web.reactive.function.client.WebClient;
import static org.assertj.core.api.Assertions.assertThat;
import static org.awaitility.Awaitility.await;
/**
* @author wind57
*/
final class TestAssertions {
private TestAssertions() {
}
static void assertLogStatement(CapturedOutput output, String textToAssert) {
Awaitility.await()
.during(Duration.ofSeconds(5))
.pollInterval(Duration.ofMillis(200))
.untilAsserted(() -> Assertions.assertThat(output.getOut()).contains(textToAssert));
}
/**
* the checks are the same for both endpoints and endpoint slices, while the set-up
* for them is different.
*/
@SuppressWarnings("unchecked")
static void invokeAndAssert(Util util, Set<String> namespaces, int port, String assertionNamespace) {
WebClient client = builder().baseUrl("http://localhost:" + port + "/result").build();
EndpointNameAndNamespace[] holder = new EndpointNameAndNamespace[2];
ResolvableType resolvableType = ResolvableType.forClassWithGenerics(List.class, EndpointNameAndNamespace.class);
await().pollInterval(Duration.ofMillis(200)).atMost(Duration.ofSeconds(30)).until(() -> {
List<EndpointNameAndNamespace> result = (List<EndpointNameAndNamespace>) client.method(HttpMethod.GET)
.retrieve()
.bodyToMono(ParameterizedTypeReference.forType(resolvableType.getType()))
.retryWhen(retrySpec())
.block();
if (result != null) {
if (result.size() != 2) {
return false;
}
holder[0] = result.get(0);
holder[1] = result.get(1);
return true;
}
return false;
});
EndpointNameAndNamespace resultOne = holder[0];
EndpointNameAndNamespace resultTwo = holder[1];
assertThat(resultOne).isNotNull();
assertThat(resultTwo).isNotNull();
assertThat(resultOne.endpointName()).contains("busybox");
assertThat(resultTwo.endpointName()).contains("busybox");
assertThat(resultOne.namespace()).isEqualTo(assertionNamespace);
assertThat(resultTwo.namespace()).isEqualTo(assertionNamespace);
namespaces.forEach(namespace -> util.busybox(namespace, Phase.DELETE));
await().pollInterval(Duration.ofSeconds(1)).atMost(Duration.ofSeconds(240)).until(() -> {
List<EndpointNameAndNamespace> result = (List<EndpointNameAndNamespace>) client.method(HttpMethod.GET)
.retrieve()
.bodyToMono(ParameterizedTypeReference.forType(resolvableType.getType()))
.retryWhen(retrySpec())
.block();
// we need to get the event from KubernetesCatalogWatch, but that happens
// on periodic bases. So in order to be sure we got the event we care about
// we wait until there is no entry, which means busybox was deleted
// and KubernetesCatalogWatch received that update.
return Objects.requireNonNull(result).isEmpty();
});
}
private static WebClient.Builder builder() {
return WebClient.builder().clientConnector(new ReactorClientHttpConnector(HttpClient.create()));
}
private static RetryBackoffSpec retrySpec() {
return Retry.fixedDelay(15, Duration.ofSeconds(1)).filter(Objects::nonNull);
}
}

View File

@@ -1,33 +0,0 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: spring-cloud-kubernetes-fabric8-client-catalog-watcher
spec:
selector:
matchLabels:
app: spring-cloud-kubernetes-fabric8-client-catalog-watcher
template:
metadata:
labels:
app: spring-cloud-kubernetes-fabric8-client-catalog-watcher
spec:
serviceAccountName: spring-cloud-kubernetes-serviceaccount
containers:
- name: spring-cloud-kubernetes-fabric8-client-catalog-watcher
image: docker.io/springcloud/spring-cloud-kubernetes-fabric8-client-catalog-watcher
imagePullPolicy: IfNotPresent
readinessProbe:
httpGet:
port: 8080
path: /actuator/health/readiness
livenessProbe:
httpGet:
port: 8080
path: /actuator/health/liveness
ports:
- containerPort: 8080
env:
- name: LOGGING_LEVEL_ORG_SPRINGFRAMEWORK_CLOUD_KUBERNETES_FABRIC8_DISCOVERY
value: "DEBUG"
- name: SPRING_CLOUD_KUBERNETES_DISCOVERY_USE_ENDPOINT_SLICES
value: "FALSE"

View File

@@ -1,16 +0,0 @@
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: spring-cloud-kubernetes-fabric8-client-catalog-watcher-ingress
namespace: default
spec:
rules:
- http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: spring-cloud-kubernetes-fabric8-client-catalog-watcher-service
port:
number: 8080

View File

@@ -1,14 +0,0 @@
apiVersion: v1
kind: Service
metadata:
labels:
app: spring-cloud-kubernetes-fabric8-client-catalog-watcher-service
name: spring-cloud-kubernetes-fabric8-client-catalog-watcher-service
spec:
ports:
- name: http
port: 8080
targetPort: 8080
selector:
app: spring-cloud-kubernetes-fabric8-client-catalog-watcher
type: ClusterIP

View File

@@ -79,7 +79,9 @@ public final class Images {
}
public static void loadBusybox(K3sContainer container) {
Commons.load(container, BUSYBOX_TAR, BUSYBOX, busyboxVersion());
if (!imageAlreadyInK3s(container, BUSYBOX_TAR)) {
Commons.load(container, BUSYBOX_TAR, BUSYBOX, busyboxVersion());
}
}
public static void loadWiremock(K3sContainer container) {
@@ -106,6 +108,25 @@ public final class Images {
Commons.load(container, RABBITMQ_TAR, RABBITMQ, rabbitMqVersion());
}
private static boolean imageAlreadyInK3s(K3sContainer container, String tarName) {
try {
boolean present = container.execInContainer("sh", "-c", "ctr images list | grep " + tarName)
.getStdout()
.contains(tarName);
if (present) {
System.out.println("image : " + tarName + " already in k3s, skipping");
return true;
}
else {
System.out.println("image : " + tarName + " not in k3s");
return false;
}
}
catch (Exception e) {
throw new RuntimeException(e);
}
}
// find the image version from current-images.txt
private static String imageVersion(String imageNameForDownload) {
BufferedReader reader = new BufferedReader(

View File

@@ -406,10 +406,12 @@ public final class Util {
Map<String, String> matchLabels = deployment.getSpec().getSelector().getMatchLabels();
long start = System.currentTimeMillis();
await().pollInterval(Duration.ofSeconds(1)).atMost(30, TimeUnit.SECONDS).until(() -> {
Deployment inner = client.apps().deployments().inNamespace(namespace).withName(deploymentName).get();
return inner == null;
});
System.out.println("Ended in " + (System.currentTimeMillis() - start) + "ms");
await().pollInterval(Duration.ofSeconds(1)).atMost(60, TimeUnit.SECONDS).until(() -> {
List<Pod> podList = client.pods().inNamespace(namespace).withLabels(matchLabels).list().getItems();