refactor fabric8 catalog watch integration test using PATCH (#1450)
This commit is contained in:
@@ -19,22 +19,17 @@ package org.springframework.cloud.kubernetes.fabric8.catalog.watch;
|
||||
import java.io.InputStream;
|
||||
import java.time.Duration;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
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.KubernetesClient;
|
||||
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.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.testcontainers.k3s.K3sContainer;
|
||||
import reactor.netty.http.client.HttpClient;
|
||||
import reactor.util.retry.Retry;
|
||||
import reactor.util.retry.RetryBackoffSpec;
|
||||
|
||||
import org.springframework.cloud.kubernetes.commons.discovery.EndpointNameAndNamespace;
|
||||
import org.springframework.cloud.kubernetes.integration.tests.commons.Commons;
|
||||
@@ -43,46 +38,61 @@ import org.springframework.cloud.kubernetes.integration.tests.commons.fabric8_cl
|
||||
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.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 APP_NAME = "spring-cloud-kubernetes-fabric8-client-catalog-watcher";
|
||||
|
||||
private static final String NAMESPACE = "default";
|
||||
|
||||
private static final K3sContainer K3S = Commons.container();
|
||||
public static final String NAMESPACE_A = "namespacea";
|
||||
|
||||
private static KubernetesClient client;
|
||||
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();
|
||||
util = new Util(K3S);
|
||||
client = util.client();
|
||||
Commons.validateImage(IMAGE_NAME, K3S);
|
||||
Commons.loadSpringCloudKubernetesImage(IMAGE_NAME, K3S);
|
||||
|
||||
Commons.validateImage(APP_NAME, K3S);
|
||||
Commons.loadSpringCloudKubernetesImage(APP_NAME, 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() {
|
||||
Commons.systemPrune();
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
void beforeEach() {
|
||||
util.busybox(NAMESPACE, Phase.CREATE);
|
||||
util.deleteNamespace(NAMESPACE_A);
|
||||
util.deleteNamespace(NAMESPACE_B);
|
||||
|
||||
app(Phase.DELETE);
|
||||
Commons.systemPrune();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -95,18 +105,35 @@ class Fabric8CatalogWatchIT {
|
||||
*/
|
||||
@Test
|
||||
void testCatalogWatchWithEndpoints() throws Exception {
|
||||
app(false, Phase.CREATE);
|
||||
assertLogStatement("stateGenerator is of type: Fabric8EndpointsCatalogWatch");
|
||||
assertLogStatement();
|
||||
test();
|
||||
app(false, Phase.DELETE);
|
||||
|
||||
testCatalogWatchWithEndpointSlices();
|
||||
testCatalogWatchWithNamespaceFilterAndEndpoints();
|
||||
testCatalogWatchWithNamespaceFilterAndEndpointSlices();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCatalogWatchWithEndpointSlices() throws Exception {
|
||||
app(true, Phase.CREATE);
|
||||
assertLogStatement("stateGenerator is of type: Fabric8EndpointSliceV1CatalogWatch");
|
||||
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();
|
||||
app(true, Phase.DELETE);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -114,13 +141,13 @@ class Fabric8CatalogWatchIT {
|
||||
* EndpointSlices. Here we make sure that in the test we actually use the correct
|
||||
* type.
|
||||
*/
|
||||
private void assertLogStatement(String log) throws Exception {
|
||||
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(log));
|
||||
Assertions.assertTrue(allLogs.contains("stateGenerator is of type: Fabric8EndpointsCatalogWatch"));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -143,6 +170,9 @@ class Fabric8CatalogWatchIT {
|
||||
// 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;
|
||||
@@ -190,21 +220,18 @@ class Fabric8CatalogWatchIT {
|
||||
return false;
|
||||
});
|
||||
|
||||
Assertions.assertTrue(afterDelete[0].endpointName().contains(APP_NAME));
|
||||
Assertions.assertTrue(afterDelete[0].endpointName().contains(IMAGE_NAME));
|
||||
Assertions.assertEquals("default", afterDelete[0].namespace());
|
||||
|
||||
}
|
||||
|
||||
private static void app(boolean useEndpointSlices, Phase phase) {
|
||||
private static void app(Phase phase) {
|
||||
|
||||
InputStream endpointsDeploymentStream = util.inputStream("app/watcher-endpoints-deployment.yaml");
|
||||
InputStream endpointSlicesDeploymentStream = util.inputStream("app/watcher-endpoint-slices-deployment.yaml");
|
||||
InputStream serviceStream = util.inputStream("app/watcher-service.yaml");
|
||||
InputStream ingressStream = util.inputStream("app/watcher-ingress.yaml");
|
||||
|
||||
Deployment deployment = useEndpointSlices
|
||||
? Serialization.unmarshal(endpointSlicesDeploymentStream, Deployment.class)
|
||||
: Serialization.unmarshal(endpointsDeploymentStream, Deployment.class);
|
||||
Deployment deployment = Serialization.unmarshal(endpointsDeploymentStream, Deployment.class);
|
||||
Service service = Serialization.unmarshal(serviceStream, Service.class);
|
||||
Ingress ingress = Serialization.unmarshal(ingressStream, Ingress.class);
|
||||
|
||||
@@ -217,12 +244,4 @@ class Fabric8CatalogWatchIT {
|
||||
|
||||
}
|
||||
|
||||
private WebClient.Builder builder() {
|
||||
return WebClient.builder().clientConnector(new ReactorClientHttpConnector(HttpClient.create()));
|
||||
}
|
||||
|
||||
private RetryBackoffSpec retrySpec() {
|
||||
return Retry.fixedDelay(15, Duration.ofSeconds(1)).filter(Objects::nonNull);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,156 @@
|
||||
/*
|
||||
* 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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
/*
|
||||
* 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());
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,258 +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.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
|
||||
import io.fabric8.kubernetes.api.model.EnvVar;
|
||||
import io.fabric8.kubernetes.api.model.EnvVarBuilder;
|
||||
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.KubernetesClient;
|
||||
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.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.testcontainers.k3s.K3sContainer;
|
||||
import reactor.netty.http.client.HttpClient;
|
||||
import reactor.util.retry.Retry;
|
||||
import reactor.util.retry.RetryBackoffSpec;
|
||||
|
||||
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.http.client.reactive.ReactorClientHttpConnector;
|
||||
import org.springframework.web.reactive.function.client.WebClient;
|
||||
|
||||
import static org.awaitility.Awaitility.await;
|
||||
|
||||
/**
|
||||
* @author wind57
|
||||
*/
|
||||
class Fabric8CatalogWatchWithNamespacesIT {
|
||||
|
||||
private static final String APP_NAME = "spring-cloud-kubernetes-fabric8-client-catalog-watcher";
|
||||
|
||||
private static final String NAMESPACE_A = "namespacea";
|
||||
|
||||
private static final String NAMESPACE_B = "namespaceb";
|
||||
|
||||
private static final String NAMESPACE_DEFAULT = "default";
|
||||
|
||||
private static final K3sContainer K3S = Commons.container();
|
||||
|
||||
private static KubernetesClient client;
|
||||
|
||||
private static Util util;
|
||||
|
||||
@BeforeAll
|
||||
static void beforeAll() throws Exception {
|
||||
K3S.start();
|
||||
util = new Util(K3S);
|
||||
client = util.client();
|
||||
|
||||
Commons.validateImage(APP_NAME, K3S);
|
||||
Commons.loadSpringCloudKubernetesImage(APP_NAME, K3S);
|
||||
|
||||
util.createNamespace(NAMESPACE_A);
|
||||
util.createNamespace(NAMESPACE_B);
|
||||
|
||||
util.setUpClusterWide(NAMESPACE_DEFAULT, Set.of(NAMESPACE_DEFAULT, NAMESPACE_A, NAMESPACE_B));
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
void beforeEach() {
|
||||
util.busybox(NAMESPACE_A, Phase.CREATE);
|
||||
util.busybox(NAMESPACE_B, Phase.CREATE);
|
||||
}
|
||||
|
||||
@AfterAll
|
||||
static void afterAll() {
|
||||
util.deleteNamespace(NAMESPACE_A);
|
||||
util.deleteNamespace(NAMESPACE_B);
|
||||
Commons.systemPrune();
|
||||
}
|
||||
|
||||
/**
|
||||
* <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>
|
||||
*/
|
||||
@Test
|
||||
void testCatalogWatchWithEndpoints() throws Exception {
|
||||
app(false, Phase.CREATE);
|
||||
assertLogStatement("stateGenerator is of type: Fabric8EndpointsCatalogWatch");
|
||||
test();
|
||||
app(false, Phase.DELETE);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCatalogWatchWithEndpointSlices() throws Exception {
|
||||
app(true, Phase.CREATE);
|
||||
assertLogStatement("stateGenerator is of type: Fabric8EndpointSliceV1CatalogWatch");
|
||||
test();
|
||||
app(true, Phase.DELETE);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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(String log) 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(log));
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
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());
|
||||
|
||||
}
|
||||
|
||||
private static void app(boolean useEndpointSlices, Phase phase) {
|
||||
|
||||
InputStream endpointsDeploymentStream = util.inputStream("app/watcher-endpoints-deployment.yaml");
|
||||
InputStream endpointSlicesDeploymentStream = util.inputStream("app/watcher-endpoint-slices-deployment.yaml");
|
||||
InputStream serviceStream = util.inputStream("app/watcher-service.yaml");
|
||||
InputStream ingressStream = util.inputStream("app/watcher-ingress.yaml");
|
||||
|
||||
Deployment deployment = useEndpointSlices
|
||||
? Serialization.unmarshal(endpointSlicesDeploymentStream, Deployment.class)
|
||||
: Serialization.unmarshal(endpointsDeploymentStream, Deployment.class);
|
||||
|
||||
List<EnvVar> envVars = new ArrayList<>(
|
||||
deployment.getSpec().getTemplate().getSpec().getContainers().get(0).getEnv());
|
||||
EnvVar namespaceAEnvVar = new EnvVarBuilder().withName("SPRING_CLOUD_KUBERNETES_DISCOVERY_NAMESPACES_0")
|
||||
.withValue(NAMESPACE_A).build();
|
||||
EnvVar namespaceDefaultEnvVar = new EnvVarBuilder().withName("SPRING_CLOUD_KUBERNETES_DISCOVERY_NAMESPACES_1")
|
||||
.withValue(NAMESPACE_DEFAULT).build();
|
||||
envVars.add(namespaceAEnvVar);
|
||||
envVars.add(namespaceDefaultEnvVar);
|
||||
|
||||
deployment.getSpec().getTemplate().getSpec().getContainers().get(0).setEnv(envVars);
|
||||
|
||||
Service service = Serialization.unmarshal(serviceStream, Service.class);
|
||||
Ingress ingress = Serialization.unmarshal(ingressStream, Ingress.class);
|
||||
|
||||
if (phase.equals(Phase.CREATE)) {
|
||||
util.createAndWait(Fabric8CatalogWatchWithNamespacesIT.NAMESPACE_DEFAULT, null, deployment, service,
|
||||
ingress, true);
|
||||
}
|
||||
else {
|
||||
util.deleteAndWait(Fabric8CatalogWatchWithNamespacesIT.NAMESPACE_DEFAULT, deployment, service, ingress);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private WebClient.Builder builder() {
|
||||
return WebClient.builder().clientConnector(new ReactorClientHttpConnector(HttpClient.create()));
|
||||
}
|
||||
|
||||
private RetryBackoffSpec retrySpec() {
|
||||
return Retry.fixedDelay(15, Duration.ofSeconds(1)).filter(Objects::nonNull);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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: true
|
||||
@@ -28,6 +28,6 @@ spec:
|
||||
- containerPort: 8080
|
||||
env:
|
||||
- name: LOGGING_LEVEL_ORG_SPRINGFRAMEWORK_CLOUD_KUBERNETES_FABRIC8_DISCOVERY
|
||||
value: DEBUG
|
||||
value: "DEBUG"
|
||||
- name: SPRING_CLOUD_KUBERNETES_DISCOVERY_USE_ENDPOINT_SLICES
|
||||
value: false
|
||||
value: "FALSE"
|
||||
|
||||
@@ -158,7 +158,15 @@ public final class Commons {
|
||||
Files.copy(imageStream, imagePath);
|
||||
// import image with ctr. this works because TEMP_FOLDER is mounted in the
|
||||
// container
|
||||
container.execInContainer("ctr", "i", "import", TEMP_FOLDER + "/" + tarName + ".tar");
|
||||
await().atMost(Duration.ofMinutes(2)).pollInterval(Duration.ofSeconds(1)).until(() -> {
|
||||
Container.ExecResult result = container.execInContainer("ctr", "i", "import",
|
||||
TEMP_FOLDER + "/" + tarName + ".tar");
|
||||
boolean noErrors = result.getStderr() == null || result.getStderr().isEmpty();
|
||||
if (!noErrors) {
|
||||
LOG.info("error is : " + result.getStderr());
|
||||
}
|
||||
return noErrors;
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -414,7 +414,8 @@ public final class Util {
|
||||
fail("No deployment with name " + deploymentName);
|
||||
}
|
||||
|
||||
Deployment deployment = deployments.getItems().get(0);
|
||||
Deployment deployment = deployments.getItems().stream()
|
||||
.filter(x -> x.getMetadata().getName().equals(deploymentName)).findFirst().orElseThrow();
|
||||
// if no replicas are defined, it means only 1 is needed
|
||||
int replicas = Optional.ofNullable(deployment.getSpec().getReplicas()).orElse(1);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user