Adds Discovery Server and Discovery Client (#886)

This commit is contained in:
Ryan Baxter
2021-10-15 07:24:31 -04:00
committed by GitHub
parent 41c37c7b02
commit a265331fd0
62 changed files with 3491 additions and 40 deletions

View File

@@ -114,6 +114,20 @@ access from a Spring Boot application running as a pod.
This is something that you get for free by adding the following dependency inside your project:
====
HTTP Based `DiscoveryClient`
[source,xml]
----
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-kubernetes-discoveryclient</artifactId>
</dependency>
----
====
NOTE: `spring-cloud-starter-kubernetes-discoveryclient` is designed to be used with the
<<spring-cloud-kubernetes-discoveryserver, Spring Cloud Kubernetes DiscoveryServer>>.
====
Fabric8 Kubernetes Client
[source,xml]
@@ -1514,6 +1528,219 @@ items:
----
====
[#spring-cloud-kubernetes-discoveryserver]
## Spring Cloud Kubernetes Discovery Server
The Spring Cloud Kubernetes Discovery Server provides HTTP endpoints apps can use to gather information
about services available within a Kubernetes cluster. The Spring Cloud Kubernetes Discovery Server
can be used by apps using the `spring-cloud-starter-kubernetes-discoveryclient` to provide data to
the `DiscoveryClient` implementation provided by that starter.
### Permissions
The Spring Cloud Discovery server uses
the Kubernetes API server to get data about Service and Endpoint resrouces so it needs list, watch, and
get permissions to use those endpoints. See the below sample Kubernetes deployment YAML for an
examlpe of how to configure the Service Account on Kubernetes.
### Endpoints
There are three endpoints exposed by the server.
#### `/apps`
A `GET` request sent to `/apps` will return a JSON array of available services. Each item contains
the name of the Kubernetes service and service instance information. Below is a sample response.
====
[source,json]
----
[
{
"name":"spring-cloud-kubernetes-discoveryserver",
"serviceInstances":[
{
"instanceId":"836a2f25-daee-4af2-a1be-aab9ce2b938f",
"serviceId":"spring-cloud-kubernetes-discoveryserver",
"host":"10.244.1.6",
"port":8761,
"uri":"http://10.244.1.6:8761",
"secure":false,
"metadata":{
"app":"spring-cloud-kubernetes-discoveryserver",
"kubectl.kubernetes.io/last-applied-configuration":"{\"apiVersion\":\"v1\",\"kind\":\"Service\",\"metadata\":{\"annotations\":{},\"labels\":{\"app\":\"spring-cloud-kubernetes-discoveryserver\"},\"name\":\"spring-cloud-kubernetes-discoveryserver\",\"namespace\":\"default\"},\"spec\":{\"ports\":[{\"name\":\"http\",\"port\":80,\"targetPort\":8761}],\"selector\":{\"app\":\"spring-cloud-kubernetes-discoveryserver\"},\"type\":\"ClusterIP\"}}\n",
"http":"8761"
},
"namespace":"default",
"scheme":"http"
}
]
},
{
"name":"kubernetes",
"serviceInstances":[
{
"instanceId":"1234",
"serviceId":"kubernetes",
"host":"172.18.0.3",
"port":6443,
"uri":"http://172.18.0.3:6443",
"secure":false,
"metadata":{
"provider":"kubernetes",
"component":"apiserver",
"https":"6443"
},
"namespace":"default",
"scheme":"http"
}
]
}
]
----
====
#### `/app/{name}`
A `GET` request to `/app/{name}` can be used to get instance data for all instances of a given
service. Below is a sample response when a `GET` request is made to `/app/kubernetes`.
====
[source,json]
----
[
{
"instanceId":"1234",
"serviceId":"kubernetes",
"host":"172.18.0.3",
"port":6443,
"uri":"http://172.18.0.3:6443",
"secure":false,
"metadata":{
"provider":"kubernetes",
"component":"apiserver",
"https":"6443"
},
"namespace":"default",
"scheme":"http"
}
]
----
====
#### `/app/{name}/{instanceid}`
A `GET` request made to `/app/{name}/{instanceid}` will return the instance data for a specific
instance of a given service. Below is a sample response when a `GET` request is made to `/app/kubernetes/1234`.
====
[source,json]
----
{
"instanceId":"1234",
"serviceId":"kubernetes",
"host":"172.18.0.3",
"port":6443,
"uri":"http://172.18.0.3:6443",
"secure":false,
"metadata":{
"provider":"kubernetes",
"component":"apiserver",
"https":"6443"
},
"namespace":"default",
"scheme":"http"
}
----
====
### Deployment YAML
An image of the Spring Cloud Discovery Server is hosted on Docker Hub.
Below is a sample deployment YAML you can use to deploy the Kubernetes Configuration Watcher to Kubernetes.
====
[source,yaml]
----
---
apiVersion: v1
kind: List
items:
- apiVersion: v1
kind: Service
metadata:
labels:
app: spring-cloud-kubernetes-discoveryserver
name: spring-cloud-kubernetes-discoveryserver
spec:
ports:
- name: http
port: 80
targetPort: 8761
selector:
app: spring-cloud-kubernetes-discoveryserver
type: ClusterIP
- apiVersion: v1
kind: ServiceAccount
metadata:
labels:
app: spring-cloud-kubernetes-discoveryserver
name: spring-cloud-kubernetes-discoveryserver
- apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
labels:
app: spring-cloud-kubernetes-discoveryserver
name: spring-cloud-kubernetes-discoveryserver:view
roleRef:
kind: Role
apiGroup: rbac.authorization.k8s.io
name: namespace-reader
subjects:
- kind: ServiceAccount
name: spring-cloud-kubernetes-discoveryserver
- apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
namespace: default
name: namespace-reader
rules:
- apiGroups: ["", "extensions", "apps"]
resources: ["services", "endpoints"]
verbs: ["get", "list", "watch"]
- apiVersion: apps/v1
kind: Deployment
metadata:
name: spring-cloud-kubernetes-discoveryserver-deployment
spec:
selector:
matchLabels:
app: spring-cloud-kubernetes-discoveryserver
template:
metadata:
labels:
app: spring-cloud-kubernetes-discoveryserver
spec:
serviceAccount: spring-cloud-kubernetes-discoveryserver
containers:
- name: spring-cloud-kubernetes-discoveryserver
image: springcloud/spring-cloud-kubernetes-discoveryserver:2.1.0-SNAPSHOT
imagePullPolicy: IfNotPresent
readinessProbe:
httpGet:
port: 8761
path: /actuator/health/readiness
livenessProbe:
httpGet:
port: 8761
path: /actuator/health/liveness
ports:
- containerPort: 8761
----
====
== Examples
Spring Cloud Kubernetes tries to make it transparent for your applications to consume Kubernetes Native Services by

View File

@@ -8,6 +8,20 @@ access from a Spring Boot application running as a pod.
This is something that you get for free by adding the following dependency inside your project:
====
HTTP Based `DiscoveryClient`
[source,xml]
----
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-kubernetes-discoveryclient</artifactId>
</dependency>
----
====
NOTE: `spring-cloud-starter-kubernetes-discoveryclient` is designed to be used with the
<<spring-cloud-kubernetes-discoveryserver, Spring Cloud Kubernetes DiscoveryServer>>.
====
Fabric8 Kubernetes Client
[source,xml]

View File

@@ -0,0 +1,212 @@
[#spring-cloud-kubernetes-discoveryserver]
## Spring Cloud Kubernetes Discovery Server
The Spring Cloud Kubernetes Discovery Server provides HTTP endpoints apps can use to gather information
about services available within a Kubernetes cluster. The Spring Cloud Kubernetes Discovery Server
can be used by apps using the `spring-cloud-starter-kubernetes-discoveryclient` to provide data to
the `DiscoveryClient` implementation provided by that starter.
### Permissions
The Spring Cloud Discovery server uses
the Kubernetes API server to get data about Service and Endpoint resrouces so it needs list, watch, and
get permissions to use those endpoints. See the below sample Kubernetes deployment YAML for an
examlpe of how to configure the Service Account on Kubernetes.
### Endpoints
There are three endpoints exposed by the server.
#### `/apps`
A `GET` request sent to `/apps` will return a JSON array of available services. Each item contains
the name of the Kubernetes service and service instance information. Below is a sample response.
====
[source,json]
----
[
{
"name":"spring-cloud-kubernetes-discoveryserver",
"serviceInstances":[
{
"instanceId":"836a2f25-daee-4af2-a1be-aab9ce2b938f",
"serviceId":"spring-cloud-kubernetes-discoveryserver",
"host":"10.244.1.6",
"port":8761,
"uri":"http://10.244.1.6:8761",
"secure":false,
"metadata":{
"app":"spring-cloud-kubernetes-discoveryserver",
"kubectl.kubernetes.io/last-applied-configuration":"{\"apiVersion\":\"v1\",\"kind\":\"Service\",\"metadata\":{\"annotations\":{},\"labels\":{\"app\":\"spring-cloud-kubernetes-discoveryserver\"},\"name\":\"spring-cloud-kubernetes-discoveryserver\",\"namespace\":\"default\"},\"spec\":{\"ports\":[{\"name\":\"http\",\"port\":80,\"targetPort\":8761}],\"selector\":{\"app\":\"spring-cloud-kubernetes-discoveryserver\"},\"type\":\"ClusterIP\"}}\n",
"http":"8761"
},
"namespace":"default",
"scheme":"http"
}
]
},
{
"name":"kubernetes",
"serviceInstances":[
{
"instanceId":"1234",
"serviceId":"kubernetes",
"host":"172.18.0.3",
"port":6443,
"uri":"http://172.18.0.3:6443",
"secure":false,
"metadata":{
"provider":"kubernetes",
"component":"apiserver",
"https":"6443"
},
"namespace":"default",
"scheme":"http"
}
]
}
]
----
====
#### `/app/{name}`
A `GET` request to `/app/{name}` can be used to get instance data for all instances of a given
service. Below is a sample response when a `GET` request is made to `/app/kubernetes`.
====
[source,json]
----
[
{
"instanceId":"1234",
"serviceId":"kubernetes",
"host":"172.18.0.3",
"port":6443,
"uri":"http://172.18.0.3:6443",
"secure":false,
"metadata":{
"provider":"kubernetes",
"component":"apiserver",
"https":"6443"
},
"namespace":"default",
"scheme":"http"
}
]
----
====
#### `/app/{name}/{instanceid}`
A `GET` request made to `/app/{name}/{instanceid}` will return the instance data for a specific
instance of a given service. Below is a sample response when a `GET` request is made to `/app/kubernetes/1234`.
====
[source,json]
----
{
"instanceId":"1234",
"serviceId":"kubernetes",
"host":"172.18.0.3",
"port":6443,
"uri":"http://172.18.0.3:6443",
"secure":false,
"metadata":{
"provider":"kubernetes",
"component":"apiserver",
"https":"6443"
},
"namespace":"default",
"scheme":"http"
}
----
====
### Deployment YAML
An image of the Spring Cloud Discovery Server is hosted on Docker Hub.
Below is a sample deployment YAML you can use to deploy the Kubernetes Configuration Watcher to Kubernetes.
====
[source,yaml]
----
---
apiVersion: v1
kind: List
items:
- apiVersion: v1
kind: Service
metadata:
labels:
app: spring-cloud-kubernetes-discoveryserver
name: spring-cloud-kubernetes-discoveryserver
spec:
ports:
- name: http
port: 80
targetPort: 8761
selector:
app: spring-cloud-kubernetes-discoveryserver
type: ClusterIP
- apiVersion: v1
kind: ServiceAccount
metadata:
labels:
app: spring-cloud-kubernetes-discoveryserver
name: spring-cloud-kubernetes-discoveryserver
- apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
labels:
app: spring-cloud-kubernetes-discoveryserver
name: spring-cloud-kubernetes-discoveryserver:view
roleRef:
kind: Role
apiGroup: rbac.authorization.k8s.io
name: namespace-reader
subjects:
- kind: ServiceAccount
name: spring-cloud-kubernetes-discoveryserver
- apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
namespace: default
name: namespace-reader
rules:
- apiGroups: ["", "extensions", "apps"]
resources: ["services", "endpoints"]
verbs: ["get", "list", "watch"]
- apiVersion: apps/v1
kind: Deployment
metadata:
name: spring-cloud-kubernetes-discoveryserver-deployment
spec:
selector:
matchLabels:
app: spring-cloud-kubernetes-discoveryserver
template:
metadata:
labels:
app: spring-cloud-kubernetes-discoveryserver
spec:
serviceAccount: spring-cloud-kubernetes-discoveryserver
containers:
- name: spring-cloud-kubernetes-discoveryserver
image: springcloud/spring-cloud-kubernetes-discoveryserver:2.1.0-SNAPSHOT
imagePullPolicy: IfNotPresent
readinessProbe:
httpGet:
port: 8761
path: /actuator/health/readiness
livenessProbe:
httpGet:
port: 8761
path: /actuator/health/liveness
ports:
- containerPort: 8761
----
====

View File

@@ -33,6 +33,8 @@ include::spring-cloud-kubernetes-configuration-watcher.adoc[]
include::spring-cloud-kubernetes-configserver.adoc[]
include::spring-cloud-kubernetes-discoveryserver.adoc[]
include::examples.adoc[]
include::other-resources.adoc[]

View File

@@ -110,6 +110,8 @@
<module>docs</module>
<module>spring-cloud-kubernetes-fabric8-loadbalancer</module>
<module>spring-cloud-starter-kubernetes-fabric8-loadbalancer</module>
<module>spring-cloud-kubernetes-discovery</module>
<module>spring-cloud-starter-kubernetes-discoveryclient</module>
</modules>
<dependencyManagement>

View File

@@ -3,4 +3,5 @@ set -e
./mvnw deploy -DskipTests -B -Pfast,deploy ${@}
./mvnw dockerfile:push -pl :spring-cloud-kubernetes-configuration-watcher -Pdockerpush ${@}
./mvnw dockerfile:push -pl :spring-cloud-kubernetes-discoveryserver -Pdockerpush ${@}
./mvnw dockerfile:push -pl :spring-cloud-kubernetes-configserver -Pdockerpush ${@}

View File

@@ -163,7 +163,8 @@ public class KubernetesInformerDiscoveryClient implements DiscoveryClient, Initi
return addresses.stream()
.map(addr -> new KubernetesServiceInstance(
addr.getTargetRef() != null ? addr.getTargetRef().getUid() : "", serviceId,
addr.getIp(), port, metadata, false));
addr.getIp(), port, metadata, false, service.getMetadata().getNamespace(),
service.getMetadata().getClusterName()));
}).collect(Collectors.toList());
}

View File

@@ -151,8 +151,8 @@ public class KubernetesInformerDiscoveryClientTests {
sharedInformerFactory, serviceLister, endpointsLister, null, null, kubernetesDiscoveryProperties);
assertThat(discoveryClient.getInstances("test-svc-1").toArray()).isEmpty();
assertThat(discoveryClient.getInstances("test-svc-3").toArray())
.containsOnly(new KubernetesServiceInstance("", "test-svc-3", "2.2.2.2", 8080, new HashMap<>(), false));
assertThat(discoveryClient.getInstances("test-svc-3").toArray()).containsOnly(new KubernetesServiceInstance("",
"test-svc-3", "2.2.2.2", 8080, new HashMap<>(), false, "namespace1", null));
}
@Test
@@ -179,8 +179,8 @@ public class KubernetesInformerDiscoveryClientTests {
KubernetesInformerDiscoveryClient discoveryClient = new KubernetesInformerDiscoveryClient("",
sharedInformerFactory, serviceLister, endpointsLister, null, null, kubernetesDiscoveryProperties);
assertThat(discoveryClient.getInstances("test-svc-1"))
.containsOnly(new KubernetesServiceInstance("", "test-svc-1", "2.2.2.2", 8080, new HashMap<>(), false));
assertThat(discoveryClient.getInstances("test-svc-1")).containsOnly(new KubernetesServiceInstance("",
"test-svc-1", "2.2.2.2", 8080, new HashMap<>(), false, "namespace1", null));
verify(kubernetesDiscoveryProperties, times(2)).isAllNamespaces();
verify(kubernetesDiscoveryProperties, times(1)).getPrimaryPortName();
@@ -196,8 +196,8 @@ public class KubernetesInformerDiscoveryClientTests {
KubernetesInformerDiscoveryClient discoveryClient = new KubernetesInformerDiscoveryClient("namespace1",
sharedInformerFactory, serviceLister, endpointsLister, null, null, kubernetesDiscoveryProperties);
assertThat(discoveryClient.getInstances("test-svc-1"))
.containsOnly(new KubernetesServiceInstance("", "test-svc-1", "2.2.2.2", 8080, new HashMap<>(), false));
assertThat(discoveryClient.getInstances("test-svc-1")).containsOnly(new KubernetesServiceInstance("",
"test-svc-1", "2.2.2.2", 8080, new HashMap<>(), false, "namespace1", null));
verify(kubernetesDiscoveryProperties, times(1)).isAllNamespaces();
verify(kubernetesDiscoveryProperties, times(1)).getPrimaryPortName();
}
@@ -229,8 +229,8 @@ public class KubernetesInformerDiscoveryClientTests {
KubernetesInformerDiscoveryClient discoveryClient = new KubernetesInformerDiscoveryClient("namespace1",
sharedInformerFactory, serviceLister, endpointsLister, null, null, kubernetesDiscoveryProperties);
assertThat(discoveryClient.getInstances("test-svc-1"))
.containsOnly(new KubernetesServiceInstance("", "test-svc-1", "2.2.2.2", 8080, new HashMap<>(), false));
assertThat(discoveryClient.getInstances("test-svc-1")).containsOnly(new KubernetesServiceInstance("",
"test-svc-1", "2.2.2.2", 8080, new HashMap<>(), false, "namespace1", null));
verify(kubernetesDiscoveryProperties, times(1)).isAllNamespaces();
verify(kubernetesDiscoveryProperties, times(1)).getPrimaryPortName();
verify(kubernetesDiscoveryProperties, times(1)).isIncludeNotReadyAddresses();
@@ -275,8 +275,8 @@ public class KubernetesInformerDiscoveryClientTests {
KubernetesInformerDiscoveryClient discoveryClient = new KubernetesInformerDiscoveryClient("namespace1",
sharedInformerFactory, serviceLister, endpointsLister, null, null, kubernetesDiscoveryProperties);
assertThat(discoveryClient.getInstances("test-svc-1"))
.containsOnly(new KubernetesServiceInstance("", "test-svc-1", "1.1.1.1", 443, new HashMap<>(), false));
assertThat(discoveryClient.getInstances("test-svc-1")).containsOnly(new KubernetesServiceInstance("",
"test-svc-1", "1.1.1.1", 443, new HashMap<>(), false, "namespace1", null));
verify(kubernetesDiscoveryProperties, times(1)).isAllNamespaces();
verify(kubernetesDiscoveryProperties, times(1)).getPrimaryPortName();
verify(kubernetesDiscoveryProperties, times(1)).isIncludeNotReadyAddresses();
@@ -294,8 +294,8 @@ public class KubernetesInformerDiscoveryClientTests {
KubernetesInformerDiscoveryClient discoveryClient = new KubernetesInformerDiscoveryClient("namespace1",
sharedInformerFactory, serviceLister, endpointsLister, null, null, kubernetesDiscoveryProperties);
assertThat(discoveryClient.getInstances("test-svc-1"))
.containsOnly(new KubernetesServiceInstance("", "test-svc-1", "1.1.1.1", 80, new HashMap<>(), false));
assertThat(discoveryClient.getInstances("test-svc-1")).containsOnly(new KubernetesServiceInstance("",
"test-svc-1", "1.1.1.1", 80, new HashMap<>(), false, "namespace1", null));
verify(kubernetesDiscoveryProperties, times(1)).isAllNamespaces();
verify(kubernetesDiscoveryProperties, times(1)).getPrimaryPortName();
}
@@ -311,8 +311,8 @@ public class KubernetesInformerDiscoveryClientTests {
KubernetesInformerDiscoveryClient discoveryClient = new KubernetesInformerDiscoveryClient("namespace1",
sharedInformerFactory, serviceLister, endpointsLister, null, null, kubernetesDiscoveryProperties);
assertThat(discoveryClient.getInstances("test-svc-1"))
.containsOnly(new KubernetesServiceInstance("", "test-svc-1", "1.1.1.1", 443, new HashMap<>(), false));
assertThat(discoveryClient.getInstances("test-svc-1")).containsOnly(new KubernetesServiceInstance("",
"test-svc-1", "1.1.1.1", 443, new HashMap<>(), false, "namespace1", null));
verify(kubernetesDiscoveryProperties, times(1)).getPrimaryPortName();
verify(kubernetesDiscoveryProperties, times(1)).isAllNamespaces();
verify(kubernetesDiscoveryProperties, times(1)).isIncludeNotReadyAddresses();
@@ -330,8 +330,8 @@ public class KubernetesInformerDiscoveryClientTests {
KubernetesInformerDiscoveryClient discoveryClient = new KubernetesInformerDiscoveryClient("namespace1",
sharedInformerFactory, serviceLister, endpointsLister, null, null, kubernetesDiscoveryProperties);
assertThat(discoveryClient.getInstances("test-svc-1"))
.containsOnly(new KubernetesServiceInstance("", "test-svc-1", "1.1.1.1", 80, new HashMap<>(), false));
assertThat(discoveryClient.getInstances("test-svc-1")).containsOnly(new KubernetesServiceInstance("",
"test-svc-1", "1.1.1.1", 80, new HashMap<>(), false, "namespace1", null));
verify(kubernetesDiscoveryProperties, times(1)).isAllNamespaces();
verify(kubernetesDiscoveryProperties, times(1)).getPrimaryPortName();
}
@@ -346,8 +346,8 @@ public class KubernetesInformerDiscoveryClientTests {
KubernetesInformerDiscoveryClient discoveryClient = new KubernetesInformerDiscoveryClient("namespace1",
sharedInformerFactory, serviceLister, endpointsLister, null, null, kubernetesDiscoveryProperties);
assertThat(discoveryClient.getInstances("test-svc-1"))
.containsOnly(new KubernetesServiceInstance("", "test-svc-1", "1.1.1.1", 443, new HashMap<>(), false));
assertThat(discoveryClient.getInstances("test-svc-1")).containsOnly(new KubernetesServiceInstance("",
"test-svc-1", "1.1.1.1", 443, new HashMap<>(), false, "namespace1", null));
verify(kubernetesDiscoveryProperties, times(1)).isAllNamespaces();
verify(kubernetesDiscoveryProperties, times(1)).getPrimaryPortName();
}
@@ -362,8 +362,8 @@ public class KubernetesInformerDiscoveryClientTests {
KubernetesInformerDiscoveryClient discoveryClient = new KubernetesInformerDiscoveryClient("namespace1",
sharedInformerFactory, serviceLister, endpointsLister, null, null, kubernetesDiscoveryProperties);
assertThat(discoveryClient.getInstances("test-svc-1"))
.containsOnly(new KubernetesServiceInstance("", "test-svc-1", "1.1.1.1", 80, new HashMap<>(), false));
assertThat(discoveryClient.getInstances("test-svc-1")).containsOnly(new KubernetesServiceInstance("",
"test-svc-1", "1.1.1.1", 80, new HashMap<>(), false, "namespace1", null));
verify(kubernetesDiscoveryProperties, times(1)).isAllNamespaces();
verify(kubernetesDiscoveryProperties, times(1)).getPrimaryPortName();
}
@@ -379,8 +379,8 @@ public class KubernetesInformerDiscoveryClientTests {
KubernetesInformerDiscoveryClient discoveryClient = new KubernetesInformerDiscoveryClient("namespace1",
sharedInformerFactory, serviceLister, endpointsLister, null, null, kubernetesDiscoveryProperties);
assertThat(discoveryClient.getInstances("test-svc-1"))
.containsOnly(new KubernetesServiceInstance("", "test-svc-1", "1.1.1.1", 80, new HashMap<>(), false));
assertThat(discoveryClient.getInstances("test-svc-1")).containsOnly(new KubernetesServiceInstance("",
"test-svc-1", "1.1.1.1", 80, new HashMap<>(), false, "namespace1", null));
verify(kubernetesDiscoveryProperties, times(1)).isAllNamespaces();
verify(kubernetesDiscoveryProperties, times(1)).getPrimaryPortName();
}

View File

@@ -116,8 +116,9 @@ public class KubernetesInformerReactiveDiscoveryClientTests {
new KubernetesNamespaceProvider(new MockEnvironment()), sharedInformerFactory, serviceLister,
endpointsLister, null, null, kubernetesDiscoveryProperties);
StepVerifier.create(discoveryClient.getInstances("test-svc-1"))
.expectNext(new KubernetesServiceInstance("", "test-svc-1", "2.2.2.2", 8080, new HashMap<>(), false))
StepVerifier
.create(discoveryClient.getInstances("test-svc-1")).expectNext(new KubernetesServiceInstance("",
"test-svc-1", "2.2.2.2", 8080, new HashMap<>(), false, "namespace1", null))
.expectComplete().verify();
verify(kubernetesDiscoveryProperties, times(2)).isAllNamespaces();
@@ -135,8 +136,9 @@ public class KubernetesInformerReactiveDiscoveryClientTests {
kubernetesNamespaceProvider, sharedInformerFactory, serviceLister, endpointsLister, null, null,
kubernetesDiscoveryProperties);
StepVerifier.create(discoveryClient.getInstances("test-svc-1"))
.expectNext(new KubernetesServiceInstance("", "test-svc-1", "2.2.2.2", 8080, new HashMap<>(), false))
StepVerifier
.create(discoveryClient.getInstances("test-svc-1")).expectNext(new KubernetesServiceInstance("",
"test-svc-1", "2.2.2.2", 8080, new HashMap<>(), false, "namespace1", null))
.expectComplete().verify();
verify(kubernetesDiscoveryProperties, times(1)).isAllNamespaces();

View File

@@ -20,6 +20,10 @@
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-commons</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-actuator-autoconfigure</artifactId>

View File

@@ -37,19 +37,23 @@ public class KubernetesServiceInstance implements ServiceInstance {
private static final String COLON = ":";
private final String instanceId;
private String instanceId;
private final String serviceId;
private String serviceId;
private final String host;
private String host;
private final int port;
private int port;
private final URI uri;
private URI uri;
private final Boolean secure;
private Boolean secure;
private final Map<String, String> metadata;
private Map<String, String> metadata;
private String namespace;
private String cluster;
/**
* @param instanceId the id of the instance.
@@ -68,6 +72,35 @@ public class KubernetesServiceInstance implements ServiceInstance {
this.metadata = metadata;
this.secure = secure;
this.uri = createUri(secure ? HTTPS_PREFIX : HTTP_PREFIX, host, port);
this.namespace = null;
this.cluster = null;
}
/**
* @param instanceId the id of the instance.
* @param serviceId the id of the service.
* @param host the address where the service instance can be found.
* @param port the port on which the service is running.
* @param metadata a map containing metadata.
* @param secure indicates whether or not the connection needs to be secure.
* @param namespace the namespace of the service.
* @param cluster the clust the service resides in.
*/
public KubernetesServiceInstance(String instanceId, String serviceId, String host, int port,
Map<String, String> metadata, Boolean secure, String namespace, String cluster) {
this.instanceId = instanceId;
this.serviceId = serviceId;
this.host = host;
this.port = port;
this.metadata = metadata;
this.secure = secure;
this.uri = createUri(secure ? HTTPS_PREFIX : HTTP_PREFIX, host, port);
this.namespace = namespace;
this.cluster = cluster;
}
// Allows for deserialization
public KubernetesServiceInstance() {
}
@Override
@@ -114,7 +147,51 @@ public class KubernetesServiceInstance implements ServiceInstance {
}
public String getNamespace() {
return this.metadata != null ? this.metadata.get(NAMESPACE_METADATA_KEY) : null;
return namespace != null ? namespace : this.metadata.get(NAMESPACE_METADATA_KEY);
}
public String getCluster() {
return this.cluster;
}
public void setInstanceId(String instanceId) {
this.instanceId = instanceId;
}
public void setServiceId(String serviceId) {
this.serviceId = serviceId;
}
public void setHost(String host) {
this.host = host;
}
public void setPort(int port) {
this.port = port;
}
public void setUri(URI uri) {
this.uri = uri;
}
public void setSecure(Boolean secure) {
this.secure = secure;
}
public void setMetadata(Map<String, String> metadata) {
this.metadata = metadata;
}
public void setNamespace(String namespace) {
this.namespace = namespace;
}
public void setCluster(String cluster) {
this.cluster = cluster;
}
public Boolean getSecure() {
return secure;
}
@Override
@@ -129,19 +206,20 @@ public class KubernetesServiceInstance implements ServiceInstance {
return port == that.port && Objects.equals(instanceId, that.instanceId)
&& Objects.equals(serviceId, that.serviceId) && Objects.equals(host, that.host)
&& Objects.equals(uri, that.uri) && Objects.equals(secure, that.secure)
&& Objects.equals(metadata, that.metadata);
&& Objects.equals(metadata, that.metadata) && Objects.equals(getNamespace(), that.getNamespace())
&& Objects.equals(cluster, that.cluster);
}
@Override
public String toString() {
return "KubernetesServiceInstance{" + "instanceId='" + instanceId + '\'' + ", serviceId='" + serviceId + '\''
+ ", host='" + host + '\'' + ", port=" + port + ", uri=" + uri + ", secure=" + secure + ", metadata="
+ metadata + '}';
+ ", host='" + host + '\'' + ", port=" + port + ", uri=" + uri + ", secure=" + secure + ", namespace="
+ getNamespace() + ", cluster=" + cluster + ", metadata=" + metadata + '}';
}
@Override
public int hashCode() {
return Objects.hash(instanceId, serviceId, host, port, uri, secure, metadata);
return Objects.hash(instanceId, serviceId, host, port, uri, secure, getNamespace(), cluster, metadata);
}
}

View File

@@ -14,6 +14,7 @@
<modules>
<module>spring-cloud-kubernetes-configuration-watcher</module>
<module>spring-cloud-kubernetes-discoveryserver</module>
<module>spring-cloud-kubernetes-configserver</module>
</modules>

View File

@@ -0,0 +1,74 @@
---
apiVersion: v1
kind: List
items:
- apiVersion: v1
kind: Service
metadata:
labels:
app: spring-cloud-kubernetes-discoveryserver
name: spring-cloud-kubernetes-discoveryserver
spec:
ports:
- name: http
port: 80
targetPort: 8761
selector:
app: spring-cloud-kubernetes-discoveryserver
type: LoadBalancer
- apiVersion: v1
kind: ServiceAccount
metadata:
labels:
app: spring-cloud-kubernetes-discoveryserver
name: spring-cloud-kubernetes-discoveryserver
- apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
labels:
app: spring-cloud-kubernetes-discoveryserver
name: spring-cloud-kubernetes-discoveryserver:view
roleRef:
kind: Role
apiGroup: rbac.authorization.k8s.io
name: namespace-reader
subjects:
- kind: ServiceAccount
name: spring-cloud-kubernetes-discoveryserver
- apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
namespace: default
name: namespace-reader
rules:
- apiGroups: ["", "extensions", "apps"]
resources: ["configmaps", "pods", "services", "endpoints", "secrets"]
verbs: ["get", "list", "watch"]
- apiVersion: apps/v1
kind: Deployment
metadata:
name: spring-cloud-kubernetes-discoveryserver-deployment
spec:
selector:
matchLabels:
app: spring-cloud-kubernetes-discoveryserver
template:
metadata:
labels:
app: spring-cloud-kubernetes-discoveryserver
spec:
serviceAccount: spring-cloud-kubernetes-discoveryserver
containers:
- name: spring-cloud-kubernetes-discoveryserver
image: springcloud/spring-cloud-kubernetes-discoveryserver:2.1.0-SNAPSHOT
imagePullPolicy: IfNotPresent
readinessProbe:
httpGet:
port: 8761
path: /actuator/health/readiness
livenessProbe:
httpGet:
port: 8761
path: /actuator/health/liveness
ports:
- containerPort: 8761

View File

@@ -0,0 +1,149 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>spring-cloud-kubernetes-controllers</artifactId>
<groupId>org.springframework.cloud</groupId>
<version>2.1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>spring-cloud-kubernetes-discoveryserver</artifactId>
<properties>
<jib.version>1.8.0</jib.version>
<base.image>openjdk:8u222-slim</base.image>
<docker.registry.organization>springcloud</docker.registry.organization>
<plexus-archiver.version>4.1.0</plexus-archiver.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-kubernetes-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.github.tomakehurst</groupId>
<artifactId>wiremock-jre8</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<image>
<name>${env.IMAGE}</name>
</image>
<goal>build-image</goal>
</configuration>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>build-image</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
<profiles>
<profile>
<id>dockerpush</id>
<build>
<plugins>
<plugin>
<groupId>com.spotify</groupId>
<artifactId>dockerfile-maven-plugin</artifactId>
<version>1.4.12</version>
<configuration>
<repository>${docker.registry.organization}/${artifactId}</repository>
<tag>${project.version}</tag>
<username>${env.DOCKER_HUB_USERNAME}</username>
<password>${env.DOCKER_HUB_PASSWORD}</password>
<build>
<noCache>true</noCache>
</build>
</configuration>
<dependencies>
<dependency>
<groupId>org.codehaus.plexus</groupId>
<artifactId>plexus-archiver</artifactId>
<version>${plexus-archiver.version}</version>
</dependency>
</dependencies>
</plugin>
</plugins>
</build>
</profile>
<profile>
<id>imagename</id>
<activation>
<property>
<name>!env.IMAGE</name>
</property>
</activation>
<properties>
<env.IMAGE>springcloud/${project.artifactId}:${project.version}</env.IMAGE>
</properties>
</profile>
<profile>
<id>jib</id>
<build>
<plugins>
<plugin>
<groupId>com.google.cloud.tools</groupId>
<artifactId>jib-maven-plugin</artifactId>
<version>${jib.version}</version>
<configuration>
<from>
<image>${base.image}</image>
</from>
<to>
<image>spring-cloud/${project.artifactId}</image>
</to>
<container>
<user>nobody:nogroup</user>
<environment>
</environment>
</container>
</configuration>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>dockerBuild</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</profile>
</profiles>
</project>

View File

@@ -0,0 +1,20 @@
apiVersion: skaffold/v2alpha3
kind: Config
metadata:
name: spring-cloud-kubernetes-discoveryserver
build:
artifacts:
- image: springcloud/spring-cloud-kubernetes-discoveryserver
# custom:
# buildCommand: "../../mvnw clean install"
# dependencies:
# paths:
# - src
# - pom.xml
jib: {
args: ["-Pjib"]
}
deploy:
kubectl:
manifests:
- k8s/deployment.yaml

View File

@@ -0,0 +1,32 @@
/*
* Copyright 2013-2021 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.springframewok.cloud.kubernetes.discoveryserver;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;
/**
* @author Ryan Baxter
*/
@SpringBootApplication
public class DiscoveryServerApplication {
public static void main(String[] args) {
new SpringApplicationBuilder(DiscoveryServerApplication.class).run(args);
}
}

View File

@@ -0,0 +1,110 @@
/*
* Copyright 2013-2021 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.springframewok.cloud.kubernetes.discoveryserver;
import java.util.List;
import java.util.Objects;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.kubernetes.client.discovery.reactive.KubernetesInformerReactiveDiscoveryClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
/**
* @author Ryan Baxter
*/
@RestController
public class DiscoveryServerController {
private KubernetesInformerReactiveDiscoveryClient reactiveDiscoveryClient;
public DiscoveryServerController(KubernetesInformerReactiveDiscoveryClient reactiveDiscoveryClient) {
this.reactiveDiscoveryClient = reactiveDiscoveryClient;
}
@GetMapping("/apps")
public Flux<Service> apps() {
return reactiveDiscoveryClient.getServices().flatMap(service -> reactiveDiscoveryClient.getInstances(service)
.collectList().flatMap(serviceInstances -> Mono.just(new Service(service, serviceInstances))));
}
@GetMapping("/apps/{name}")
public Flux<ServiceInstance> appInstances(@PathVariable String name) {
return reactiveDiscoveryClient.getInstances(name);
}
@GetMapping("/app/{name}/{instanceId}")
public Mono<ServiceInstance> appInstance(@PathVariable String name, @PathVariable String instanceId) {
return reactiveDiscoveryClient.getInstances(name)
.filter(serviceInstance -> serviceInstance.getInstanceId().equals(instanceId)).singleOrEmpty();
}
public static class Service {
private String name;
private List<ServiceInstance> serviceInstances;
public Service() {
}
public Service(String name, List<ServiceInstance> serviceInstances) {
this.name = name;
this.serviceInstances = serviceInstances;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public List<ServiceInstance> getServiceInstances() {
return serviceInstances;
}
public void setServiceInstances(List<ServiceInstance> serviceInstances) {
this.serviceInstances = serviceInstances;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
Service service = (Service) o;
return Objects.equals(getName(), service.getName())
&& Objects.equals(getServiceInstances(), service.getServiceInstances());
}
@Override
public int hashCode() {
return Objects.hash(getName(), getServiceInstances());
}
}
}

View File

@@ -0,0 +1,120 @@
/*
* Copyright 2013-2021 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.springframewok.cloud.kubernetes.discoveryserver;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import reactor.core.publisher.Flux;
import reactor.test.StepVerifier;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.kubernetes.client.discovery.reactive.KubernetesInformerReactiveDiscoveryClient;
import org.springframework.cloud.kubernetes.commons.discovery.KubernetesServiceInstance;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
/**
* @author Ryan Baxter
*/
class DiscoveryServerControllerTests {
private static final KubernetesServiceInstance serviceAInstance1 = new KubernetesServiceInstance("serviceAInstance1",
"serviceAInstance1", "2.2.2.2", 8080, new HashMap<>(), false, "namespace1", null);
private static final KubernetesServiceInstance serviceAInstance2 = new KubernetesServiceInstance("serviceAInstance2",
"serviceAInstance2", "2.2.2.2", 8080, new HashMap<>(), false, "namespace1", null);
private static final KubernetesServiceInstance serviceAInstance3 = new KubernetesServiceInstance("serviceAInstance3",
"serviceAInstance3", "2.2.2.2", 8080, new HashMap<>(), false, "namespace2", null);
private static final KubernetesServiceInstance serviceBInstance1 = new KubernetesServiceInstance("serviceBInstance1",
"serviceBInstance1", "2.2.2.2", 8080, new HashMap<>(), false, "namespace1", null);
private static final KubernetesServiceInstance serviceCInstance1 = new KubernetesServiceInstance("serviceCInstance1",
"serviceCInstance1", "2.2.2.2", 8080, new HashMap<>(), false, "namespace2", null);
private static DiscoveryServerController.Service serviceA = new DiscoveryServerController.Service();
private static DiscoveryServerController.Service serviceB = new DiscoveryServerController.Service();
private static DiscoveryServerController.Service serviceC = new DiscoveryServerController.Service();
private static KubernetesInformerReactiveDiscoveryClient discoveryClient;
@BeforeAll
static void beforeAll() {
Flux<String> services = Flux.just("serviceA", "serviceB", "serviceC");
List<ServiceInstance> serviceAInstanceList = new ArrayList<>();
serviceAInstanceList.add(serviceAInstance1);
serviceAInstanceList.add(serviceAInstance2);
serviceAInstanceList.add(serviceAInstance3);
Flux<ServiceInstance> serviceAInstances = Flux.fromIterable(serviceAInstanceList);
List<ServiceInstance> serviceBInstanceList = Collections.singletonList(serviceBInstance1);
Flux<ServiceInstance> serviceBInstances = Flux.fromIterable(serviceBInstanceList);
List<ServiceInstance> serviceCInstanceList = Collections.singletonList(serviceCInstance1);
Flux<ServiceInstance> serviceCInstances = Flux.fromIterable(serviceCInstanceList);
discoveryClient = mock(KubernetesInformerReactiveDiscoveryClient.class);
when(discoveryClient.getServices()).thenReturn(services);
when(discoveryClient.getInstances(eq("serviceA"))).thenReturn(serviceAInstances);
when(discoveryClient.getInstances(eq("serviceB"))).thenReturn(serviceBInstances);
when(discoveryClient.getInstances(eq("serviceC"))).thenReturn(serviceCInstances);
when(discoveryClient.getInstances(eq("serviceD"))).thenReturn(Flux.empty());
serviceA.setName("serviceA");
serviceA.setServiceInstances(serviceAInstanceList);
serviceB.setName("serviceB");
serviceB.setServiceInstances(serviceBInstanceList);
serviceC.setName("serviceC");
serviceC.setServiceInstances(serviceCInstanceList);
}
@Test
void apps() {
DiscoveryServerController controller = new DiscoveryServerController(discoveryClient);
StepVerifier.create(controller.apps()).expectNext(serviceA, serviceB, serviceC).verifyComplete();
}
@Test
void appInstances() {
DiscoveryServerController controller = new DiscoveryServerController(discoveryClient);
StepVerifier.create(controller.appInstances("serviceA")).expectNext(serviceAInstance1, serviceAInstance2, serviceAInstance3).verifyComplete();
StepVerifier.create(controller.appInstances("serviceB")).expectNext(serviceBInstance1).verifyComplete();
StepVerifier.create(controller.appInstances("serviceC")).expectNext(serviceCInstance1).verifyComplete();
StepVerifier.create(controller.appInstances("serviceD")).expectNextCount(0).verifyComplete();
}
@Test
void appInstance() {
DiscoveryServerController controller = new DiscoveryServerController(discoveryClient);
StepVerifier.create(controller.appInstance("serviceA", "serviceAInstance2")).expectNext(serviceAInstance2).verifyComplete();
StepVerifier.create(controller.appInstance("serviceB", "doesnotexist")).expectNextCount(0).verifyComplete();
}
}

View File

@@ -0,0 +1,208 @@
/*
* Copyright 2013-2021 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.springframewok.cloud.kubernetes.discoveryserver;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import com.github.tomakehurst.wiremock.WireMockServer;
import com.github.tomakehurst.wiremock.client.WireMock;
import io.kubernetes.client.openapi.ApiClient;
import io.kubernetes.client.openapi.JSON;
import io.kubernetes.client.openapi.models.V1EndpointAddress;
import io.kubernetes.client.openapi.models.V1EndpointPort;
import io.kubernetes.client.openapi.models.V1EndpointSubset;
import io.kubernetes.client.openapi.models.V1Endpoints;
import io.kubernetes.client.openapi.models.V1EndpointsListBuilder;
import io.kubernetes.client.openapi.models.V1ListMetaBuilder;
import io.kubernetes.client.openapi.models.V1ObjectMeta;
import io.kubernetes.client.openapi.models.V1ObjectReferenceBuilder;
import io.kubernetes.client.openapi.models.V1Service;
import io.kubernetes.client.openapi.models.V1ServiceListBuilder;
import io.kubernetes.client.openapi.models.V1ServiceSpec;
import io.kubernetes.client.openapi.models.V1ServiceStatus;
import io.kubernetes.client.util.ClientBuilder;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.cloud.kubernetes.commons.KubernetesNamespaceProvider;
import org.springframework.cloud.kubernetes.commons.discovery.KubernetesServiceInstance;
import org.springframework.context.annotation.Bean;
import org.springframework.test.web.reactive.server.WebTestClient;
import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
import static com.github.tomakehurst.wiremock.client.WireMock.get;
import static com.github.tomakehurst.wiremock.client.WireMock.stubFor;
import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.options;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
/**
* @author Ryan Baxter
*/
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, classes = DiscoveryServerIntegrationTests.TestConfig.class,
properties = {"debug=true"})
public class DiscoveryServerIntegrationTests {
private static final V1Service testService1 = new V1Service()
.metadata(new V1ObjectMeta().name("test-svc-1").namespace("namespace1"))
.spec(new V1ServiceSpec().loadBalancerIP("1.1.1.1")).status(new V1ServiceStatus());
private static final V1Endpoints testEndpoints1 = new V1Endpoints()
.metadata(new V1ObjectMeta().name("test-svc-1").namespace("namespace1"))
.addSubsetsItem(new V1EndpointSubset().addPortsItem(new V1EndpointPort().port(8080).name("http"))
.addAddressesItem(new V1EndpointAddress().ip("2.2.2.2").targetRef(new V1ObjectReferenceBuilder().withUid("uid1").build())));
private static final V1Service testService2 = new V1Service()
.metadata(new V1ObjectMeta().name("test-svc-1").namespace("namespace2"))
.spec(new V1ServiceSpec().loadBalancerIP("1.1.1.1")).status(new V1ServiceStatus());
private static final V1Service testService3 = new V1Service()
.metadata(new V1ObjectMeta().name("test-svc-3").namespace("namespace1").putLabelsItem("spring", "true")
.putLabelsItem("k8s", "true"))
.spec(new V1ServiceSpec().loadBalancerIP("1.1.1.1")).status(new V1ServiceStatus());
private static final V1Endpoints testEndpoints3 = new V1Endpoints()
.metadata(new V1ObjectMeta().name("test-svc-3").namespace("namespace1"))
.addSubsetsItem(new V1EndpointSubset().addPortsItem(new V1EndpointPort().port(8080).name("http"))
.addAddressesItem(new V1EndpointAddress().ip("2.2.2.2").targetRef(new V1ObjectReferenceBuilder().withUid("uid2").build())));
private static WireMockServer wireMockServer;
@Autowired
WebTestClient webTestClient;
@Test
void apps(){
Map<String, String> kubernetesServiceInstance1Metadata = new HashMap<>();
kubernetesServiceInstance1Metadata.put(testEndpoints1.getSubsets().get(0).getPorts().get(0).getName(), testEndpoints1.getSubsets().get(0).getPorts().get(0).getPort().toString());
Map<String, String> kubernetesServiceInstance2Metadata = new HashMap<>();
kubernetesServiceInstance2Metadata.put(testEndpoints3.getSubsets().get(0).getPorts().get(0).getName(), testEndpoints3.getSubsets().get(0).getPorts().get(0).getPort().toString());
kubernetesServiceInstance2Metadata.putAll(testService3.getMetadata().getLabels());
KubernetesServiceInstance kubernetesServiceInstance1 = new KubernetesServiceInstance(testEndpoints1.getSubsets().get(0).getAddresses().get(0).getTargetRef().getUid(), testService1.getMetadata().getName(), testEndpoints1.getSubsets().get(0).getAddresses().get(0).getIp(), testEndpoints1.getSubsets().get(0).getPorts().get(0).getPort(), kubernetesServiceInstance1Metadata, false, testService1.getMetadata().getNamespace(), null);
KubernetesServiceInstance kubernetesServiceInstance3 = new KubernetesServiceInstance(testEndpoints3.getSubsets().get(0).getAddresses().get(0).getTargetRef().getUid(), testService3.getMetadata().getName(), testEndpoints3.getSubsets().get(0).getAddresses().get(0).getIp(), testEndpoints3.getSubsets().get(0).getPorts().get(0).getPort(), kubernetesServiceInstance2Metadata, false, testService3.getMetadata().getNamespace(), null);
webTestClient.get().uri("/apps").exchange().expectBodyList(KubernetesService.class).hasSize(2).contains(new KubernetesService(testService1.getMetadata().getName(),
Collections.singletonList(kubernetesServiceInstance1)), new KubernetesService(testService3.getMetadata().getName(), Collections.singletonList(kubernetesServiceInstance3)));
}
@Test
void appsName() {
Map<String, String> kubernetesServiceInstance2Metadata = new HashMap<>();
kubernetesServiceInstance2Metadata.put(testEndpoints3.getSubsets().get(0).getPorts().get(0).getName(), testEndpoints3.getSubsets().get(0).getPorts().get(0).getPort().toString());
kubernetesServiceInstance2Metadata.putAll(testService3.getMetadata().getLabels());
KubernetesServiceInstance kubernetesServiceInstance3 = new KubernetesServiceInstance(testEndpoints3.getSubsets().get(0).getAddresses().get(0).getTargetRef().getUid(), testService3.getMetadata().getName(), testEndpoints3.getSubsets().get(0).getAddresses().get(0).getIp(), testEndpoints3.getSubsets().get(0).getPorts().get(0).getPort(), kubernetesServiceInstance2Metadata, false, testService3.getMetadata().getNamespace(), null);
webTestClient.get().uri("/apps/test-svc-3").exchange().expectBodyList(KubernetesServiceInstance.class).hasSize(1).contains(kubernetesServiceInstance3);
}
@Test
void instance() {
Map<String, String> kubernetesServiceInstance2Metadata = new HashMap<>();
kubernetesServiceInstance2Metadata.put(testEndpoints3.getSubsets().get(0).getPorts().get(0).getName(), testEndpoints3.getSubsets().get(0).getPorts().get(0).getPort().toString());
kubernetesServiceInstance2Metadata.putAll(testService3.getMetadata().getLabels());
KubernetesServiceInstance kubernetesServiceInstance3 = new KubernetesServiceInstance(testEndpoints3.getSubsets().get(0).getAddresses().get(0).getTargetRef().getUid(), testService3.getMetadata().getName(), testEndpoints3.getSubsets().get(0).getAddresses().get(0).getIp(), testEndpoints3.getSubsets().get(0).getPorts().get(0).getPort(), kubernetesServiceInstance2Metadata, false, testService3.getMetadata().getNamespace(), null);
webTestClient.get().uri("/app/test-svc-3/uid2").exchange().expectBody(KubernetesServiceInstance.class).isEqualTo(kubernetesServiceInstance3);
}
@SpringBootApplication
protected static class TestConfig {
@Bean
public KubernetesNamespaceProvider kubernetesNamespaceProvider() {
KubernetesNamespaceProvider provider = mock(KubernetesNamespaceProvider.class);
when(provider.getNamespace()).thenReturn("namespace1");
return provider;
}
@Bean
public ApiClient apiClient() {
wireMockServer = new WireMockServer(options().dynamicPort());
wireMockServer.start();
WireMock.configureFor(wireMockServer.port());
stubFor(get("/api/v1/namespaces/namespace1/endpoints?resourceVersion=0&watch=false")
.willReturn(aResponse().withStatus(200).withBody(new JSON().serialize(new V1EndpointsListBuilder()
.withMetadata(new V1ListMetaBuilder().withNewResourceVersion("0").build()).addToItems(testEndpoints1, testEndpoints3).build()))));
stubFor(get("/api/v1/namespaces/namespace1/services?resourceVersion=0&watch=false")
.willReturn(aResponse().withStatus(200).withBody(new JSON().serialize(new V1ServiceListBuilder()
.withMetadata(new V1ListMetaBuilder().withNewResourceVersion("0").build()).addToItems(testService1, testService2, testService3).build()))));
stubFor(get("/api/v1/namespaces/namespace1/endpoints?watch=true")
.willReturn(aResponse().withStatus(200)));
stubFor(get("/api/v1/namespaces/namespace1/services?watch=true")
.willReturn(aResponse().withStatus(200)));
ApiClient apiClient = new ClientBuilder().setBasePath(wireMockServer.baseUrl()).build();
return apiClient;
}
}
public static class KubernetesService {
private String name;
private List<KubernetesServiceInstance> serviceInstances;
public KubernetesService() { }
public KubernetesService(String name, List<KubernetesServiceInstance> serviceInstances) {
this.name = name;
this.serviceInstances = serviceInstances;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public List<KubernetesServiceInstance> getServiceInstances() {
return serviceInstances;
}
public void setServiceInstances(List<KubernetesServiceInstance> serviceInstances) {
this.serviceInstances = serviceInstances;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
KubernetesService service = (KubernetesService) o;
return Objects.equals(getName(), service.getName()) && Objects.equals(getServiceInstances(), service.getServiceInstances());
}
@Override
public int hashCode() {
return Objects.hash(getName(), getServiceInstances());
}
@Override
public String toString() {
return "KubernetesService{" +
"name='" + name + '\'' +
", serviceInstances=" + serviceInstances +
'}';
}
}
}

View File

@@ -140,6 +140,12 @@
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-kubernetes-discovery</artifactId>
<version>${project.version}</version>
</dependency>
<!-- Own dependencies - Starters -->
<dependency>
<groupId>org.springframework.cloud</groupId>
@@ -189,6 +195,12 @@
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-kubernetes-discoveryclient</artifactId>
<version>${project.version}</version>
</dependency>
<!-- Testing Dependencies -->
<dependency>
<groupId>org.jboss.arquillian.junit</groupId>

View File

@@ -0,0 +1,59 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>spring-cloud-kubernetes</artifactId>
<groupId>org.springframework.cloud</groupId>
<version>2.1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>spring-cloud-kubernetes-discovery</artifactId>
<properties>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-commons</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-actuator</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.github.tomakehurst</groupId>
<artifactId>wiremock-jre8</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,28 @@
/*
* Copyright 2013-2021 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.discovery;
/**
* @author Ryan Baxter
*/
public class DiscoveryServerUrlInvalidException extends RuntimeException {
public DiscoveryServerUrlInvalidException() {
super("spring.cloud.kubernetes.discovery-server-url must be specified and a valid URL.");
}
}

View File

@@ -0,0 +1,72 @@
/*
* Copyright 2013-2021 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.discovery;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.util.StringUtils;
import org.springframework.web.client.RestTemplate;
/**
* @author Ryan Baxter
*/
public class KubernetesDiscoveryClient implements DiscoveryClient {
private RestTemplate rest;
private KubernetesDiscoveryClientProperties properties;
public KubernetesDiscoveryClient(RestTemplate rest, KubernetesDiscoveryClientProperties properties) {
if (!StringUtils.hasText(properties.getDiscoveryServerUrl())) {
throw new DiscoveryServerUrlInvalidException();
}
this.rest = rest;
this.properties = properties;
}
@Override
public String description() {
return "Kubernetes Discovery Client";
}
@Override
public List<ServiceInstance> getInstances(String serviceId) {
List<ServiceInstance> response = Collections.emptyList();
KubernetesServiceInstance[] responseBody = rest.getForEntity(
properties.getDiscoveryServerUrl() + "/apps/" + serviceId, KubernetesServiceInstance[].class).getBody();
if (responseBody != null && responseBody.length > 0) {
response = Arrays.asList(responseBody);
}
return response;
}
@Override
public List<String> getServices() {
List<String> response = Collections.emptyList();
Service[] services = rest.getForEntity(properties.getDiscoveryServerUrl() + "/apps", Service[].class).getBody();
if (services != null && services.length > 0) {
response = Arrays.stream(services).map(service -> service.getName()).collect(Collectors.toList());
}
return response;
}
}

View File

@@ -0,0 +1,114 @@
/*
* Copyright 2013-2021 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.discovery;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.boot.actuate.health.HealthIndicator;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.cloud.client.ConditionalOnDiscoveryEnabled;
import org.springframework.cloud.client.ConditionalOnDiscoveryHealthIndicatorEnabled;
import org.springframework.cloud.client.ConditionalOnReactiveDiscoveryEnabled;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.cloud.client.discovery.ReactiveDiscoveryClient;
import org.springframework.cloud.client.discovery.event.InstanceRegisteredEvent;
import org.springframework.cloud.client.discovery.health.DiscoveryClientHealthIndicatorProperties;
import org.springframework.cloud.client.discovery.health.reactive.ReactiveDiscoveryClientHealthIndicator;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.reactive.function.client.WebClient;
/**
* @author Ryan Baxter
*/
@Configuration(proxyBeanMethods = false)
@ConditionalOnDiscoveryEnabled
@ConditionalOnProperty(value = { "spring.cloud.kubernetes.enabled", "spring.cloud.kubernetes.discovery.enabled" },
matchIfMissing = true)
@EnableConfigurationProperties({ DiscoveryClientHealthIndicatorProperties.class,
KubernetesDiscoveryClientProperties.class })
public class KubernetesDiscoveryClientAutoConfiguration {
@Configuration(proxyBeanMethods = false)
public static class Servlet {
@Bean
@ConditionalOnMissingClass("org.springframework.web.reactive.function.client.WebClient")
public RestTemplate restTemplate() {
return new RestTemplateBuilder().build();
}
@Bean
@ConditionalOnMissingClass("org.springframework.web.reactive.function.client.WebClient")
public DiscoveryClient kubernetesDiscoveryClient(RestTemplate restTemplate,
KubernetesDiscoveryClientProperties properties) {
return new KubernetesDiscoveryClient(restTemplate, properties);
}
@Bean
@ConditionalOnClass({ HealthIndicator.class })
@ConditionalOnDiscoveryHealthIndicatorEnabled
public InitializingBean indicatorInitializer(ApplicationEventPublisher applicationEventPublisher,
ApplicationContext applicationContext) {
return () -> applicationEventPublisher
.publishEvent(new InstanceRegisteredEvent<>(applicationContext.getId(), null));
}
}
@Configuration(proxyBeanMethods = false)
@ConditionalOnReactiveDiscoveryEnabled
public static class Reactive {
@Bean
@ConditionalOnClass(name = { "org.springframework.web.reactive.function.client.WebClient" })
@ConditionalOnMissingBean(WebClient.Builder.class)
public WebClient.Builder webClientBuilder() {
return WebClient.builder();
}
@Bean
@ConditionalOnClass(name = { "org.springframework.web.reactive.function.client.WebClient" })
public ReactiveDiscoveryClient kubernetesReactiveDiscoveryClient(WebClient.Builder webClientBuilder,
KubernetesDiscoveryClientProperties properties) {
return new KubernetesReactiveDiscoveryClient(webClientBuilder, properties);
}
@Bean
@ConditionalOnClass(name = "org.springframework.boot.actuate.health.ReactiveHealthIndicator")
@ConditionalOnDiscoveryHealthIndicatorEnabled
public ReactiveDiscoveryClientHealthIndicator kubernetesReactiveDiscoveryClientHealthIndicator(
KubernetesReactiveDiscoveryClient client, DiscoveryClientHealthIndicatorProperties properties,
ApplicationContext applicationContext) {
ReactiveDiscoveryClientHealthIndicator healthIndicator = new ReactiveDiscoveryClientHealthIndicator(client,
properties);
InstanceRegisteredEvent event = new InstanceRegisteredEvent(applicationContext.getId(), null);
healthIndicator.onApplicationEvent(event);
return healthIndicator;
}
}
}

View File

@@ -0,0 +1,47 @@
/*
* Copyright 2013-2021 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.discovery;
import org.springframework.boot.context.properties.ConfigurationProperties;
/**
* @author Ryan Baxter
*/
@ConfigurationProperties("spring.cloud.kubernetes.discovery")
public class KubernetesDiscoveryClientProperties {
private String discoveryServerUrl;
private boolean enabled = true;
public String getDiscoveryServerUrl() {
return discoveryServerUrl;
}
public void setDiscoveryServerUrl(String discoveryServerUrl) {
this.discoveryServerUrl = discoveryServerUrl;
}
public boolean isEnabled() {
return enabled;
}
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
}

View File

@@ -0,0 +1,61 @@
/*
* Copyright 2013-2021 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.discovery;
import reactor.core.publisher.Flux;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.discovery.ReactiveDiscoveryClient;
import org.springframework.util.StringUtils;
import org.springframework.web.reactive.function.client.WebClient;
/**
* @author Ryan Baxter
*/
public class KubernetesReactiveDiscoveryClient implements ReactiveDiscoveryClient {
private WebClient webClient;
public KubernetesReactiveDiscoveryClient(WebClient.Builder webClientBuilder,
KubernetesDiscoveryClientProperties properties) {
if (!StringUtils.hasText(properties.getDiscoveryServerUrl())) {
throw new DiscoveryServerUrlInvalidException();
}
this.webClient = webClientBuilder.baseUrl(properties.getDiscoveryServerUrl()).build();
}
@Override
public String description() {
return "Reactive Kubernetes Discovery Client";
}
@Override
@Cacheable("serviceinstances")
public Flux<ServiceInstance> getInstances(String serviceId) {
return webClient.get().uri("/apps/" + serviceId)
.exchangeToFlux(clientResponse -> clientResponse.bodyToFlux(KubernetesServiceInstance.class));
}
@Override
@Cacheable("services")
public Flux<String> getServices() {
return webClient.get().uri("/apps").exchangeToFlux(
clientResponse -> clientResponse.bodyToFlux(Service.class).map(service -> service.getName()));
}
}

View File

@@ -0,0 +1,166 @@
/*
* Copyright 2013-2021 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.discovery;
import java.net.URI;
import java.util.Map;
import java.util.Objects;
import org.springframework.cloud.client.ServiceInstance;
/**
* @author Ryan Baxter
*/
public class KubernetesServiceInstance implements ServiceInstance {
private String instanceId;
private String serviceId;
private String host;
private int port;
private boolean secure;
private URI uri;
private Map<String, String> metadata;
private String scheme;
private String namespace;
public KubernetesServiceInstance() {
}
public KubernetesServiceInstance(String instanceId, String serviceId, String host, int port, boolean secure,
URI uri, Map<String, String> metadata, String scheme, String namespace) {
this.instanceId = instanceId;
this.serviceId = serviceId;
this.host = host;
this.port = port;
this.secure = secure;
this.uri = uri;
this.metadata = metadata;
this.scheme = scheme;
this.namespace = namespace;
}
@Override
public String getInstanceId() {
return instanceId;
}
@Override
public String getServiceId() {
return serviceId;
}
@Override
public String getHost() {
return host;
}
@Override
public int getPort() {
return port;
}
@Override
public boolean isSecure() {
return secure;
}
@Override
public URI getUri() {
return uri;
}
@Override
public Map<String, String> getMetadata() {
return metadata;
}
public void setInstanceId(String instanceId) {
this.instanceId = instanceId;
}
public void setServiceId(String serviceId) {
this.serviceId = serviceId;
}
public void setHost(String host) {
this.host = host;
}
public void setPort(int port) {
this.port = port;
}
public void setSecure(boolean secure) {
this.secure = secure;
}
public void setUri(URI uri) {
this.uri = uri;
}
public void setMetadata(Map<String, String> metadata) {
this.metadata = metadata;
}
public void setScheme(String scheme) {
this.scheme = scheme;
}
public String getNamespace() {
return namespace;
}
public void setNamespace(String namespace) {
this.namespace = namespace;
}
@Override
public String getScheme() {
return scheme;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
KubernetesServiceInstance that = (KubernetesServiceInstance) o;
return getPort() == that.getPort() && isSecure() == that.isSecure()
&& Objects.equals(getInstanceId(), that.getInstanceId())
&& Objects.equals(getServiceId(), that.getServiceId()) && Objects.equals(getHost(), that.getHost())
&& Objects.equals(getUri(), that.getUri()) && Objects.equals(getMetadata(), that.getMetadata())
&& Objects.equals(getScheme(), that.getScheme()) && Objects.equals(getNamespace(), that.getNamespace());
}
@Override
public int hashCode() {
return Objects.hash(getInstanceId(), getServiceId(), getHost(), getPort(), isSecure(), getUri(), getMetadata(),
getScheme(), getNamespace());
}
}

View File

@@ -0,0 +1,49 @@
/*
* Copyright 2013-2021 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.discovery;
import java.util.List;
/**
* @author Ryan Baxter
*/
public class Service {
private String name;
private List<KubernetesServiceInstance> serviceInstances;
public Service() {
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public List<KubernetesServiceInstance> getServiceInstances() {
return serviceInstances;
}
public void setServiceInstances(List<KubernetesServiceInstance> serviceInstances) {
this.serviceInstances = serviceInstances;
}
}

View File

@@ -0,0 +1,2 @@
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.cloud.kubernetes.discovery.KubernetesDiscoveryClientAutoConfiguration

View File

@@ -0,0 +1,113 @@
/*
* Copyright 2013-2021 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.discovery;
import org.junit.jupiter.api.Test;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.test.context.FilteredClassLoader;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.cloud.client.ReactiveCommonsClientAutoConfiguration;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.cloud.client.discovery.ReactiveDiscoveryClient;
import org.springframework.cloud.client.discovery.health.reactive.ReactiveDiscoveryClientHealthIndicator;
import org.springframework.cloud.commons.util.UtilAutoConfiguration;
import static org.assertj.core.api.Assertions.assertThat;
/**
* @author Ryan Baxter
*/
class KubernetesDiscoveryClientAutoConfigurationTests {
private ApplicationContextRunner contextRunner = new ApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(UtilAutoConfiguration.class,
ReactiveCommonsClientAutoConfiguration.class, KubernetesDiscoveryClientAutoConfiguration.class));
@Test
public void shouldWorkWithDefaults() {
contextRunner
.withPropertyValues("spring.cloud.kubernetes.discovery.discovery-server-url=http://k8sdiscoveryserver")
.withClassLoader(new FilteredClassLoader("org.springframework.web.reactive")).run(context -> {
assertThat(context).hasSingleBean(DiscoveryClient.class);
assertThat(context).doesNotHaveBean(ReactiveDiscoveryClient.class);
assertThat(context).doesNotHaveBean(ReactiveDiscoveryClientHealthIndicator.class);
});
}
@Test
public void shouldNotHaveDiscoveryClientWhenDiscoveryDisabled() {
contextRunner
.withPropertyValues("spring.cloud.discovery.enabled=false",
"spring.cloud.kubernetes.discovery.discovery-server-url=http://k8sdiscoveryserver")
.run(context -> {
assertThat(context).doesNotHaveBean(ReactiveDiscoveryClient.class);
assertThat(context).doesNotHaveBean(DiscoveryClient.class);
assertThat(context).doesNotHaveBean(ReactiveDiscoveryClientHealthIndicator.class);
});
}
@Test
public void shouldNotHaveDiscoveryClientWhenKubernetesDiscoveryDisabled() {
contextRunner
.withPropertyValues("spring.cloud.kubernetes.discovery.enabled=false",
"spring.cloud.kubernetes.discovery.discovery-server-url=http://k8sdiscoveryserver")
.run(context -> {
assertThat(context).doesNotHaveBean(ReactiveDiscoveryClient.class);
assertThat(context).doesNotHaveBean(DiscoveryClient.class);
assertThat(context).doesNotHaveBean(ReactiveDiscoveryClientHealthIndicator.class);
});
}
@Test
public void shouldHaveReactiveDiscoveryClient() {
contextRunner
.withPropertyValues("spring.cloud.kubernetes.discovery.discovery-server-url=http://k8sdiscoveryserver")
.run(context -> {
assertThat(context).hasSingleBean(ReactiveDiscoveryClient.class);
assertThat(context).doesNotHaveBean(DiscoveryClient.class);
assertThat(context).hasSingleBean(ReactiveDiscoveryClientHealthIndicator.class);
});
}
@Test
public void shouldNotHaveDiscoveryClientWhenReactiveDiscoveryDisabled() {
contextRunner.withPropertyValues("spring.cloud.discovery.reactive.enabled=false").run(context -> {
assertThat(context).doesNotHaveBean(ReactiveDiscoveryClient.class);
assertThat(context).doesNotHaveBean(ReactiveDiscoveryClientHealthIndicator.class);
});
}
@Test
public void shouldNotHaveDiscoveryClientWhenKubernetesDisabled() {
contextRunner.withPropertyValues("spring.cloud.kubernetes.enabled=false").run(context -> {
assertThat(context).doesNotHaveBean(ReactiveDiscoveryClient.class);
assertThat(context).doesNotHaveBean(ReactiveDiscoveryClientHealthIndicator.class);
});
}
@Test
public void worksWithoutActuator() {
contextRunner
.withPropertyValues("spring.cloud.kubernetes.discovery.discovery-server-url=http://k8sdiscoveryserver")
.withClassLoader(new FilteredClassLoader("org.springframework.boot.actuate")).run(context -> {
assertThat(context).hasSingleBean(ReactiveDiscoveryClient.class);
assertThat(context).doesNotHaveBean(ReactiveDiscoveryClientHealthIndicator.class);
});
}
}

View File

@@ -0,0 +1,86 @@
/*
* Copyright 2013-2021 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.discovery;
import java.net.URI;
import java.util.HashMap;
import java.util.Map;
import com.github.tomakehurst.wiremock.WireMockServer;
import com.github.tomakehurst.wiremock.client.WireMock;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.web.client.RestTemplate;
import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
import static com.github.tomakehurst.wiremock.client.WireMock.get;
import static com.github.tomakehurst.wiremock.client.WireMock.stubFor;
import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.options;
import static org.assertj.core.api.Assertions.assertThat;
/**
* @author Ryan Baxter
*/
class KubernetesDiscoveryClientTests {
private static final String APPS = "[{\"name\":\"test-svc-1\",\"serviceInstances\":[{\"instanceId\":\"uid1\",\"serviceId\":\"test-svc-1\",\"host\":\"2.2.2.2\",\"port\":8080,\"uri\":\"http://2.2.2.2:8080\",\"secure\":false,\"metadata\":{\"http\":\"8080\"},\"namespace\":\"namespace1\",\"cluster\":null,\"scheme\":\"http\"}]},{\"name\":\"test-svc-3\",\"serviceInstances\":[{\"instanceId\":\"uid2\",\"serviceId\":\"test-svc-3\",\"host\":\"2.2.2.2\",\"port\":8080,\"uri\":\"http://2.2.2.2:8080\",\"secure\":false,\"metadata\":{\"spring\":\"true\",\"http\":\"8080\",\"k8s\":\"true\"},\"namespace\":\"namespace1\",\"cluster\":null,\"scheme\":\"http\"}]}]";
private static final String APPS_NAME = "[{\"instanceId\":\"uid2\",\"serviceId\":\"test-svc-3\",\"host\":\"2.2.2.2\",\"port\":8080,\"uri\":\"http://2.2.2.2:8080\",\"secure\":false,\"metadata\":{\"spring\":\"true\",\"http\":\"8080\",\"k8s\":\"true\"},\"namespace\":\"namespace1\",\"cluster\":null,\"scheme\":\"http\"}]";
private static WireMockServer wireMockServer;
@BeforeAll
static void beforeAll() {
wireMockServer = new WireMockServer(options().dynamicPort());
wireMockServer.start();
WireMock.configureFor(wireMockServer.port());
stubFor(get("/apps")
.willReturn(aResponse().withStatus(200).withBody(APPS).withHeader("content-type", "application/json")));
stubFor(get("/apps/test-svc-3").willReturn(
aResponse().withStatus(200).withBody(APPS_NAME).withHeader("content-type", "application/json")));
stubFor(get("/apps/does-not-exist")
.willReturn(aResponse().withStatus(200).withBody("").withHeader("content-type", "application/json")));
}
@Test
void getInstances() {
RestTemplate rest = new RestTemplateBuilder().build();
KubernetesDiscoveryClientProperties properties = new KubernetesDiscoveryClientProperties();
properties.setDiscoveryServerUrl(wireMockServer.baseUrl());
KubernetesDiscoveryClient discoveryClient = new KubernetesDiscoveryClient(rest, properties);
assertThat(discoveryClient.getServices()).contains("test-svc-1", "test-svc-3");
}
@Test
void getServices() {
RestTemplate rest = new RestTemplateBuilder().build();
KubernetesDiscoveryClientProperties properties = new KubernetesDiscoveryClientProperties();
properties.setDiscoveryServerUrl(wireMockServer.baseUrl());
KubernetesDiscoveryClient discoveryClient = new KubernetesDiscoveryClient(rest, properties);
Map<String, String> metadata = new HashMap<>();
metadata.put("spring", "true");
metadata.put("http", "8080");
metadata.put("k8s", "true");
assertThat(discoveryClient.getInstances("test-svc-3"))
.contains(new KubernetesServiceInstance("uid2", "test-svc-3", "2.2.2.2", 8080, false,
URI.create("http://2.2.2.2:8080"), metadata, "http", "namespace1"));
assertThat(discoveryClient.getInstances("does-not-exist")).isEmpty();
}
}

View File

@@ -0,0 +1,86 @@
/*
* Copyright 2013-2021 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.discovery;
import java.net.URI;
import java.util.HashMap;
import java.util.Map;
import com.github.tomakehurst.wiremock.WireMockServer;
import com.github.tomakehurst.wiremock.client.WireMock;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import reactor.test.StepVerifier;
import org.springframework.web.reactive.function.client.WebClient;
import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
import static com.github.tomakehurst.wiremock.client.WireMock.get;
import static com.github.tomakehurst.wiremock.client.WireMock.stubFor;
import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.options;
/**
* @author Ryan Baxter
*/
class KubernetesReactiveDiscoveryClientTests {
private static final String APPS = "[{\"name\":\"test-svc-1\",\"serviceInstances\":[{\"instanceId\":\"uid1\",\"serviceId\":\"test-svc-1\",\"host\":\"2.2.2.2\",\"port\":8080,\"uri\":\"http://2.2.2.2:8080\",\"secure\":false,\"metadata\":{\"http\":\"8080\"},\"namespace\":\"namespace1\",\"cluster\":null,\"scheme\":\"http\"}]},{\"name\":\"test-svc-3\",\"serviceInstances\":[{\"instanceId\":\"uid2\",\"serviceId\":\"test-svc-3\",\"host\":\"2.2.2.2\",\"port\":8080,\"uri\":\"http://2.2.2.2:8080\",\"secure\":false,\"metadata\":{\"spring\":\"true\",\"http\":\"8080\",\"k8s\":\"true\"},\"namespace\":\"namespace1\",\"cluster\":null,\"scheme\":\"http\"}]}]";
private static final String APPS_NAME = "[{\"instanceId\":\"uid2\",\"serviceId\":\"test-svc-3\",\"host\":\"2.2.2.2\",\"port\":8080,\"uri\":\"http://2.2.2.2:8080\",\"secure\":false,\"metadata\":{\"spring\":\"true\",\"http\":\"8080\",\"k8s\":\"true\"},\"namespace\":\"namespace1\",\"cluster\":null,\"scheme\":\"http\"}]";
private static WireMockServer wireMockServer;
@BeforeAll
static void beforeAll() {
wireMockServer = new WireMockServer(options().dynamicPort());
wireMockServer.start();
WireMock.configureFor(wireMockServer.port());
stubFor(get("/apps")
.willReturn(aResponse().withStatus(200).withBody(APPS).withHeader("content-type", "application/json")));
stubFor(get("/apps/test-svc-3").willReturn(
aResponse().withStatus(200).withBody(APPS_NAME).withHeader("content-type", "application/json")));
stubFor(get("/apps/does-not-exist")
.willReturn(aResponse().withStatus(200).withBody("").withHeader("content-type", "application/json")));
}
@Test
void getInstances() {
KubernetesDiscoveryClientProperties properties = new KubernetesDiscoveryClientProperties();
properties.setDiscoveryServerUrl(wireMockServer.baseUrl());
KubernetesReactiveDiscoveryClient discoveryClient = new KubernetesReactiveDiscoveryClient(WebClient.builder(),
properties);
StepVerifier.create(discoveryClient.getServices()).expectNext("test-svc-1", "test-svc-3").verifyComplete();
}
@Test
void getServices() {
KubernetesDiscoveryClientProperties properties = new KubernetesDiscoveryClientProperties();
properties.setDiscoveryServerUrl(wireMockServer.baseUrl());
KubernetesReactiveDiscoveryClient discoveryClient = new KubernetesReactiveDiscoveryClient(WebClient.builder(),
properties);
Map<String, String> metadata = new HashMap<>();
metadata.put("spring", "true");
metadata.put("http", "8080");
metadata.put("k8s", "true");
StepVerifier.create(discoveryClient.getInstances("test-svc-3"))
.expectNext(new KubernetesServiceInstance("uid2", "test-svc-3", "2.2.2.2", 8080, false,
URI.create("http://2.2.2.2:8080"), metadata, "http", "namespace1"))
.verifyComplete();
StepVerifier.create(discoveryClient.getInstances("test-svc-3")).expectNextCount(0);
}
}

View File

@@ -30,6 +30,8 @@ ALL_INTEGRATION_PROJECTS=(
"spring-cloud-kubernetes-configuration-watcher-it"
"spring-cloud-kubernetes-client-loadbalancer-it"
"spring-cloud-kubernetes-client-reactive-discovery-client-it"
"spring-cloud-kubernetes-discoverclient-it"
"spring-cloud-kubernetes-reactive-discoveryclient-it"
)
INTEGRATION_PROJECTS=(${INTEGRATION_PROJECTS:-${ALL_INTEGRATION_PROJECTS[@]}})
@@ -42,7 +44,8 @@ DEFAULT_PULLING_IMAGES=(
)
PULLING_IMAGES=(${PULLING_IMAGES:-${DEFAULT_PULLING_IMAGES[@]}})
LOADING_IMAGES=(${LOADING_IMAGES:-${DEFAULT_PULLING_IMAGES[@]}} "docker.io/springcloud/spring-cloud-kubernetes-configuration-watcher:${MVN_VERSION}")
LOADING_IMAGES=(${LOADING_IMAGES:-${DEFAULT_PULLING_IMAGES[@]}} "docker.io/springcloud/spring-cloud-kubernetes-configuration-watcher:${MVN_VERSION}"
"docker.io/springcloud/spring-cloud-kubernetes-discoveryserver:${MVN_VERSION}")
# cleanup on exit (useful for running locally)
cleanup() {
"${KIND}" delete cluster || true

View File

@@ -0,0 +1,26 @@
apiVersion: apps/v1
kind: Deployment
metadata:
creationTimestamp: null
labels:
app: spring-cloud-kubernetes-discoveryclient-it
name: spring-cloud-kubernetes-discoveryclient-it-deployment
spec:
replicas: 1
selector:
matchLabels:
app: spring-cloud-kubernetes-discoveryclient-it
strategy: {}
template:
metadata:
creationTimestamp: null
labels:
app: spring-cloud-kubernetes-discoveryclient-it
spec:
serviceAccountName: spring-cloud-kubernetes-serviceaccount
containers:
- image: springcloud/spring-cloud-kubernetes-discoveryclient-it:2.1.0-SNAPSHOT
imagePullPolicy: IfNotPresent
name: spring-cloud-kubernetes-discoveryclient-it
resources: {}
status: {}

View File

@@ -0,0 +1,18 @@
apiVersion: v1
kind: Service
metadata:
creationTimestamp: null
labels:
app: spring-cloud-kubernetes-discoveryclient-it
name: spring-cloud-kubernetes-discoveryclient-it
spec:
ports:
- name: 80-8080
port: 80
protocol: TCP
targetPort: 8080
selector:
app: spring-cloud-kubernetes-discoveryclient-it
type: ClusterIP
status:
loadBalancer: {}

View File

@@ -0,0 +1,143 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>spring-cloud-kubernetes-integration-tests</artifactId>
<groupId>org.springframework.cloud</groupId>
<version>2.1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>spring-cloud-kubernetes-discoverclient-it</artifactId>
<properties>
<jib.version>1.8.0</jib.version>
<base.image>openjdk:8u222-slim</base.image>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-kubernetes-discoveryclient</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-kubernetes-test-support</artifactId>
</dependency>
<dependency>
<groupId>io.kubernetes</groupId>
<artifactId>client-java</artifactId>
</dependency>
<dependency>
<groupId>io.kubernetes</groupId>
<artifactId>client-java-extended</artifactId>
</dependency>
<dependency>
<groupId>com.github.docker-java</groupId>
<artifactId>docker-java-core</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.github.docker-java</groupId>
<artifactId>docker-java-transport-httpclient5</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<resources>
<resource>
<directory>../src/main/resources</directory>
<filtering>true</filtering>
</resource>
<resource>
<directory>src/main/resources</directory>
<filtering>true</filtering>
</resource>
</resources>
</build>
<profiles>
<profile>
<id>skaffold</id>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<image>
<name>${env.IMAGE}</name>
</image>
<goal>build-image</goal>
</configuration>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>build-image</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</profile>
<profile>
<id>imagename</id>
<activation>
<property>
<name>!env.IMAGE</name>
</property>
</activation>
<properties>
<env.IMAGE>springcloud/${project.artifactId}:${project.version}</env.IMAGE>
</properties>
</profile>
<profile>
<id>jib</id>
<build>
<plugins>
<plugin>
<groupId>com.google.cloud.tools</groupId>
<artifactId>jib-maven-plugin</artifactId>
<version>${jib.version}</version>
<configuration>
<from>
<image>${base.image}</image>
</from>
<to>
<image>spring-cloud/${project.artifactId}</image>
</to>
<container>
<user>nobody:nogroup</user>
<environment>
</environment>
</container>
</configuration>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>dockerBuild</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</profile>
</profiles>
</project>

View File

@@ -0,0 +1,22 @@
apiVersion: skaffold/v2alpha3
kind: Config
metadata:
name: spring-cloud-kubernetes-discoveryclient-it
build:
artifacts:
- image: springcloud/spring-cloud-kubernetes-discoveryclient-it
jib: {
args: [ "-Pjib" ]
}
# custom:
# buildCommand: "../../mvnw clean install -Pskaffold"
# dependencies:
# paths:
# - src
# - pom.xml
deploy:
kubectl:
manifests:
- k8s/deployment-it.yaml
- k8s/service-it.yaml
- ../permissions.yaml

View File

@@ -0,0 +1,54 @@
/*
* Copyright 2013-2021 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.discoveryclient.it;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
/**
* @author Ryan Baxter
*/
@SpringBootApplication
@RestController
public class KubernetesDiscoveryClientApplicationIt {
@Autowired
DiscoveryClient discoveryClient;
public static void main(String[] args) {
SpringApplication.run(KubernetesDiscoveryClientApplicationIt.class, args);
}
@GetMapping("/services")
public List<String> services() {
return discoveryClient.getServices();
}
@GetMapping("/service/{serviceId}")
public List<ServiceInstance> service(@PathVariable String serviceId) {
return discoveryClient.getInstances(serviceId);
}
}

View File

@@ -0,0 +1,13 @@
spring:
cloud:
kubernetes:
discovery:
discoveryServerUrl: http://spring-cloud-kubernetes-discoveryserver
management:
endpoint:
health:
show-details: always
endpoints:
web:
exposure:
include: "*"

View File

@@ -0,0 +1,237 @@
/*
* Copyright 2013-2021 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.discoveryclient.it;
import java.io.IOException;
import java.time.Duration;
import java.util.Arrays;
import java.util.Map;
import io.kubernetes.client.openapi.ApiClient;
import io.kubernetes.client.openapi.ApiException;
import io.kubernetes.client.openapi.apis.AppsV1Api;
import io.kubernetes.client.openapi.apis.CoreV1Api;
import io.kubernetes.client.openapi.apis.NetworkingV1Api;
import io.kubernetes.client.openapi.models.V1Deployment;
import io.kubernetes.client.openapi.models.V1Ingress;
import io.kubernetes.client.openapi.models.V1Service;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.cloud.kubernetes.integration.tests.commons.K8SUtils;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.web.client.ResponseErrorHandler;
import org.springframework.web.client.RestTemplate;
import static org.assertj.core.api.Assertions.assertThat;
import static org.awaitility.Awaitility.await;
import static org.springframework.cloud.kubernetes.integration.tests.commons.K8SUtils.createApiClient;
import static org.springframework.cloud.kubernetes.integration.tests.commons.K8SUtils.getPomVersion;
/**
* @author Ryan Baxter
*/
public class DiscoveryClientIT {
private static final Log LOG = LogFactory.getLog(DiscoveryClientIT.class);
private static final String DISCOVERYSERVER_DEPLOYMENT_NAME = "spring-cloud-kubernetes-discoveryserver-deployment";
private static final String DISCOVERYSERVER_APP_NAME = "spring-cloud-kubernetes-discoveryserver";
private static final String SPRING_CLOUD_K8S_DISCOVERYCLIENT_DEPLOYMENT_NAME = "spring-cloud-kubernetes-discoveryclient-it-deployment";
private static final String SPRING_CLOUD_K8S_DISCOVERYCLIENT_APP_NAME = "spring-cloud-kubernetes-discoveryclient-it";
private static final String NAMESPACE = "default";
private static ApiClient client;
private static CoreV1Api api;
private static AppsV1Api appsApi;
private static NetworkingV1Api networkingApi;
private static K8SUtils k8SUtils;
@BeforeAll
public static void setup() throws Exception {
client = createApiClient();
api = new CoreV1Api();
appsApi = new AppsV1Api();
networkingApi = new NetworkingV1Api();
k8SUtils = new K8SUtils(api, appsApi);
deployDiscoveryServer();
// Check to make sure the discovery server deployment is ready
k8SUtils.waitForDeployment(DISCOVERYSERVER_DEPLOYMENT_NAME, NAMESPACE);
// Check to see if endpoint is ready
k8SUtils.waitForEndpointReady(DISCOVERYSERVER_APP_NAME, NAMESPACE);
}
@Test
public void testDiscoveryClient() throws Exception {
try {
deployDiscoveryIt();
testLoadBalancer();
testHealth();
}
catch (Exception e) {
e.printStackTrace();
}
finally {
cleanup();
}
}
private void cleanup() throws ApiException {
appsApi.deleteCollectionNamespacedDeployment(NAMESPACE, null, null, null,
"metadata.name=" + SPRING_CLOUD_K8S_DISCOVERYCLIENT_DEPLOYMENT_NAME, null, null, null, null, null, null,
null, null, null);
api.deleteNamespacedService(SPRING_CLOUD_K8S_DISCOVERYCLIENT_APP_NAME, NAMESPACE, null, null, null, null, null,
null);
networkingApi.deleteNamespacedIngress("it-ingress", NAMESPACE, null, null, null, null, null, null);
}
private void testLoadBalancer() throws Exception {
// Check to make sure the controller deployment is ready
k8SUtils.waitForDeployment(SPRING_CLOUD_K8S_DISCOVERYCLIENT_DEPLOYMENT_NAME, NAMESPACE);
RestTemplate rest = createRestTemplate();
// Sometimes the NGINX ingress takes a bit to catch up and realize the service is
// available and we get a 503, we just need to wait a bit
await().timeout(Duration.ofSeconds(60))
.until(() -> rest.getForEntity("http://localhost:80/discoveryclient-it/services", String.class)
.getStatusCode().is2xxSuccessful());
String[] result = rest.getForObject("http://localhost:80/discoveryclient-it/services", String[].class);
LOG.info("Services: " + result);
assertThat(Arrays.stream(result)
.anyMatch(s -> "spring-cloud-kubernetes-discoveryserver".equalsIgnoreCase(s))).isTrue();
}
private RestTemplate createRestTemplate() {
RestTemplate rest = new RestTemplateBuilder().build();
rest.setErrorHandler(new ResponseErrorHandler() {
@Override
public boolean hasError(ClientHttpResponse clientHttpResponse) throws IOException {
LOG.warn("Received response status code: " + clientHttpResponse.getRawStatusCode());
if (clientHttpResponse.getRawStatusCode() == 503) {
return false;
}
return true;
}
@Override
public void handleError(ClientHttpResponse clientHttpResponse) throws IOException {
}
});
return rest;
}
public void testHealth() {
RestTemplate rest = createRestTemplate();
// Sometimes the NGINX ingress takes a bit to catch up and realize the service is
// available and we get a 503, we just need to wait a bit
await().timeout(Duration.ofSeconds(60))
.until(() -> rest.getForEntity("http://localhost:80/discoveryclient-it/actuator/health", String.class)
.getStatusCode().is2xxSuccessful());
Map<String, Object> health = rest.getForObject("http://localhost:80/discoveryclient-it/actuator/health",
Map.class);
Map<String, Object> components = (Map) health.get("components");
Map<String, Object> discoveryComposite = (Map) components.get("discoveryComposite");
assertThat(discoveryComposite.get("status")).isEqualTo("UP");
}
@AfterAll
public static void after() throws Exception {
appsApi.deleteCollectionNamespacedDeployment(NAMESPACE, null, null, null,
"metadata.name=" + DISCOVERYSERVER_DEPLOYMENT_NAME, null, null, null, null, null, null, null, null,
null);
api.deleteNamespacedService(DISCOVERYSERVER_APP_NAME, NAMESPACE, null, null, null, null, null, null);
networkingApi.deleteNamespacedIngress("discoveryserver-ingress", NAMESPACE, null, null, null, null, null, null);
}
private void deployDiscoveryIt() throws Exception {
appsApi.createNamespacedDeployment(NAMESPACE, getDiscoveryItDeployment(), null, null, null);
api.createNamespacedService(NAMESPACE, getDiscoveryService(), null, null, null);
networkingApi.createNamespacedIngress(NAMESPACE, getDiscoveryItIngress(), null, null, null);
}
private V1Service getDiscoveryService() throws Exception {
V1Service service = (V1Service) k8SUtils
.readYamlFromClasspath("spring-cloud-kubernetes-discoveryclient-it-service.yaml");
return service;
}
private V1Deployment getDiscoveryItDeployment() throws Exception {
V1Deployment deployment = (V1Deployment) k8SUtils
.readYamlFromClasspath("spring-cloud-kubernetes-discoveryclient-it-deployment.yaml");
String image = deployment.getSpec().getTemplate().getSpec().getContainers().get(0).getImage() + ":"
+ getPomVersion();
deployment.getSpec().getTemplate().getSpec().getContainers().get(0).setImage(image);
return deployment;
}
private V1Ingress getDiscoveryItIngress() throws Exception {
V1Ingress ingress = (V1Ingress) k8SUtils
.readYamlFromClasspath("spring-cloud-kubernetes-discoveryclient-it-ingress.yaml");
return ingress;
}
private static void deployDiscoveryServer() throws Exception {
appsApi.createNamespacedDeployment(NAMESPACE, getDiscoveryServerDeployment(), null, null, null);
api.createNamespacedService(NAMESPACE, getDiscoveryServerService(), null, null, null);
networkingApi.createNamespacedIngress(NAMESPACE, getDiscoveryServerIngress(), null, null, null);
}
private static V1Ingress getDiscoveryServerIngress() throws Exception {
V1Ingress ingress = (V1Ingress) k8SUtils
.readYamlFromClasspath("spring-cloud-kubernetes-discoveryserver-ingress.yaml");
return ingress;
}
private static V1Service getDiscoveryServerService() throws Exception {
V1Service service = (V1Service) k8SUtils
.readYamlFromClasspath("spring-cloud-kubernetes-discoveryserver-service.yaml");
return service;
}
private static V1Deployment getDiscoveryServerDeployment() throws Exception {
V1Deployment deployment = (V1Deployment) k8SUtils
.readYamlFromClasspath("spring-cloud-kubernetes-discoveryserver-deployment.yaml");
String image = deployment.getSpec().getTemplate().getSpec().getContainers().get(0).getImage() + ":"
+ getPomVersion();
deployment.getSpec().getTemplate().getSpec().getContainers().get(0).setImage(image);
return deployment;
}
}

View File

@@ -0,0 +1,28 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: spring-cloud-kubernetes-discoveryclient-it-deployment
spec:
selector:
matchLabels:
app: spring-cloud-kubernetes-discoveryclient-it
template:
metadata:
labels:
app: spring-cloud-kubernetes-discoveryclient-it
spec:
serviceAccountName: spring-cloud-kubernetes-serviceaccount
containers:
- name: spring-cloud-kubernetes-discoveryclient-it
image: docker.io/springcloud/spring-cloud-kubernetes-discoveryclient-it
imagePullPolicy: IfNotPresent
readinessProbe:
httpGet:
port: 8080
path: /actuator/health/readiness
livenessProbe:
httpGet:
port: 8080
path: /actuator/health/liveness
ports:
- containerPort: 8080

View File

@@ -0,0 +1,18 @@
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: it-ingress
namespace: default
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /$2
spec:
rules:
- http:
paths:
- path: /discoveryclient-it(/|$)(.*)
pathType: Prefix
backend:
service:
name: spring-cloud-kubernetes-discoveryclient-it
port:
number: 8080

View File

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

View File

@@ -0,0 +1,28 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: spring-cloud-kubernetes-discoveryserver-deployment
spec:
selector:
matchLabels:
app: spring-cloud-kubernetes-discoveryserver
template:
metadata:
labels:
app: spring-cloud-kubernetes-discoveryserver
spec:
serviceAccountName: spring-cloud-kubernetes-serviceaccount
containers:
- name: spring-cloud-kubernetes-discoveryserver
image: docker.io/springcloud/spring-cloud-kubernetes-discoveryserver
imagePullPolicy: IfNotPresent
readinessProbe:
httpGet:
port: 8761
path: /actuator/health/readiness
livenessProbe:
httpGet:
port: 8761
path: /actuator/health/liveness
ports:
- containerPort: 8761

View File

@@ -0,0 +1,18 @@
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: discoveryserver-ingress
namespace: default
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /$2
spec:
rules:
- http:
paths:
- path: /discoveryserver(/|$)(.*)
pathType: Prefix
backend:
service:
name: spring-cloud-kubernetes-discoveryserver
port:
number: 80

View File

@@ -0,0 +1,14 @@
apiVersion: v1
kind: Service
metadata:
labels:
app: spring-cloud-kubernetes-discoveryserver
name: spring-cloud-kubernetes-discoveryserver
spec:
ports:
- name: http
port: 80
targetPort: 8761
selector:
app: spring-cloud-kubernetes-discoveryserver
type: ClusterIP

View File

@@ -0,0 +1,26 @@
apiVersion: apps/v1
kind: Deployment
metadata:
creationTimestamp: null
labels:
app: spring-cloud-kubernetes-reactive-discoveryclient-it
name: spring-cloud-kubernetes-reactive-discoveryclient-it-deployment
spec:
replicas: 1
selector:
matchLabels:
app: spring-cloud-kubernetes-reactive-discoveryclient-it
strategy: {}
template:
metadata:
creationTimestamp: null
labels:
app: spring-cloud-kubernetes-reactive-discoveryclient-it
spec:
serviceAccountName: spring-cloud-kubernetes-serviceaccount
containers:
- image: springcloud/spring-cloud-kubernetes-reactive-discoveryclient-it:2.0.4-SNAPSHOT
imagePullPolicy: IfNotPresent
name: spring-cloud-kubernetes-reactive-discoveryclient-it
resources: {}
status: {}

View File

@@ -0,0 +1,18 @@
apiVersion: v1
kind: Service
metadata:
creationTimestamp: null
labels:
app: spring-cloud-kubernetes-reactive-discoveryclient-it
name: spring-cloud-kubernetes-reactive-discoveryclient-it
spec:
ports:
- name: 80-8080
port: 80
protocol: TCP
targetPort: 8080
selector:
app: spring-cloud-kubernetes-reactive-discoveryclient-it
type: ClusterIP
status:
loadBalancer: {}

View File

@@ -0,0 +1,141 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>spring-cloud-kubernetes-integration-tests</artifactId>
<groupId>org.springframework.cloud</groupId>
<version>2.1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>spring-cloud-kubernetes-reactive-discoveryclient-it</artifactId>
<properties>
<jib.version>1.8.0</jib.version>
<base.image>openjdk:8u222-slim</base.image>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-kubernetes-discoveryclient</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-kubernetes-test-support</artifactId>
</dependency>
<dependency>
<groupId>io.kubernetes</groupId>
<artifactId>client-java</artifactId>
</dependency>
<dependency>
<groupId>io.kubernetes</groupId>
<artifactId>client-java-extended</artifactId>
</dependency>
<dependency>
<groupId>com.github.docker-java</groupId>
<artifactId>docker-java-core</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.github.docker-java</groupId>
<artifactId>docker-java-transport-httpclient5</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<resources>
<resource>
<directory>../src/main/resources</directory>
<filtering>true</filtering>
</resource>
<resource>
<directory>src/main/resources</directory>
<filtering>true</filtering>
</resource>
</resources>
</build>
<profiles>
<profile>
<id>skaffold</id>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<image>
<name>${env.IMAGE}</name>
</image>
<goal>build-image</goal>
</configuration>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>build-image</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</profile>
<profile>
<id>imagename</id>
<activation>
<property>
<name>!env.IMAGE</name>
</property>
</activation>
<properties>
<env.IMAGE>springcloud/${project.artifactId}:${project.version}</env.IMAGE>
</properties>
</profile>
<profile>
<id>jib</id>
<build>
<plugins>
<plugin>
<groupId>com.google.cloud.tools</groupId>
<artifactId>jib-maven-plugin</artifactId>
<version>${jib.version}</version>
<configuration>
<from>
<image>${base.image}</image>
</from>
<to>
<image>spring-cloud/${project.artifactId}</image>
</to>
<container>
<user>nobody:nogroup</user>
<environment>
</environment>
</container>
</configuration>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>dockerBuild</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</profile>
</profiles>
</project>

View File

@@ -0,0 +1,22 @@
apiVersion: skaffold/v2alpha3
kind: Config
metadata:
name: spring-cloud-kubernetes-reactive-discoveryclient-it
build:
artifacts:
- image: springcloud/spring-cloud-kubernetes-reactive-discoveryclient-it
jib: {
args: [ "-Pjib" ]
}
# custom:
# buildCommand: "../../mvnw clean install -Pskaffold"
# dependencies:
# paths:
# - src
# - pom.xml
deploy:
kubectl:
manifests:
- k8s/deployment-it.yaml
- k8s/service-it.yaml
- ../permissions.yaml

View File

@@ -0,0 +1,58 @@
/*
* Copyright 2013-2021 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.reactive.discoveryclient.it;
import java.util.List;
import java.util.stream.Collectors;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.discovery.ReactiveDiscoveryClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
/**
* @author Ryan Baxter
*/
@SpringBootApplication
@RestController
public class KubernetesReactiveDiscoveryClientApplicationIt {
@Autowired
ReactiveDiscoveryClient discoveryClient;
public static void main(String[] args) {
SpringApplication.run(KubernetesReactiveDiscoveryClientApplicationIt.class, args);
}
@GetMapping("/services")
public Mono<List<String>> services() {
return discoveryClient.getServices().collect(Collectors.toList());
}
@GetMapping("/service/{serviceId}")
public Flux<ServiceInstance> service(@PathVariable String serviceId) {
return discoveryClient.getInstances(serviceId);
}
}

View File

@@ -0,0 +1,13 @@
spring:
cloud:
kubernetes:
discovery:
discoveryServerUrl: http://spring-cloud-kubernetes-discoveryserver
management:
endpoint:
health:
show-details: always
endpoints:
web:
exposure:
include: "*"

View File

@@ -0,0 +1,237 @@
/*
* Copyright 2013-2021 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.reactive.discoveryclient.it;
import java.io.IOException;
import java.time.Duration;
import java.util.Arrays;
import java.util.Map;
import io.kubernetes.client.openapi.ApiClient;
import io.kubernetes.client.openapi.ApiException;
import io.kubernetes.client.openapi.apis.AppsV1Api;
import io.kubernetes.client.openapi.apis.CoreV1Api;
import io.kubernetes.client.openapi.apis.NetworkingV1Api;
import io.kubernetes.client.openapi.models.V1Deployment;
import io.kubernetes.client.openapi.models.V1Ingress;
import io.kubernetes.client.openapi.models.V1Service;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.cloud.kubernetes.integration.tests.commons.K8SUtils;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.web.client.ResponseErrorHandler;
import org.springframework.web.client.RestTemplate;
import static org.assertj.core.api.Assertions.assertThat;
import static org.awaitility.Awaitility.await;
import static org.springframework.cloud.kubernetes.integration.tests.commons.K8SUtils.createApiClient;
import static org.springframework.cloud.kubernetes.integration.tests.commons.K8SUtils.getPomVersion;
/**
* @author Ryan Baxter
*/
class ReactiveDiscoveryClientIT {
private static final Log LOG = LogFactory.getLog(ReactiveDiscoveryClientIT.class);
private static final String DISCOVERYSERVER_DEPLOYMENT_NAME = "spring-cloud-kubernetes-discoveryserver-deployment";
private static final String DISCOVERYSERVER_APP_NAME = "spring-cloud-kubernetes-discoveryserver";
private static final String SPRING_CLOUD_K8S_DISCOVERYCLIENT_DEPLOYMENT_NAME = "spring-cloud-kubernetes-discoveryclient-it-deployment";
private static final String SPRING_CLOUD_K8S_DISCOVERYCLIENT_APP_NAME = "spring-cloud-kubernetes-discoveryclient-it";
private static final String NAMESPACE = "default";
private static ApiClient client;
private static CoreV1Api api;
private static AppsV1Api appsApi;
private static NetworkingV1Api networkingApi;
private static K8SUtils k8SUtils;
@BeforeAll
public static void setup() throws Exception {
client = createApiClient();
api = new CoreV1Api();
appsApi = new AppsV1Api();
networkingApi = new NetworkingV1Api();
k8SUtils = new K8SUtils(api, appsApi);
deployDiscoveryServer();
// Check to make sure the discovery server deployment is ready
k8SUtils.waitForDeployment(DISCOVERYSERVER_DEPLOYMENT_NAME, NAMESPACE);
// Check to see if endpoint is ready
k8SUtils.waitForEndpointReady(DISCOVERYSERVER_APP_NAME, NAMESPACE);
}
@Test
public void testDiscoveryClient() throws Exception {
try {
deployDiscoveryIt();
testLoadBalancer();
testHealth();
}
catch (Exception e) {
e.printStackTrace();
}
finally {
cleanup();
}
}
private void cleanup() throws ApiException {
appsApi.deleteCollectionNamespacedDeployment(NAMESPACE, null, null, null,
"metadata.name=" + SPRING_CLOUD_K8S_DISCOVERYCLIENT_DEPLOYMENT_NAME, null, null, null, null, null, null,
null, null, null);
api.deleteNamespacedService(SPRING_CLOUD_K8S_DISCOVERYCLIENT_APP_NAME, NAMESPACE, null, null, null, null, null,
null);
networkingApi.deleteNamespacedIngress("it-ingress", NAMESPACE, null, null, null, null, null, null);
}
private void testLoadBalancer() throws Exception {
// Check to make sure the controller deployment is ready
k8SUtils.waitForDeployment(SPRING_CLOUD_K8S_DISCOVERYCLIENT_DEPLOYMENT_NAME, NAMESPACE);
RestTemplate rest = createRestTemplate();
// Sometimes the NGINX ingress takes a bit to catch up and realize the service is
// available and we get a 503, we just need to wait a bit
await().timeout(Duration.ofSeconds(60))
.until(() -> rest.getForEntity("http://localhost:80/discoveryclient-it/services", String.class)
.getStatusCode().is2xxSuccessful());
String[] result = rest.getForObject("http://localhost:80/discoveryclient-it/services", String[].class);
LOG.info("Services: " + result);
assertThat(Arrays.stream(result).anyMatch(s -> "spring-cloud-kubernetes-discoveryserver".equalsIgnoreCase(s)))
.isTrue();
}
private RestTemplate createRestTemplate() {
RestTemplate rest = new RestTemplateBuilder().build();
rest.setErrorHandler(new ResponseErrorHandler() {
@Override
public boolean hasError(ClientHttpResponse clientHttpResponse) throws IOException {
LOG.warn("Received response status code: " + clientHttpResponse.getRawStatusCode());
if (clientHttpResponse.getRawStatusCode() == 503) {
return false;
}
return true;
}
@Override
public void handleError(ClientHttpResponse clientHttpResponse) throws IOException {
}
});
return rest;
}
public void testHealth() {
RestTemplate rest = createRestTemplate();
// Sometimes the NGINX ingress takes a bit to catch up and realize the service is
// available and we get a 503, we just need to wait a bit
await().timeout(Duration.ofSeconds(60))
.until(() -> rest.getForEntity("http://localhost:80/discoveryclient-it/actuator/health", String.class)
.getStatusCode().is2xxSuccessful());
Map<String, Object> health = rest.getForObject("http://localhost:80/discoveryclient-it/actuator/health",
Map.class);
Map<String, Object> components = (Map) health.get("components");
Map<String, Object> discoveryComposite = (Map) components.get("discoveryComposite");
assertThat(discoveryComposite.get("status")).isEqualTo("UP");
}
@AfterAll
public static void after() throws Exception {
appsApi.deleteCollectionNamespacedDeployment(NAMESPACE, null, null, null,
"metadata.name=" + DISCOVERYSERVER_DEPLOYMENT_NAME, null, null, null, null, null, null, null, null,
null);
api.deleteNamespacedService(DISCOVERYSERVER_APP_NAME, NAMESPACE, null, null, null, null, null, null);
networkingApi.deleteNamespacedIngress("discoveryserver-ingress", NAMESPACE, null, null, null, null, null, null);
}
private void deployDiscoveryIt() throws Exception {
appsApi.createNamespacedDeployment(NAMESPACE, getDiscoveryItDeployment(), null, null, null);
api.createNamespacedService(NAMESPACE, getDiscoveryService(), null, null, null);
networkingApi.createNamespacedIngress(NAMESPACE, getDiscoveryItIngress(), null, null, null);
}
private V1Service getDiscoveryService() throws Exception {
V1Service service = (V1Service) k8SUtils
.readYamlFromClasspath("spring-cloud-kubernetes-discoveryclient-it-service.yaml");
return service;
}
private V1Deployment getDiscoveryItDeployment() throws Exception {
V1Deployment deployment = (V1Deployment) k8SUtils
.readYamlFromClasspath("spring-cloud-kubernetes-discoveryclient-it-deployment.yaml");
String image = deployment.getSpec().getTemplate().getSpec().getContainers().get(0).getImage() + ":"
+ getPomVersion();
deployment.getSpec().getTemplate().getSpec().getContainers().get(0).setImage(image);
return deployment;
}
private V1Ingress getDiscoveryItIngress() throws Exception {
V1Ingress ingress = (V1Ingress) k8SUtils
.readYamlFromClasspath("spring-cloud-kubernetes-discoveryclient-it-ingress.yaml");
return ingress;
}
private static void deployDiscoveryServer() throws Exception {
appsApi.createNamespacedDeployment(NAMESPACE, getDiscoveryServerDeployment(), null, null, null);
api.createNamespacedService(NAMESPACE, getDiscoveryServerService(), null, null, null);
networkingApi.createNamespacedIngress(NAMESPACE, getDiscoveryServerIngress(), null, null, null);
}
private static V1Ingress getDiscoveryServerIngress() throws Exception {
V1Ingress ingress = (V1Ingress) k8SUtils
.readYamlFromClasspath("spring-cloud-kubernetes-discoveryserver-ingress.yaml");
return ingress;
}
private static V1Service getDiscoveryServerService() throws Exception {
V1Service service = (V1Service) k8SUtils
.readYamlFromClasspath("spring-cloud-kubernetes-discoveryserver-service.yaml");
return service;
}
private static V1Deployment getDiscoveryServerDeployment() throws Exception {
V1Deployment deployment = (V1Deployment) k8SUtils
.readYamlFromClasspath("spring-cloud-kubernetes-discoveryserver-deployment.yaml");
String image = deployment.getSpec().getTemplate().getSpec().getContainers().get(0).getImage() + ":"
+ getPomVersion();
deployment.getSpec().getTemplate().getSpec().getContainers().get(0).setImage(image);
return deployment;
}
}

View File

@@ -0,0 +1,28 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: spring-cloud-kubernetes-discoveryclient-it-deployment
spec:
selector:
matchLabels:
app: spring-cloud-kubernetes-discoveryclient-it
template:
metadata:
labels:
app: spring-cloud-kubernetes-discoveryclient-it
spec:
serviceAccountName: spring-cloud-kubernetes-serviceaccount
containers:
- name: spring-cloud-kubernetes-discoveryclient-it
image: docker.io/springcloud/spring-cloud-kubernetes-reactive-discoveryclient-it
imagePullPolicy: IfNotPresent
readinessProbe:
httpGet:
port: 8080
path: /actuator/health/readiness
livenessProbe:
httpGet:
port: 8080
path: /actuator/health/liveness
ports:
- containerPort: 8080

View File

@@ -0,0 +1,18 @@
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: it-ingress
namespace: default
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /$2
spec:
rules:
- http:
paths:
- path: /discoveryclient-it(/|$)(.*)
pathType: Prefix
backend:
service:
name: spring-cloud-kubernetes-discoveryclient-it
port:
number: 8080

View File

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

View File

@@ -0,0 +1,28 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: spring-cloud-kubernetes-discoveryserver-deployment
spec:
selector:
matchLabels:
app: spring-cloud-kubernetes-discoveryserver
template:
metadata:
labels:
app: spring-cloud-kubernetes-discoveryserver
spec:
serviceAccountName: spring-cloud-kubernetes-serviceaccount
containers:
- name: spring-cloud-kubernetes-discoveryserver
image: docker.io/springcloud/spring-cloud-kubernetes-discoveryserver
imagePullPolicy: IfNotPresent
readinessProbe:
httpGet:
port: 8761
path: /actuator/health/readiness
livenessProbe:
httpGet:
port: 8761
path: /actuator/health/liveness
ports:
- containerPort: 8761

View File

@@ -0,0 +1,18 @@
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: discoveryserver-ingress
namespace: default
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /$2
spec:
rules:
- http:
paths:
- path: /discoveryserver(/|$)(.*)
pathType: Prefix
backend:
service:
name: spring-cloud-kubernetes-discoveryserver
port:
number: 80

View File

@@ -0,0 +1,14 @@
apiVersion: v1
kind: Service
metadata:
labels:
app: spring-cloud-kubernetes-discoveryserver
name: spring-cloud-kubernetes-discoveryserver
spec:
ports:
- name: http
port: 80
targetPort: 8761
selector:
app: spring-cloud-kubernetes-discoveryserver
type: ClusterIP

View File

@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>spring-cloud-kubernetes</artifactId>
<groupId>org.springframework.cloud</groupId>
<version>2.1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>spring-cloud-starter-kubernetes-discoveryclient</artifactId>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-commons</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-kubernetes-discovery</artifactId>
</dependency>
</dependencies>
</project>

View File

@@ -13,5 +13,6 @@
<suppress files=".*PollingConfigurationChangeDetector.*" checks="LineLength*"/>
<suppress files=".*LeaderRecordWatcherTest.*" checks="LineLength*"/>
<suppress files=".*ConfigurationWatcherApplication\.java" checks="HideUtilityClassConstructor"/>
<suppress files=".*DiscoveryServerApplication\.java" checks="HideUtilityClassConstructor"/>
<suppress files=".*KubernetesConfigServerApplication\.java" checks="HideUtilityClassConstructor"/>
</suppressions>