add pod metadata and annotations as part of the service discovery response (#1254)
This commit is contained in:
@@ -116,6 +116,10 @@ By default all of the ports and their names will be added to the metadata of the
|
||||
|
||||
As said before, if you want to get the list of `ServiceInstance` to also include the `ExternalName` type services, you need to enable that support via: `spring.cloud.kubernetes.discovery.include-external-name-services=true`. As such, when calling `DiscoveryClient::getInstances` those will be returned also. You can distinguish between `ExternalName` and any other types by inspecting `ServiceInstance::getMetadata` and lookup for a field called `type`. This will be the type of the service returned : `ExternalName`/`ClusterIP`, etc.
|
||||
|
||||
`ServiceInstance` can include the labels and annotations of specific pods from the underlying service instance. To obtain such information, you need to also enable:
|
||||
|
||||
`spring.cloud.kubernetes.discovery.metadata.add-pod-labels=true` and/or `spring.cloud.kubernetes.discovery.metadata.add-pod-annotations=true`. At the moment, such functionality is present only in the fabric8 client implementation, but will be added to the kubernetes native client in a later release.
|
||||
|
||||
If, for any reason, you need to disable the `DiscoveryClient`, you can set the following property in `application.properties`:
|
||||
|
||||
====
|
||||
|
||||
@@ -35,8 +35,8 @@ import static org.springframework.cloud.kubernetes.commons.discovery.KubernetesD
|
||||
* @param cluster the cluster the service resides in.
|
||||
*/
|
||||
public record DefaultKubernetesServiceInstance(String instanceId, String serviceId, String host, int port,
|
||||
Map<String, String> metadata, boolean secure, String namespace,
|
||||
String cluster) implements KubernetesServiceInstance {
|
||||
Map<String, String> metadata, boolean secure, String namespace, String cluster,
|
||||
Map<String, Map<String, String>> podMetadata) implements KubernetesServiceInstance {
|
||||
|
||||
/**
|
||||
* @param instanceId the id of the instance.
|
||||
@@ -48,7 +48,12 @@ public record DefaultKubernetesServiceInstance(String instanceId, String service
|
||||
*/
|
||||
public DefaultKubernetesServiceInstance(String instanceId, String serviceId, String host, int port,
|
||||
Map<String, String> metadata, boolean secure) {
|
||||
this(instanceId, serviceId, host, port, metadata, secure, null, null);
|
||||
this(instanceId, serviceId, host, port, metadata, secure, null, null, Map.of());
|
||||
}
|
||||
|
||||
public DefaultKubernetesServiceInstance(String instanceId, String serviceId, String host, int port,
|
||||
Map<String, String> metadata, boolean secure, String namespace, String cluster) {
|
||||
this(instanceId, serviceId, host, port, metadata, secure, namespace, cluster, Map.of());
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -101,6 +106,11 @@ public record DefaultKubernetesServiceInstance(String instanceId, String service
|
||||
return this.cluster;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Map<String, String>> podMetadata() {
|
||||
return podMetadata;
|
||||
}
|
||||
|
||||
private URI createUri(String scheme, String host, int port) {
|
||||
// assume ExternalName type of service
|
||||
if (port == -1) {
|
||||
|
||||
@@ -92,15 +92,30 @@ public record KubernetesDiscoveryProperties(
|
||||
* @param annotationsPrefix prefix for the annotations
|
||||
* @param addPorts include ports as metadata
|
||||
* @param portsPrefix prefix for the ports, by default it is "port."
|
||||
* @param addPodLabels add pod labels as part of the response.
|
||||
* @param addPodAnnotations add pod annotations as part of the response.
|
||||
*/
|
||||
public record Metadata(@DefaultValue("true") boolean addLabels, String labelsPrefix,
|
||||
@DefaultValue("true") boolean addAnnotations, String annotationsPrefix,
|
||||
@DefaultValue("true") boolean addPorts, @DefaultValue("port.") String portsPrefix) {
|
||||
@DefaultValue("true") boolean addPorts, @DefaultValue("port.") String portsPrefix, boolean addPodLabels,
|
||||
boolean addPodAnnotations) {
|
||||
|
||||
@ConstructorBinding
|
||||
public Metadata {
|
||||
|
||||
}
|
||||
|
||||
public Metadata(@DefaultValue("true") boolean addLabels, String labelsPrefix,
|
||||
@DefaultValue("true") boolean addAnnotations, String annotationsPrefix,
|
||||
@DefaultValue("true") boolean addPorts, @DefaultValue("port.") String portsPrefix) {
|
||||
|
||||
this(addLabels, labelsPrefix, addAnnotations, annotationsPrefix, addPorts, portsPrefix, false, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Default instance.
|
||||
*/
|
||||
public static final Metadata DEFAULT = new Metadata(true, null, true, null, true, "port.");
|
||||
public static final Metadata DEFAULT = new Metadata(true, null, true, null, true, "port.", false, false);
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -16,6 +16,8 @@
|
||||
|
||||
package org.springframework.cloud.kubernetes.commons.discovery;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.cloud.client.ServiceInstance;
|
||||
|
||||
/**
|
||||
@@ -29,4 +31,8 @@ sealed public interface KubernetesServiceInstance extends ServiceInstance permit
|
||||
|
||||
String getCluster();
|
||||
|
||||
default Map<String, Map<String, String>> podMetadata() {
|
||||
return Map.of();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -39,18 +39,24 @@ class KubernetesDiscoveryPropertiesMetadataTests {
|
||||
assertThat(m.annotationsPrefix()).isNull();
|
||||
assertThat(m.addPorts()).isTrue();
|
||||
assertThat(m.portsPrefix()).isEqualTo("port.");
|
||||
assertThat(m.addPodLabels()).isFalse();
|
||||
assertThat(m.addPodAnnotations()).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSpringBindingFields() {
|
||||
new ApplicationContextRunner().withUserConfiguration(Config.class)
|
||||
.withPropertyValues("spring.cloud.kubernetes.discovery.metadata.labelsPrefix=labelsPrefix")
|
||||
.withPropertyValues("spring.cloud.kubernetes.discovery.metadata.labelsPrefix=labelsPrefix",
|
||||
"spring.cloud.kubernetes.discovery.metadata.add-pod-annotations=true",
|
||||
"spring.cloud.kubernetes.discovery.metadata.add-pod-labels=true")
|
||||
.run(context -> {
|
||||
KubernetesDiscoveryProperties props = context.getBean(KubernetesDiscoveryProperties.class);
|
||||
assertThat(props).isNotNull();
|
||||
assertThat(props.metadata().labelsPrefix()).isEqualTo("labelsPrefix");
|
||||
assertThat(props.metadata().addPorts()).isTrue();
|
||||
assertThat(props.metadata().portsPrefix()).isEqualTo("port.");
|
||||
assertThat(props.metadata().addPodLabels()).isTrue();
|
||||
assertThat(props.metadata().addPodAnnotations()).isTrue();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -44,6 +44,7 @@ class KubernetesServiceInstanceTests {
|
||||
assertThat(instance.getScheme()).isEqualTo("https");
|
||||
assertThat(instance.getNamespace()).isEqualTo("spring-k8s");
|
||||
assertThat(instance.getCluster()).isNull();
|
||||
assertThat(instance.podMetadata()).isEqualTo(Map.of());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -61,6 +62,27 @@ class KubernetesServiceInstanceTests {
|
||||
assertThat(instance.getScheme()).isEqualTo("https");
|
||||
assertThat(instance.getNamespace()).isEqualTo("spring-k8s");
|
||||
assertThat(instance.getCluster()).isEqualTo("cluster");
|
||||
assertThat(instance.podMetadata()).isEqualTo(Map.of());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testThirdConstructor() {
|
||||
DefaultKubernetesServiceInstance instance = new DefaultKubernetesServiceInstance("instanceId", "serviceId",
|
||||
"host", 8080, Map.of("a", "b"), true, "spring-k8s", "cluster",
|
||||
Map.of("labels", Map.of("a", "b"), "annotations", Map.of("c", "d")));
|
||||
|
||||
assertThat(instance.getInstanceId()).isEqualTo("instanceId");
|
||||
assertThat(instance.getServiceId()).isEqualTo("serviceId");
|
||||
assertThat(instance.getHost()).isEqualTo("host");
|
||||
assertThat(instance.getPort()).isEqualTo(8080);
|
||||
assertThat(instance.isSecure()).isTrue();
|
||||
assertThat(instance.getUri()).isEqualTo(URI.create("https://host:8080"));
|
||||
assertThat(instance.getMetadata()).isEqualTo(Map.of("a", "b"));
|
||||
assertThat(instance.getScheme()).isEqualTo("https");
|
||||
assertThat(instance.getNamespace()).isEqualTo("spring-k8s");
|
||||
assertThat(instance.getCluster()).isEqualTo("cluster");
|
||||
assertThat(instance.podMetadata())
|
||||
.isEqualTo(Map.of("labels", Map.of("a", "b"), "annotations", Map.of("c", "d")));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -121,7 +121,7 @@ public class KubernetesDiscoveryClient implements DiscoveryClient, EnvironmentAw
|
||||
Map<String, String> serviceMetadata = serviceMetadata(serviceId, service, properties, List.of(),
|
||||
service.getMetadata().getNamespace());
|
||||
ServiceInstance externalNameServiceInstance = serviceInstance(null, service, null, -1, serviceId,
|
||||
serviceMetadata, service.getMetadata().getNamespace());
|
||||
serviceMetadata, service.getMetadata().getNamespace(), properties, client);
|
||||
instances.add(externalNameServiceInstance);
|
||||
}
|
||||
}
|
||||
@@ -152,7 +152,7 @@ public class KubernetesDiscoveryClient implements DiscoveryClient, EnvironmentAw
|
||||
List<EndpointAddress> addresses = addresses(endpointSubset, properties);
|
||||
for (EndpointAddress endpointAddress : addresses) {
|
||||
ServiceInstance serviceInstance = serviceInstance(servicePortSecureResolver, service, endpointAddress,
|
||||
endpointPort, serviceId, serviceMetadata, namespace);
|
||||
endpointPort, serviceId, serviceMetadata, namespace, properties, client);
|
||||
instances.add(serviceInstance);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,7 +29,9 @@ import io.fabric8.kubernetes.api.model.EndpointPort;
|
||||
import io.fabric8.kubernetes.api.model.EndpointSubset;
|
||||
import io.fabric8.kubernetes.api.model.Endpoints;
|
||||
import io.fabric8.kubernetes.api.model.EndpointsList;
|
||||
import io.fabric8.kubernetes.api.model.ObjectMeta;
|
||||
import io.fabric8.kubernetes.api.model.ObjectReference;
|
||||
import io.fabric8.kubernetes.api.model.Pod;
|
||||
import io.fabric8.kubernetes.api.model.Service;
|
||||
import io.fabric8.kubernetes.api.model.ServiceList;
|
||||
import io.fabric8.kubernetes.client.KubernetesClient;
|
||||
@@ -51,6 +53,7 @@ import org.springframework.util.StringUtils;
|
||||
|
||||
import static java.util.stream.Collectors.toMap;
|
||||
import static org.springframework.cloud.kubernetes.commons.config.ConfigUtils.keysWithPrefix;
|
||||
import static org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscoveryConstants.EXTERNAL_NAME;
|
||||
import static org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscoveryConstants.HTTP;
|
||||
import static org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscoveryConstants.HTTPS;
|
||||
import static org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscoveryConstants.NAMESPACE_METADATA_KEY;
|
||||
@@ -236,7 +239,8 @@ final class KubernetesDiscoveryClientUtils {
|
||||
|
||||
static ServiceInstance serviceInstance(@Nullable ServicePortSecureResolver servicePortSecureResolver,
|
||||
Service service, @Nullable EndpointAddress endpointAddress, int endpointPort, String serviceId,
|
||||
Map<String, String> serviceMetadata, String namespace) {
|
||||
Map<String, String> serviceMetadata, String namespace, KubernetesDiscoveryProperties properties,
|
||||
KubernetesClient client) {
|
||||
// instanceId is usually the pod-uid as seen in the .metadata.uid
|
||||
String instanceId = Optional.ofNullable(endpointAddress).map(EndpointAddress::getTargetRef)
|
||||
.map(ObjectReference::getUid).orElseGet(() -> service.getMetadata().getUid());
|
||||
@@ -254,8 +258,11 @@ final class KubernetesDiscoveryClientUtils {
|
||||
String host = Optional.ofNullable(endpointAddress).map(EndpointAddress::getIp)
|
||||
.orElseGet(() -> service.getSpec().getExternalName());
|
||||
|
||||
Map<String, Map<String, String>> podMetadata = podMetadata(client, serviceMetadata, properties, endpointAddress,
|
||||
namespace);
|
||||
|
||||
return new DefaultKubernetesServiceInstance(instanceId, serviceId, host, endpointPort, serviceMetadata, secured,
|
||||
namespace, null);
|
||||
namespace, null, podMetadata);
|
||||
}
|
||||
|
||||
static List<Service> services(KubernetesDiscoveryProperties properties, KubernetesClient client,
|
||||
@@ -287,6 +294,37 @@ final class KubernetesDiscoveryClientUtils {
|
||||
return services;
|
||||
}
|
||||
|
||||
static Map<String, Map<String, String>> podMetadata(KubernetesClient client, Map<String, String> serviceMetadata,
|
||||
KubernetesDiscoveryProperties properties, EndpointAddress endpointAddress, String namespace) {
|
||||
if (!EXTERNAL_NAME.equals(serviceMetadata.get(SERVICE_TYPE))) {
|
||||
if (properties.metadata().addPodLabels() || properties.metadata().addPodAnnotations()) {
|
||||
String podName = Optional.ofNullable(endpointAddress).map(EndpointAddress::getTargetRef)
|
||||
.filter(objectReference -> "Pod".equals(objectReference.getKind()))
|
||||
.map(ObjectReference::getName).orElse(null);
|
||||
|
||||
if (podName != null) {
|
||||
ObjectMeta metadata = Optional
|
||||
.ofNullable(client.pods().inNamespace(namespace).withName(podName).get())
|
||||
.map(Pod::getMetadata).orElse(new ObjectMeta());
|
||||
Map<String, Map<String, String>> result = new HashMap<>();
|
||||
if (properties.metadata().addPodLabels() && !metadata.getLabels().isEmpty()) {
|
||||
result.put("labels", metadata.getLabels());
|
||||
}
|
||||
|
||||
if (properties.metadata().addPodAnnotations() && !metadata.getAnnotations().isEmpty()) {
|
||||
result.put("annotations", metadata.getAnnotations());
|
||||
}
|
||||
|
||||
LOG.debug(() -> "adding podMetadata : " + result + " from pod : " + podName);
|
||||
return result;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
return Map.of();
|
||||
}
|
||||
|
||||
/**
|
||||
* serviceName can be null, in which case, such a filter will not be applied.
|
||||
*/
|
||||
|
||||
@@ -20,19 +20,22 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import io.fabric8.kubernetes.api.model.EndpointAddressBuilder;
|
||||
import io.fabric8.kubernetes.api.model.EndpointPortBuilder;
|
||||
import io.fabric8.kubernetes.api.model.EndpointSubsetBuilder;
|
||||
import io.fabric8.kubernetes.api.model.Endpoints;
|
||||
import io.fabric8.kubernetes.api.model.EndpointsBuilder;
|
||||
import io.fabric8.kubernetes.api.model.ObjectMeta;
|
||||
import io.fabric8.kubernetes.api.model.ObjectMetaBuilder;
|
||||
import io.fabric8.kubernetes.api.model.ObjectReferenceBuilder;
|
||||
import io.fabric8.kubernetes.api.model.PodBuilder;
|
||||
import io.fabric8.kubernetes.api.model.Service;
|
||||
import io.fabric8.kubernetes.api.model.ServiceBuilder;
|
||||
import io.fabric8.kubernetes.api.model.ServiceSpecBuilder;
|
||||
import io.fabric8.kubernetes.client.Config;
|
||||
import io.fabric8.kubernetes.client.KubernetesClient;
|
||||
import io.fabric8.kubernetes.client.server.mock.EnableKubernetesMockClient;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
|
||||
@@ -51,17 +54,6 @@ class KubernetesDiscoveryClientTests {
|
||||
|
||||
private static KubernetesClient client;
|
||||
|
||||
@BeforeAll
|
||||
static void setUp() {
|
||||
// Configure the kubernetes master url to point to the mock server
|
||||
System.setProperty(Config.KUBERNETES_MASTER_SYSTEM_PROPERTY, client.getConfiguration().getMasterUrl());
|
||||
System.setProperty(Config.KUBERNETES_TRUST_CERT_SYSTEM_PROPERTY, "true");
|
||||
System.setProperty(Config.KUBERNETES_AUTH_TRYKUBECONFIG_SYSTEM_PROPERTY, "false");
|
||||
System.setProperty(Config.KUBERNETES_AUTH_TRYSERVICEACCOUNT_SYSTEM_PROPERTY, "false");
|
||||
System.setProperty(Config.KUBERNETES_NAMESPACE_SYSTEM_PROPERTY, "test");
|
||||
System.setProperty(Config.KUBERNETES_HTTP2_DISABLE, "true");
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void afterEach() {
|
||||
client.endpoints().inAnyNamespace().delete();
|
||||
@@ -527,6 +519,44 @@ class KubernetesDiscoveryClientTests {
|
||||
"labels-prefix-label-key", "label-value", "annotations-prefix-abc", "def", "type", "ExternalName"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testPodMetadata() {
|
||||
Service nonExternalNameService = new ServiceBuilder()
|
||||
.withSpec(new ServiceSpecBuilder().withType("ClusterIP").build()).withNewMetadata()
|
||||
.withName("blue-service").withNamespace("a").endMetadata().build();
|
||||
client.services().inNamespace("a").resource(nonExternalNameService).create();
|
||||
|
||||
client.endpoints().inNamespace("a").resource(new EndpointsBuilder()
|
||||
.withMetadata(new ObjectMetaBuilder().withName("blue-service").build())
|
||||
.withSubsets(new EndpointSubsetBuilder().withPorts(new EndpointPortBuilder().withPort(8080).build())
|
||||
.withAddresses(new EndpointAddressBuilder().withIp("127.0.0.1")
|
||||
.withTargetRef(new ObjectReferenceBuilder().withKind("Pod").withName("my-pod").build())
|
||||
.build())
|
||||
.build())
|
||||
.build()).create();
|
||||
|
||||
client.pods().inNamespace("a").resource(new PodBuilder().withMetadata(new ObjectMetaBuilder().withName("my-pod")
|
||||
.withLabels(Map.of("a", "b")).withAnnotations(Map.of("c", "d")).build()).build()).create();
|
||||
|
||||
KubernetesDiscoveryProperties.Metadata metadata = new KubernetesDiscoveryProperties.Metadata(true,
|
||||
"labels-prefix-", true, "annotations-prefix-", true, "ports-prefix", true, true);
|
||||
KubernetesDiscoveryProperties properties = new KubernetesDiscoveryProperties(true, true, Set.of("a", "b"), true,
|
||||
60L, false, "", Set.of(), Map.of(), "", metadata, 0, false, true);
|
||||
|
||||
KubernetesDiscoveryClient discoveryClient = new KubernetesDiscoveryClient(client, properties, null, null, null);
|
||||
List<ServiceInstance> result = discoveryClient.getInstances("blue-service");
|
||||
Assertions.assertEquals(result.size(), 1);
|
||||
DefaultKubernetesServiceInstance serviceInstance = (DefaultKubernetesServiceInstance) result.get(0);
|
||||
Assertions.assertEquals(serviceInstance.getServiceId(), "blue-service");
|
||||
Assertions.assertEquals(serviceInstance.getHost(), "127.0.0.1");
|
||||
Assertions.assertEquals(serviceInstance.getPort(), 8080);
|
||||
Assertions.assertFalse(serviceInstance.isSecure());
|
||||
Assertions.assertEquals(serviceInstance.getUri().toASCIIString(), "http://127.0.0.1:8080");
|
||||
Assertions.assertEquals(serviceInstance.getMetadata(), Map.of("k8s_namespace", "a", "type", "ClusterIP"));
|
||||
Assertions.assertEquals(serviceInstance.podMetadata().get("labels"), Map.of("a", "b"));
|
||||
Assertions.assertEquals(serviceInstance.podMetadata().get("annotations"), Map.of("c", "d"));
|
||||
}
|
||||
|
||||
private void createEndpoints(String namespace, String name, Map<String, String> labels) {
|
||||
client.endpoints().inNamespace(namespace)
|
||||
.resource(new EndpointsBuilder()
|
||||
|
||||
@@ -0,0 +1,213 @@
|
||||
/*
|
||||
* 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.discovery;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import io.fabric8.kubernetes.api.model.EndpointAddress;
|
||||
import io.fabric8.kubernetes.api.model.EndpointAddressBuilder;
|
||||
import io.fabric8.kubernetes.api.model.ObjectReferenceBuilder;
|
||||
import io.fabric8.kubernetes.api.model.PodBuilder;
|
||||
import io.fabric8.kubernetes.client.KubernetesClient;
|
||||
import io.fabric8.kubernetes.client.server.mock.EnableKubernetesMockClient;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
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.discovery.KubernetesDiscoveryProperties;
|
||||
|
||||
import static org.springframework.cloud.kubernetes.fabric8.discovery.KubernetesDiscoveryClientUtils.podMetadata;
|
||||
|
||||
/**
|
||||
* @author wind57
|
||||
*/
|
||||
@ExtendWith(OutputCaptureExtension.class)
|
||||
@EnableKubernetesMockClient(https = false, crud = true)
|
||||
class KubernetesDiscoveryClientUtilsPodMetadataTests {
|
||||
|
||||
private KubernetesClient client;
|
||||
|
||||
@AfterEach
|
||||
void afterEach() {
|
||||
client.pods().inAnyNamespace().delete();
|
||||
}
|
||||
|
||||
/**
|
||||
* service is of type ExternalName, thus no podMetadata is added.
|
||||
*/
|
||||
@Test
|
||||
void testExternalName() {
|
||||
Map<String, String> serviceMetadata = Map.of("type", "ExternalName");
|
||||
boolean addPodLabels = true;
|
||||
boolean addPodAnnotations = true;
|
||||
KubernetesDiscoveryProperties.Metadata metadata = new KubernetesDiscoveryProperties.Metadata(false, "", false,
|
||||
"", false, "", addPodLabels, addPodAnnotations);
|
||||
KubernetesDiscoveryProperties properties = new KubernetesDiscoveryProperties(true, true, Set.of(), true, 60L,
|
||||
false, "", Set.of(), Map.of(), "", metadata, 0, false);
|
||||
EndpointAddress endpointAddress = new EndpointAddressBuilder().build();
|
||||
String namespace = "default";
|
||||
|
||||
Map<String, Map<String, String>> result = podMetadata(client, serviceMetadata, properties, endpointAddress,
|
||||
namespace);
|
||||
Assertions.assertEquals(result, Map.of());
|
||||
}
|
||||
|
||||
/**
|
||||
* service is not of type ExternalName, but neither podLabels nor podAnnotations are
|
||||
* requested. As such, podMetadata is empty.
|
||||
*/
|
||||
@Test
|
||||
void testNotExternalName() {
|
||||
Map<String, String> serviceMetadata = Map.of("type", "ClusterIP");
|
||||
boolean addPodLabels = false;
|
||||
boolean addPodAnnotations = false;
|
||||
KubernetesDiscoveryProperties.Metadata metadata = new KubernetesDiscoveryProperties.Metadata(false, "", false,
|
||||
"", false, "", addPodLabels, addPodAnnotations);
|
||||
KubernetesDiscoveryProperties properties = new KubernetesDiscoveryProperties(true, true, Set.of(), true, 60L,
|
||||
false, "", Set.of(), Map.of(), "", metadata, 0, false);
|
||||
EndpointAddress endpointAddress = new EndpointAddressBuilder().build();
|
||||
String namespace = "default";
|
||||
|
||||
Map<String, Map<String, String>> result = podMetadata(client, serviceMetadata, properties, endpointAddress,
|
||||
namespace);
|
||||
Assertions.assertEquals(result, Map.of());
|
||||
}
|
||||
|
||||
/**
|
||||
* service is not of type ExternalName, and podLabels are requested, but a pod does
|
||||
* not exist. As such pod metadata is empty.
|
||||
*/
|
||||
@Test
|
||||
void testNotExternalPodNotPresent() {
|
||||
Map<String, String> serviceMetadata = Map.of("type", "ClusterIP");
|
||||
boolean addPodLabels = true;
|
||||
boolean addPodAnnotations = false;
|
||||
String podName = "my-pod";
|
||||
KubernetesDiscoveryProperties.Metadata metadata = new KubernetesDiscoveryProperties.Metadata(false, "", false,
|
||||
"", false, "", addPodLabels, addPodAnnotations);
|
||||
KubernetesDiscoveryProperties properties = new KubernetesDiscoveryProperties(true, true, Set.of(), true, 60L,
|
||||
false, "", Set.of(), Map.of(), "", metadata, 0, false);
|
||||
EndpointAddress endpointAddress = new EndpointAddressBuilder()
|
||||
.withTargetRef(new ObjectReferenceBuilder().withKind("Pod").withName(podName).build()).build();
|
||||
String namespace = "default";
|
||||
|
||||
Map<String, Map<String, String>> result = podMetadata(client, serviceMetadata, properties, endpointAddress,
|
||||
namespace);
|
||||
Assertions.assertEquals(result, Map.of());
|
||||
}
|
||||
|
||||
/**
|
||||
* service is not of type ExternalName, and podLabels are requested. As such,
|
||||
* podMetadata contains only pod labels.
|
||||
*/
|
||||
@Test
|
||||
void testNotExternalNamePodLabelsRequested(CapturedOutput output) {
|
||||
Map<String, String> serviceMetadata = Map.of("type", "ClusterIP");
|
||||
boolean addPodLabels = true;
|
||||
boolean addPodAnnotations = false;
|
||||
String podName = "my-pod";
|
||||
KubernetesDiscoveryProperties.Metadata metadata = new KubernetesDiscoveryProperties.Metadata(false, "", false,
|
||||
"", false, "", addPodLabels, addPodAnnotations);
|
||||
KubernetesDiscoveryProperties properties = new KubernetesDiscoveryProperties(true, true, Set.of(), true, 60L,
|
||||
false, "", Set.of(), Map.of(), "", metadata, 0, false);
|
||||
EndpointAddress endpointAddress = new EndpointAddressBuilder()
|
||||
.withTargetRef(new ObjectReferenceBuilder().withKind("Pod").withName(podName).build()).build();
|
||||
String namespace = "default";
|
||||
|
||||
client.pods().inNamespace(namespace)
|
||||
.resource(new PodBuilder().withNewMetadata().withName(podName)
|
||||
.withLabels(Map.of("label-key", "label-value"))
|
||||
.withAnnotations(Map.of("annotation-key", "annotation-value")).and().build())
|
||||
.create();
|
||||
|
||||
Map<String, Map<String, String>> result = podMetadata(client, serviceMetadata, properties, endpointAddress,
|
||||
namespace);
|
||||
Assertions.assertEquals(result.get("labels"), Map.of("label-key", "label-value"));
|
||||
Assertions.assertNull(result.get("annotations"));
|
||||
Assertions.assertTrue(
|
||||
output.getOut().contains("adding podMetadata : {labels={label-key=label-value}} from pod : my-pod"));
|
||||
}
|
||||
|
||||
/**
|
||||
* service is not of type ExternalName, and podAnnotations are requested. As such,
|
||||
* podMetadata contains only pod annotations.
|
||||
*/
|
||||
@Test
|
||||
void testNotExternalNamePodAnnotationsRequested(CapturedOutput output) {
|
||||
Map<String, String> serviceMetadata = Map.of("type", "ClusterIP");
|
||||
boolean addPodLabels = false;
|
||||
boolean addPodAnnotations = true;
|
||||
String podName = "my-pod";
|
||||
KubernetesDiscoveryProperties.Metadata metadata = new KubernetesDiscoveryProperties.Metadata(false, "", false,
|
||||
"", false, "", addPodLabels, addPodAnnotations);
|
||||
KubernetesDiscoveryProperties properties = new KubernetesDiscoveryProperties(true, true, Set.of(), true, 60L,
|
||||
false, "", Set.of(), Map.of(), "", metadata, 0, false);
|
||||
EndpointAddress endpointAddress = new EndpointAddressBuilder()
|
||||
.withTargetRef(new ObjectReferenceBuilder().withKind("Pod").withName(podName).build()).build();
|
||||
String namespace = "default";
|
||||
|
||||
client.pods().inNamespace(namespace)
|
||||
.resource(new PodBuilder().withNewMetadata().withName(podName)
|
||||
.withLabels(Map.of("label-key", "label-value"))
|
||||
.withAnnotations(Map.of("annotation-key", "annotation-value")).and().build())
|
||||
.create();
|
||||
|
||||
Map<String, Map<String, String>> result = podMetadata(client, serviceMetadata, properties, endpointAddress,
|
||||
namespace);
|
||||
Assertions.assertNull(result.get("labels"));
|
||||
Assertions.assertEquals(result.get("annotations"), Map.of("annotation-key", "annotation-value"));
|
||||
Assertions.assertTrue(output.getOut()
|
||||
.contains("adding podMetadata : {annotations={annotation-key=annotation-value}} from pod : my-pod"));
|
||||
}
|
||||
|
||||
/**
|
||||
* service is not of type ExternalName, both podLabels and podAnnotations are
|
||||
* requested. As such, podMetadata contains both.
|
||||
*/
|
||||
@Test
|
||||
void testNotExternalNamePodLabelsAndAnnotationsRequested(CapturedOutput output) {
|
||||
Map<String, String> serviceMetadata = Map.of("type", "ClusterIP");
|
||||
boolean addPodLabels = true;
|
||||
boolean addPodAnnotations = true;
|
||||
String podName = "my-pod";
|
||||
KubernetesDiscoveryProperties.Metadata metadata = new KubernetesDiscoveryProperties.Metadata(false, "", false,
|
||||
"", false, "", addPodLabels, addPodAnnotations);
|
||||
KubernetesDiscoveryProperties properties = new KubernetesDiscoveryProperties(true, true, Set.of(), true, 60L,
|
||||
false, "", Set.of(), Map.of(), "", metadata, 0, false);
|
||||
EndpointAddress endpointAddress = new EndpointAddressBuilder()
|
||||
.withTargetRef(new ObjectReferenceBuilder().withKind("Pod").withName(podName).build()).build();
|
||||
String namespace = "default";
|
||||
|
||||
client.pods().inNamespace(namespace)
|
||||
.resource(new PodBuilder().withNewMetadata().withName(podName)
|
||||
.withLabels(Map.of("label-key", "label-value"))
|
||||
.withAnnotations(Map.of("annotation-key", "annotation-value")).and().build())
|
||||
.create();
|
||||
|
||||
Map<String, Map<String, String>> result = podMetadata(client, serviceMetadata, properties, endpointAddress,
|
||||
namespace);
|
||||
Assertions.assertEquals(result.get("labels"), Map.of("label-key", "label-value"));
|
||||
Assertions.assertEquals(result.get("annotations"), Map.of("annotation-key", "annotation-value"));
|
||||
Assertions.assertTrue(output.getOut().contains(
|
||||
"adding podMetadata : {annotations={annotation-key=annotation-value}, labels={label-key=label-value}} from pod : my-pod"));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -695,14 +695,14 @@ class KubernetesDiscoveryClientUtilsTests {
|
||||
@Test
|
||||
void testServiceInstance() {
|
||||
KubernetesDiscoveryProperties properties = new KubernetesDiscoveryProperties(true, true, Set.of(), true, 60L,
|
||||
false, "", Set.of(), Map.of(), "", null, 0, false, false);
|
||||
false, "", Set.of(), Map.of(), "", KubernetesDiscoveryProperties.Metadata.DEFAULT, 0, false, false);
|
||||
ServicePortSecureResolver resolver = new ServicePortSecureResolver(properties);
|
||||
Service service = new ServiceBuilder().withMetadata(new ObjectMeta()).build();
|
||||
EndpointAddress address = new EndpointAddressBuilder().withNewTargetRef().withUid("123").endTargetRef()
|
||||
.withIp("127.0.0.1").build();
|
||||
|
||||
ServiceInstance serviceInstance = KubernetesDiscoveryClientUtils.serviceInstance(resolver, service, address,
|
||||
8080, "my-service", Map.of("a", "b"), "k8s");
|
||||
8080, "my-service", Map.of("a", "b"), "k8s", properties, null);
|
||||
Assertions.assertTrue(serviceInstance instanceof DefaultKubernetesServiceInstance);
|
||||
DefaultKubernetesServiceInstance defaultInstance = (DefaultKubernetesServiceInstance) serviceInstance;
|
||||
Assertions.assertEquals(defaultInstance.getInstanceId(), "123");
|
||||
@@ -724,7 +724,7 @@ class KubernetesDiscoveryClientUtilsTests {
|
||||
.withMetadata(new ObjectMetaBuilder().withUid("123").build()).build();
|
||||
|
||||
ServiceInstance serviceInstance = KubernetesDiscoveryClientUtils.serviceInstance(null, service, null, -1,
|
||||
"my-service", Map.of("a", "b"), "k8s");
|
||||
"my-service", Map.of("a", "b"), "k8s", KubernetesDiscoveryProperties.DEFAULT, null);
|
||||
Assertions.assertTrue(serviceInstance instanceof DefaultKubernetesServiceInstance);
|
||||
DefaultKubernetesServiceInstance defaultInstance = (DefaultKubernetesServiceInstance) serviceInstance;
|
||||
Assertions.assertEquals(defaultInstance.getInstanceId(), "123");
|
||||
|
||||
@@ -0,0 +1,166 @@
|
||||
/*
|
||||
* 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.configmap;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.time.Duration;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
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 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 reactor.netty.http.client.HttpClient;
|
||||
import reactor.util.retry.Retry;
|
||||
import reactor.util.retry.RetryBackoffSpec;
|
||||
|
||||
import org.springframework.cloud.kubernetes.commons.discovery.DefaultKubernetesServiceInstance;
|
||||
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.http.HttpMethod;
|
||||
import org.springframework.http.client.reactive.ReactorClientHttpConnector;
|
||||
import org.springframework.web.reactive.function.client.WebClient;
|
||||
|
||||
/**
|
||||
* @author wind57
|
||||
*/
|
||||
class Fabric8DiscoveryPodMetadataIT {
|
||||
|
||||
private static final String NAMESPACE = "default";
|
||||
|
||||
private static final String IMAGE_NAME = "spring-cloud-kubernetes-fabric8-client-discovery";
|
||||
|
||||
private static KubernetesClient client;
|
||||
|
||||
private static Util util;
|
||||
|
||||
private static final K3sContainer K3S = Commons.container();
|
||||
|
||||
@BeforeAll
|
||||
static void beforeAll() throws Exception {
|
||||
K3S.start();
|
||||
Commons.validateImage(IMAGE_NAME, K3S);
|
||||
Commons.loadSpringCloudKubernetesImage(IMAGE_NAME, K3S);
|
||||
|
||||
util = new Util(K3S);
|
||||
client = util.client();
|
||||
|
||||
util.setUp(NAMESPACE);
|
||||
|
||||
manifests(Phase.CREATE);
|
||||
util.busybox(NAMESPACE, Phase.CREATE);
|
||||
}
|
||||
|
||||
@AfterAll
|
||||
static void after() throws Exception {
|
||||
util.busybox(NAMESPACE, Phase.DELETE);
|
||||
manifests(Phase.DELETE);
|
||||
Commons.cleanUp(IMAGE_NAME, K3S);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testPodMetadata() throws Exception {
|
||||
|
||||
// find both pods
|
||||
String[] both = K3S.execInContainer("sh", "-c", "kubectl get pods -l app=busybox -o=name --no-headers")
|
||||
.getStdout().split("\n");
|
||||
// add a label to first pod
|
||||
K3S.execInContainer("sh", "-c",
|
||||
"kubectl label pods " + both[0].split("/")[1] + " custom-label=custom-label-value");
|
||||
// add annotation to the second pod
|
||||
K3S.execInContainer("sh", "-c",
|
||||
"kubectl annotate pods " + both[1].split("/")[1] + " custom-annotation=custom-annotation-value");
|
||||
|
||||
WebClient client = builder().baseUrl("http://localhost/service-instances/busybox-service").build();
|
||||
List<DefaultKubernetesServiceInstance> serviceInstances = client.method(HttpMethod.GET).retrieve()
|
||||
.bodyToMono(new ParameterizedTypeReference<List<DefaultKubernetesServiceInstance>>() {
|
||||
|
||||
}).retryWhen(retrySpec()).block();
|
||||
|
||||
DefaultKubernetesServiceInstance withCustomLabel = serviceInstances.stream()
|
||||
.filter(x -> x.podMetadata().getOrDefault("annotations", Map.of()).isEmpty()).toList().get(0);
|
||||
Assertions.assertEquals(withCustomLabel.getServiceId(), "busybox-service");
|
||||
Assertions.assertNotNull(withCustomLabel.getInstanceId());
|
||||
Assertions.assertNotNull(withCustomLabel.getHost());
|
||||
Assertions.assertEquals(withCustomLabel.getMetadata(),
|
||||
Map.of("k8s_namespace", "default", "type", "ClusterIP", "port.busybox-port", "80"));
|
||||
Assertions.assertTrue(withCustomLabel.podMetadata().get("labels").entrySet().stream()
|
||||
.anyMatch(x -> x.getKey().equals("custom-label") && x.getValue().equals("custom-label-value")));
|
||||
|
||||
DefaultKubernetesServiceInstance withCustomAnnotation = serviceInstances.stream()
|
||||
.filter(x -> !x.podMetadata().getOrDefault("annotations", Map.of()).isEmpty()).toList().get(0);
|
||||
Assertions.assertEquals(withCustomAnnotation.getServiceId(), "busybox-service");
|
||||
Assertions.assertNotNull(withCustomAnnotation.getInstanceId());
|
||||
Assertions.assertNotNull(withCustomAnnotation.getHost());
|
||||
Assertions.assertEquals(withCustomAnnotation.getMetadata(),
|
||||
Map.of("k8s_namespace", "default", "type", "ClusterIP", "port.busybox-port", "80"));
|
||||
Assertions.assertTrue(withCustomAnnotation.podMetadata().get("annotations").entrySet().stream().anyMatch(
|
||||
x -> x.getKey().equals("custom-annotation") && x.getValue().equals("custom-annotation-value")));
|
||||
}
|
||||
|
||||
private static void manifests(Phase phase) {
|
||||
|
||||
InputStream deploymentStream = util.inputStream("fabric8-discovery-deployment.yaml");
|
||||
InputStream serviceStream = util.inputStream("fabric8-discovery-service.yaml");
|
||||
InputStream ingressStream = util.inputStream("fabric8-discovery-ingress.yaml");
|
||||
|
||||
Deployment deployment = client.apps().deployments().load(deploymentStream).get();
|
||||
|
||||
List<EnvVar> existing = new ArrayList<>(
|
||||
deployment.getSpec().getTemplate().getSpec().getContainers().get(0).getEnv());
|
||||
existing.add(new EnvVarBuilder().withName("SPRING_CLOUD_KUBERNETES_DISCOVERY_METADATA_ADDPODLABELS")
|
||||
.withValue("true").build());
|
||||
existing.add(new EnvVarBuilder().withName("SPRING_CLOUD_KUBERNETES_DISCOVERY_METADATA_ADDPODANNOTATIONS")
|
||||
.withValue("true").build());
|
||||
existing.add(
|
||||
new EnvVarBuilder().withName("LOGGING_LEVEL_ORG_SPRINGFRAMEWORK_CLOUD_KUBERNETES_FABRIC8_DISCOVERY")
|
||||
.withValue("DEBUG").build());
|
||||
deployment.getSpec().getTemplate().getSpec().getContainers().get(0).setEnv(existing);
|
||||
|
||||
Service service = client.services().load(serviceStream).get();
|
||||
Ingress ingress = client.network().v1().ingresses().load(ingressStream).get();
|
||||
|
||||
if (phase.equals(Phase.CREATE)) {
|
||||
util.createAndWait(NAMESPACE, null, deployment, service, ingress, true);
|
||||
}
|
||||
else {
|
||||
util.deleteAndWait(NAMESPACE, 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);
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user