From a265331fd0cf451d3c4939bb57abe36050248a36 Mon Sep 17 00:00:00 2001 From: Ryan Baxter Date: Fri, 15 Oct 2021 07:24:31 -0400 Subject: [PATCH] Adds Discovery Server and Discovery Client (#886) --- README.adoc | 227 +++++++++++++++++ docs/src/main/asciidoc/discovery-client.adoc | 14 ++ ...ring-cloud-kubernetes-discoveryserver.adoc | 212 ++++++++++++++++ .../asciidoc/spring-cloud-kubernetes.adoc | 2 + pom.xml | 2 + scripts/deploy.sh | 1 + .../KubernetesInformerDiscoveryClient.java | 3 +- ...ubernetesInformerDiscoveryClientTests.java | 44 ++-- ...sInformerReactiveDiscoveryClientTests.java | 10 +- spring-cloud-kubernetes-commons/pom.xml | 4 + .../discovery/KubernetesServiceInstance.java | 102 +++++++- spring-cloud-kubernetes-controllers/pom.xml | 1 + .../k8s/deployment.yaml | 74 ++++++ .../pom.xml | 149 +++++++++++ .../skaffold.yaml | 20 ++ .../DiscoveryServerApplication.java | 32 +++ .../DiscoveryServerController.java | 110 ++++++++ .../src/main/resources/application.yaml | 2 + .../DiscoveryServerControllerTests.java | 120 +++++++++ .../DiscoveryServerIntegrationTests.java | 208 +++++++++++++++ spring-cloud-kubernetes-dependencies/pom.xml | 12 + spring-cloud-kubernetes-discovery/pom.xml | 59 +++++ .../DiscoveryServerUrlInvalidException.java | 28 +++ .../discovery/KubernetesDiscoveryClient.java | 72 ++++++ ...netesDiscoveryClientAutoConfiguration.java | 114 +++++++++ .../KubernetesDiscoveryClientProperties.java | 47 ++++ .../KubernetesReactiveDiscoveryClient.java | 61 +++++ .../discovery/KubernetesServiceInstance.java | 166 ++++++++++++ .../cloud/kubernetes/discovery/Service.java | 49 ++++ .../main/resources/META-INF/spring.factories | 2 + ...DiscoveryClientAutoConfigurationTests.java | 113 +++++++++ .../KubernetesDiscoveryClientTests.java | 86 +++++++ ...ubernetesReactiveDiscoveryClientTests.java | 86 +++++++ .../run.sh | 5 +- .../k8s/deployment-it.yaml | 26 ++ .../k8s/service-it.yaml | 18 ++ .../pom.xml | 143 +++++++++++ .../skaffold.yaml | 22 ++ ...ubernetesDiscoveryClientApplicationIt.java | 54 ++++ .../src/main/resources/application.yaml | 13 + .../discoveryclient/it/DiscoveryClientIT.java | 237 ++++++++++++++++++ ...ernetes-discoveryclient-it-deployment.yaml | 28 +++ ...kubernetes-discoveryclient-it-ingress.yaml | 18 ++ ...kubernetes-discoveryclient-it-service.yaml | 14 ++ ...kubernetes-discoveryserver-deployment.yaml | 28 +++ ...ud-kubernetes-discoveryserver-ingress.yaml | 18 ++ ...ud-kubernetes-discoveryserver-service.yaml | 14 ++ .../k8s/deployment-it.yaml | 26 ++ .../k8s/service-it.yaml | 18 ++ .../pom.xml | 141 +++++++++++ .../skaffold.yaml | 22 ++ ...sReactiveDiscoveryClientApplicationIt.java | 58 +++++ .../src/main/resources/application.yaml | 13 + .../it/ReactiveDiscoveryClientIT.java | 237 ++++++++++++++++++ ...ernetes-discoveryclient-it-deployment.yaml | 28 +++ ...kubernetes-discoveryclient-it-ingress.yaml | 18 ++ ...kubernetes-discoveryclient-it-service.yaml | 14 ++ ...kubernetes-discoveryserver-deployment.yaml | 28 +++ ...ud-kubernetes-discoveryserver-ingress.yaml | 18 ++ ...ud-kubernetes-discoveryserver-service.yaml | 14 ++ .../pom.xml | 25 ++ src/checkstyle/checkstyle-suppressions.xml | 1 + 62 files changed, 3491 insertions(+), 40 deletions(-) create mode 100644 docs/src/main/asciidoc/spring-cloud-kubernetes-discoveryserver.adoc create mode 100644 spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-discoveryserver/k8s/deployment.yaml create mode 100644 spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-discoveryserver/pom.xml create mode 100644 spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-discoveryserver/skaffold.yaml create mode 100644 spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-discoveryserver/src/main/java/org/springframewok/cloud/kubernetes/discoveryserver/DiscoveryServerApplication.java create mode 100644 spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-discoveryserver/src/main/java/org/springframewok/cloud/kubernetes/discoveryserver/DiscoveryServerController.java create mode 100644 spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-discoveryserver/src/main/resources/application.yaml create mode 100644 spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-discoveryserver/src/main/test/java/org/springframewok/cloud/kubernetes/discoveryserver/DiscoveryServerControllerTests.java create mode 100644 spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-discoveryserver/src/main/test/java/org/springframewok/cloud/kubernetes/discoveryserver/DiscoveryServerIntegrationTests.java create mode 100644 spring-cloud-kubernetes-discovery/pom.xml create mode 100644 spring-cloud-kubernetes-discovery/src/main/java/org/springframework/cloud/kubernetes/discovery/DiscoveryServerUrlInvalidException.java create mode 100644 spring-cloud-kubernetes-discovery/src/main/java/org/springframework/cloud/kubernetes/discovery/KubernetesDiscoveryClient.java create mode 100644 spring-cloud-kubernetes-discovery/src/main/java/org/springframework/cloud/kubernetes/discovery/KubernetesDiscoveryClientAutoConfiguration.java create mode 100644 spring-cloud-kubernetes-discovery/src/main/java/org/springframework/cloud/kubernetes/discovery/KubernetesDiscoveryClientProperties.java create mode 100644 spring-cloud-kubernetes-discovery/src/main/java/org/springframework/cloud/kubernetes/discovery/KubernetesReactiveDiscoveryClient.java create mode 100644 spring-cloud-kubernetes-discovery/src/main/java/org/springframework/cloud/kubernetes/discovery/KubernetesServiceInstance.java create mode 100644 spring-cloud-kubernetes-discovery/src/main/java/org/springframework/cloud/kubernetes/discovery/Service.java create mode 100644 spring-cloud-kubernetes-discovery/src/main/resources/META-INF/spring.factories create mode 100644 spring-cloud-kubernetes-discovery/src/test/java/org/springframework/cloud/kubernetes/discovery/KubernetesDiscoveryClientAutoConfigurationTests.java create mode 100644 spring-cloud-kubernetes-discovery/src/test/java/org/springframework/cloud/kubernetes/discovery/KubernetesDiscoveryClientTests.java create mode 100644 spring-cloud-kubernetes-discovery/src/test/java/org/springframework/cloud/kubernetes/discovery/KubernetesReactiveDiscoveryClientTests.java create mode 100644 spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-discoverclient-it/k8s/deployment-it.yaml create mode 100644 spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-discoverclient-it/k8s/service-it.yaml create mode 100644 spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-discoverclient-it/pom.xml create mode 100644 spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-discoverclient-it/skaffold.yaml create mode 100644 spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-discoverclient-it/src/main/java/org/springframework/cloud/kubernetes/discoveryclient/it/KubernetesDiscoveryClientApplicationIt.java create mode 100644 spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-discoverclient-it/src/main/resources/application.yaml create mode 100644 spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-discoverclient-it/src/test/java/org/springframework/cloud/kubernetes/discoveryclient/it/DiscoveryClientIT.java create mode 100644 spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-discoverclient-it/src/test/resources/spring-cloud-kubernetes-discoveryclient-it-deployment.yaml create mode 100644 spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-discoverclient-it/src/test/resources/spring-cloud-kubernetes-discoveryclient-it-ingress.yaml create mode 100644 spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-discoverclient-it/src/test/resources/spring-cloud-kubernetes-discoveryclient-it-service.yaml create mode 100644 spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-discoverclient-it/src/test/resources/spring-cloud-kubernetes-discoveryserver-deployment.yaml create mode 100644 spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-discoverclient-it/src/test/resources/spring-cloud-kubernetes-discoveryserver-ingress.yaml create mode 100644 spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-discoverclient-it/src/test/resources/spring-cloud-kubernetes-discoveryserver-service.yaml create mode 100644 spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-reactive-discoveryclient-it/k8s/deployment-it.yaml create mode 100644 spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-reactive-discoveryclient-it/k8s/service-it.yaml create mode 100644 spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-reactive-discoveryclient-it/pom.xml create mode 100644 spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-reactive-discoveryclient-it/skaffold.yaml create mode 100644 spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-reactive-discoveryclient-it/src/main/java/org/springframework/cloud/kubernetes/reactive/discoveryclient/it/KubernetesReactiveDiscoveryClientApplicationIt.java create mode 100644 spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-reactive-discoveryclient-it/src/main/resources/application.yaml create mode 100644 spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-reactive-discoveryclient-it/src/test/java/org/springframework/cloud/kubernetes/reactive/discoveryclient/it/ReactiveDiscoveryClientIT.java create mode 100644 spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-reactive-discoveryclient-it/src/test/resources/spring-cloud-kubernetes-discoveryclient-it-deployment.yaml create mode 100644 spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-reactive-discoveryclient-it/src/test/resources/spring-cloud-kubernetes-discoveryclient-it-ingress.yaml create mode 100644 spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-reactive-discoveryclient-it/src/test/resources/spring-cloud-kubernetes-discoveryclient-it-service.yaml create mode 100644 spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-reactive-discoveryclient-it/src/test/resources/spring-cloud-kubernetes-discoveryserver-deployment.yaml create mode 100644 spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-reactive-discoveryclient-it/src/test/resources/spring-cloud-kubernetes-discoveryserver-ingress.yaml create mode 100644 spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-reactive-discoveryclient-it/src/test/resources/spring-cloud-kubernetes-discoveryserver-service.yaml create mode 100644 spring-cloud-starter-kubernetes-discoveryclient/pom.xml diff --git a/README.adoc b/README.adoc index 7e52f400..b1ec7f7a 100644 --- a/README.adoc +++ b/README.adoc @@ -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] +---- + + org.springframework.cloud + spring-cloud-starter-kubernetes-discoveryclient + +---- +==== + +NOTE: `spring-cloud-starter-kubernetes-discoveryclient` is designed to be used with the +<>. + ==== 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 diff --git a/docs/src/main/asciidoc/discovery-client.adoc b/docs/src/main/asciidoc/discovery-client.adoc index 2a56f3a6..cb49eac9 100644 --- a/docs/src/main/asciidoc/discovery-client.adoc +++ b/docs/src/main/asciidoc/discovery-client.adoc @@ -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] +---- + + org.springframework.cloud + spring-cloud-starter-kubernetes-discoveryclient + +---- +==== + +NOTE: `spring-cloud-starter-kubernetes-discoveryclient` is designed to be used with the +<>. + ==== Fabric8 Kubernetes Client [source,xml] diff --git a/docs/src/main/asciidoc/spring-cloud-kubernetes-discoveryserver.adoc b/docs/src/main/asciidoc/spring-cloud-kubernetes-discoveryserver.adoc new file mode 100644 index 00000000..62f1318c --- /dev/null +++ b/docs/src/main/asciidoc/spring-cloud-kubernetes-discoveryserver.adoc @@ -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 + + +---- +==== diff --git a/docs/src/main/asciidoc/spring-cloud-kubernetes.adoc b/docs/src/main/asciidoc/spring-cloud-kubernetes.adoc index ba6f7afe..2f24a440 100644 --- a/docs/src/main/asciidoc/spring-cloud-kubernetes.adoc +++ b/docs/src/main/asciidoc/spring-cloud-kubernetes.adoc @@ -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[] diff --git a/pom.xml b/pom.xml index 3d7c4c68..cdc939ac 100644 --- a/pom.xml +++ b/pom.xml @@ -110,6 +110,8 @@ docs spring-cloud-kubernetes-fabric8-loadbalancer spring-cloud-starter-kubernetes-fabric8-loadbalancer + spring-cloud-kubernetes-discovery + spring-cloud-starter-kubernetes-discoveryclient diff --git a/scripts/deploy.sh b/scripts/deploy.sh index 7b39b7eb..c7d1b14e 100755 --- a/scripts/deploy.sh +++ b/scripts/deploy.sh @@ -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 ${@} diff --git a/spring-cloud-kubernetes-client-discovery/src/main/java/org/springframework/cloud/kubernetes/client/discovery/KubernetesInformerDiscoveryClient.java b/spring-cloud-kubernetes-client-discovery/src/main/java/org/springframework/cloud/kubernetes/client/discovery/KubernetesInformerDiscoveryClient.java index 112f8a4f..32a7d7f4 100644 --- a/spring-cloud-kubernetes-client-discovery/src/main/java/org/springframework/cloud/kubernetes/client/discovery/KubernetesInformerDiscoveryClient.java +++ b/spring-cloud-kubernetes-client-discovery/src/main/java/org/springframework/cloud/kubernetes/client/discovery/KubernetesInformerDiscoveryClient.java @@ -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()); } diff --git a/spring-cloud-kubernetes-client-discovery/src/test/java/org/springframework/cloud/kubernetes/client/discovery/KubernetesInformerDiscoveryClientTests.java b/spring-cloud-kubernetes-client-discovery/src/test/java/org/springframework/cloud/kubernetes/client/discovery/KubernetesInformerDiscoveryClientTests.java index a46dca61..c435047b 100644 --- a/spring-cloud-kubernetes-client-discovery/src/test/java/org/springframework/cloud/kubernetes/client/discovery/KubernetesInformerDiscoveryClientTests.java +++ b/spring-cloud-kubernetes-client-discovery/src/test/java/org/springframework/cloud/kubernetes/client/discovery/KubernetesInformerDiscoveryClientTests.java @@ -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(); } diff --git a/spring-cloud-kubernetes-client-discovery/src/test/java/org/springframework/cloud/kubernetes/client/discovery/reactive/KubernetesInformerReactiveDiscoveryClientTests.java b/spring-cloud-kubernetes-client-discovery/src/test/java/org/springframework/cloud/kubernetes/client/discovery/reactive/KubernetesInformerReactiveDiscoveryClientTests.java index 519f2bb8..84dcaf9b 100644 --- a/spring-cloud-kubernetes-client-discovery/src/test/java/org/springframework/cloud/kubernetes/client/discovery/reactive/KubernetesInformerReactiveDiscoveryClientTests.java +++ b/spring-cloud-kubernetes-client-discovery/src/test/java/org/springframework/cloud/kubernetes/client/discovery/reactive/KubernetesInformerReactiveDiscoveryClientTests.java @@ -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(); diff --git a/spring-cloud-kubernetes-commons/pom.xml b/spring-cloud-kubernetes-commons/pom.xml index 3bd1b90d..92208439 100644 --- a/spring-cloud-kubernetes-commons/pom.xml +++ b/spring-cloud-kubernetes-commons/pom.xml @@ -20,6 +20,10 @@ org.springframework.cloud spring-cloud-commons + + org.springframework.boot + spring-boot-starter-logging + org.springframework.boot spring-boot-actuator-autoconfigure diff --git a/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/discovery/KubernetesServiceInstance.java b/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/discovery/KubernetesServiceInstance.java index 085c0ad7..afa78de6 100644 --- a/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/discovery/KubernetesServiceInstance.java +++ b/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/discovery/KubernetesServiceInstance.java @@ -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 metadata; + private Map 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 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 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); } } diff --git a/spring-cloud-kubernetes-controllers/pom.xml b/spring-cloud-kubernetes-controllers/pom.xml index d6a87525..0d75a5c6 100644 --- a/spring-cloud-kubernetes-controllers/pom.xml +++ b/spring-cloud-kubernetes-controllers/pom.xml @@ -14,6 +14,7 @@ spring-cloud-kubernetes-configuration-watcher + spring-cloud-kubernetes-discoveryserver spring-cloud-kubernetes-configserver diff --git a/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-discoveryserver/k8s/deployment.yaml b/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-discoveryserver/k8s/deployment.yaml new file mode 100644 index 00000000..1ed29eec --- /dev/null +++ b/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-discoveryserver/k8s/deployment.yaml @@ -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 diff --git a/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-discoveryserver/pom.xml b/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-discoveryserver/pom.xml new file mode 100644 index 00000000..d0656c77 --- /dev/null +++ b/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-discoveryserver/pom.xml @@ -0,0 +1,149 @@ + + + + spring-cloud-kubernetes-controllers + org.springframework.cloud + 2.1.0-SNAPSHOT + + 4.0.0 + + spring-cloud-kubernetes-discoveryserver + + + 1.8.0 + openjdk:8u222-slim + springcloud + 4.1.0 + + + + + org.springframework.boot + spring-boot-starter-webflux + + + org.springframework.boot + spring-boot-starter-actuator + + + org.springframework.cloud + spring-cloud-starter-kubernetes-client + + + org.springframework.boot + spring-boot-starter-test + test + + + io.projectreactor + reactor-test + test + + + com.github.tomakehurst + wiremock-jre8 + test + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + ${env.IMAGE} + + build-image + + + + package + + build-image + + + + + + + + + + dockerpush + + + + com.spotify + dockerfile-maven-plugin + 1.4.12 + + ${docker.registry.organization}/${artifactId} + ${project.version} + ${env.DOCKER_HUB_USERNAME} + ${env.DOCKER_HUB_PASSWORD} + + true + + + + + org.codehaus.plexus + plexus-archiver + ${plexus-archiver.version} + + + + + + + + imagename + + + !env.IMAGE + + + + springcloud/${project.artifactId}:${project.version} + + + + jib + + + + com.google.cloud.tools + jib-maven-plugin + ${jib.version} + + + ${base.image} + + + spring-cloud/${project.artifactId} + + + nobody:nogroup + + + + + + + package + + dockerBuild + + + + + + + + + + diff --git a/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-discoveryserver/skaffold.yaml b/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-discoveryserver/skaffold.yaml new file mode 100644 index 00000000..8a0467ee --- /dev/null +++ b/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-discoveryserver/skaffold.yaml @@ -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 diff --git a/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-discoveryserver/src/main/java/org/springframewok/cloud/kubernetes/discoveryserver/DiscoveryServerApplication.java b/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-discoveryserver/src/main/java/org/springframewok/cloud/kubernetes/discoveryserver/DiscoveryServerApplication.java new file mode 100644 index 00000000..84415f74 --- /dev/null +++ b/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-discoveryserver/src/main/java/org/springframewok/cloud/kubernetes/discoveryserver/DiscoveryServerApplication.java @@ -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); + } + +} diff --git a/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-discoveryserver/src/main/java/org/springframewok/cloud/kubernetes/discoveryserver/DiscoveryServerController.java b/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-discoveryserver/src/main/java/org/springframewok/cloud/kubernetes/discoveryserver/DiscoveryServerController.java new file mode 100644 index 00000000..612a361a --- /dev/null +++ b/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-discoveryserver/src/main/java/org/springframewok/cloud/kubernetes/discoveryserver/DiscoveryServerController.java @@ -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 apps() { + return reactiveDiscoveryClient.getServices().flatMap(service -> reactiveDiscoveryClient.getInstances(service) + .collectList().flatMap(serviceInstances -> Mono.just(new Service(service, serviceInstances)))); + } + + @GetMapping("/apps/{name}") + public Flux appInstances(@PathVariable String name) { + return reactiveDiscoveryClient.getInstances(name); + } + + @GetMapping("/app/{name}/{instanceId}") + public Mono 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 serviceInstances; + + public Service() { + } + + public Service(String name, List serviceInstances) { + this.name = name; + this.serviceInstances = serviceInstances; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public List getServiceInstances() { + return serviceInstances; + } + + public void setServiceInstances(List 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()); + } + + } + +} diff --git a/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-discoveryserver/src/main/resources/application.yaml b/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-discoveryserver/src/main/resources/application.yaml new file mode 100644 index 00000000..58e7ca98 --- /dev/null +++ b/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-discoveryserver/src/main/resources/application.yaml @@ -0,0 +1,2 @@ +server: + port: 8761 diff --git a/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-discoveryserver/src/main/test/java/org/springframewok/cloud/kubernetes/discoveryserver/DiscoveryServerControllerTests.java b/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-discoveryserver/src/main/test/java/org/springframewok/cloud/kubernetes/discoveryserver/DiscoveryServerControllerTests.java new file mode 100644 index 00000000..bb29224a --- /dev/null +++ b/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-discoveryserver/src/main/test/java/org/springframewok/cloud/kubernetes/discoveryserver/DiscoveryServerControllerTests.java @@ -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 services = Flux.just("serviceA", "serviceB", "serviceC"); + + List serviceAInstanceList = new ArrayList<>(); + serviceAInstanceList.add(serviceAInstance1); + serviceAInstanceList.add(serviceAInstance2); + serviceAInstanceList.add(serviceAInstance3); + + Flux serviceAInstances = Flux.fromIterable(serviceAInstanceList); + + List serviceBInstanceList = Collections.singletonList(serviceBInstance1); + Flux serviceBInstances = Flux.fromIterable(serviceBInstanceList); + + List serviceCInstanceList = Collections.singletonList(serviceCInstance1); + Flux 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(); + } +} diff --git a/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-discoveryserver/src/main/test/java/org/springframewok/cloud/kubernetes/discoveryserver/DiscoveryServerIntegrationTests.java b/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-discoveryserver/src/main/test/java/org/springframewok/cloud/kubernetes/discoveryserver/DiscoveryServerIntegrationTests.java new file mode 100644 index 00000000..4167a0fa --- /dev/null +++ b/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-discoveryserver/src/main/test/java/org/springframewok/cloud/kubernetes/discoveryserver/DiscoveryServerIntegrationTests.java @@ -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 kubernetesServiceInstance1Metadata = new HashMap<>(); + kubernetesServiceInstance1Metadata.put(testEndpoints1.getSubsets().get(0).getPorts().get(0).getName(), testEndpoints1.getSubsets().get(0).getPorts().get(0).getPort().toString()); + + Map 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 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 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 serviceInstances; + + public KubernetesService() { } + + public KubernetesService(String name, List serviceInstances) { + this.name = name; + this.serviceInstances = serviceInstances; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public List getServiceInstances() { + return serviceInstances; + } + + public void setServiceInstances(List 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 + + '}'; + } + } +} diff --git a/spring-cloud-kubernetes-dependencies/pom.xml b/spring-cloud-kubernetes-dependencies/pom.xml index e7f831e7..cb028fa0 100644 --- a/spring-cloud-kubernetes-dependencies/pom.xml +++ b/spring-cloud-kubernetes-dependencies/pom.xml @@ -140,6 +140,12 @@ ${project.version} + + org.springframework.cloud + spring-cloud-kubernetes-discovery + ${project.version} + + org.springframework.cloud @@ -189,6 +195,12 @@ ${project.version} + + org.springframework.cloud + spring-cloud-starter-kubernetes-discoveryclient + ${project.version} + + org.jboss.arquillian.junit diff --git a/spring-cloud-kubernetes-discovery/pom.xml b/spring-cloud-kubernetes-discovery/pom.xml new file mode 100644 index 00000000..e9b615ef --- /dev/null +++ b/spring-cloud-kubernetes-discovery/pom.xml @@ -0,0 +1,59 @@ + + + + spring-cloud-kubernetes + org.springframework.cloud + 2.1.0-SNAPSHOT + + 4.0.0 + + spring-cloud-kubernetes-discovery + + + + + + + org.springframework.boot + spring-boot-starter-web + true + + + org.springframework.cloud + spring-cloud-commons + + + org.springframework.boot + spring-boot-actuator + true + + + org.springframework.boot + spring-boot-autoconfigure + true + + + org.springframework.boot + spring-boot-starter-webflux + true + + + org.springframework.boot + spring-boot-starter-test + test + + + com.github.tomakehurst + wiremock-jre8 + test + + + io.projectreactor + reactor-test + test + + + + diff --git a/spring-cloud-kubernetes-discovery/src/main/java/org/springframework/cloud/kubernetes/discovery/DiscoveryServerUrlInvalidException.java b/spring-cloud-kubernetes-discovery/src/main/java/org/springframework/cloud/kubernetes/discovery/DiscoveryServerUrlInvalidException.java new file mode 100644 index 00000000..ed173af3 --- /dev/null +++ b/spring-cloud-kubernetes-discovery/src/main/java/org/springframework/cloud/kubernetes/discovery/DiscoveryServerUrlInvalidException.java @@ -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."); + } + +} diff --git a/spring-cloud-kubernetes-discovery/src/main/java/org/springframework/cloud/kubernetes/discovery/KubernetesDiscoveryClient.java b/spring-cloud-kubernetes-discovery/src/main/java/org/springframework/cloud/kubernetes/discovery/KubernetesDiscoveryClient.java new file mode 100644 index 00000000..25f23e72 --- /dev/null +++ b/spring-cloud-kubernetes-discovery/src/main/java/org/springframework/cloud/kubernetes/discovery/KubernetesDiscoveryClient.java @@ -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 getInstances(String serviceId) { + List 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 getServices() { + List 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; + } + +} diff --git a/spring-cloud-kubernetes-discovery/src/main/java/org/springframework/cloud/kubernetes/discovery/KubernetesDiscoveryClientAutoConfiguration.java b/spring-cloud-kubernetes-discovery/src/main/java/org/springframework/cloud/kubernetes/discovery/KubernetesDiscoveryClientAutoConfiguration.java new file mode 100644 index 00000000..d40e06e6 --- /dev/null +++ b/spring-cloud-kubernetes-discovery/src/main/java/org/springframework/cloud/kubernetes/discovery/KubernetesDiscoveryClientAutoConfiguration.java @@ -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; + } + + } + +} diff --git a/spring-cloud-kubernetes-discovery/src/main/java/org/springframework/cloud/kubernetes/discovery/KubernetesDiscoveryClientProperties.java b/spring-cloud-kubernetes-discovery/src/main/java/org/springframework/cloud/kubernetes/discovery/KubernetesDiscoveryClientProperties.java new file mode 100644 index 00000000..43884142 --- /dev/null +++ b/spring-cloud-kubernetes-discovery/src/main/java/org/springframework/cloud/kubernetes/discovery/KubernetesDiscoveryClientProperties.java @@ -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; + } + +} diff --git a/spring-cloud-kubernetes-discovery/src/main/java/org/springframework/cloud/kubernetes/discovery/KubernetesReactiveDiscoveryClient.java b/spring-cloud-kubernetes-discovery/src/main/java/org/springframework/cloud/kubernetes/discovery/KubernetesReactiveDiscoveryClient.java new file mode 100644 index 00000000..e0e830b3 --- /dev/null +++ b/spring-cloud-kubernetes-discovery/src/main/java/org/springframework/cloud/kubernetes/discovery/KubernetesReactiveDiscoveryClient.java @@ -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 getInstances(String serviceId) { + return webClient.get().uri("/apps/" + serviceId) + .exchangeToFlux(clientResponse -> clientResponse.bodyToFlux(KubernetesServiceInstance.class)); + } + + @Override + @Cacheable("services") + public Flux getServices() { + return webClient.get().uri("/apps").exchangeToFlux( + clientResponse -> clientResponse.bodyToFlux(Service.class).map(service -> service.getName())); + } + +} diff --git a/spring-cloud-kubernetes-discovery/src/main/java/org/springframework/cloud/kubernetes/discovery/KubernetesServiceInstance.java b/spring-cloud-kubernetes-discovery/src/main/java/org/springframework/cloud/kubernetes/discovery/KubernetesServiceInstance.java new file mode 100644 index 00000000..f397da60 --- /dev/null +++ b/spring-cloud-kubernetes-discovery/src/main/java/org/springframework/cloud/kubernetes/discovery/KubernetesServiceInstance.java @@ -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 metadata; + + private String scheme; + + private String namespace; + + public KubernetesServiceInstance() { + } + + public KubernetesServiceInstance(String instanceId, String serviceId, String host, int port, boolean secure, + URI uri, Map 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 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 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()); + } + +} diff --git a/spring-cloud-kubernetes-discovery/src/main/java/org/springframework/cloud/kubernetes/discovery/Service.java b/spring-cloud-kubernetes-discovery/src/main/java/org/springframework/cloud/kubernetes/discovery/Service.java new file mode 100644 index 00000000..81619e21 --- /dev/null +++ b/spring-cloud-kubernetes-discovery/src/main/java/org/springframework/cloud/kubernetes/discovery/Service.java @@ -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 serviceInstances; + + public Service() { + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public List getServiceInstances() { + return serviceInstances; + } + + public void setServiceInstances(List serviceInstances) { + this.serviceInstances = serviceInstances; + } + +} diff --git a/spring-cloud-kubernetes-discovery/src/main/resources/META-INF/spring.factories b/spring-cloud-kubernetes-discovery/src/main/resources/META-INF/spring.factories new file mode 100644 index 00000000..a82e049f --- /dev/null +++ b/spring-cloud-kubernetes-discovery/src/main/resources/META-INF/spring.factories @@ -0,0 +1,2 @@ +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ +org.springframework.cloud.kubernetes.discovery.KubernetesDiscoveryClientAutoConfiguration diff --git a/spring-cloud-kubernetes-discovery/src/test/java/org/springframework/cloud/kubernetes/discovery/KubernetesDiscoveryClientAutoConfigurationTests.java b/spring-cloud-kubernetes-discovery/src/test/java/org/springframework/cloud/kubernetes/discovery/KubernetesDiscoveryClientAutoConfigurationTests.java new file mode 100644 index 00000000..60bc45e3 --- /dev/null +++ b/spring-cloud-kubernetes-discovery/src/test/java/org/springframework/cloud/kubernetes/discovery/KubernetesDiscoveryClientAutoConfigurationTests.java @@ -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); + }); + } + +} diff --git a/spring-cloud-kubernetes-discovery/src/test/java/org/springframework/cloud/kubernetes/discovery/KubernetesDiscoveryClientTests.java b/spring-cloud-kubernetes-discovery/src/test/java/org/springframework/cloud/kubernetes/discovery/KubernetesDiscoveryClientTests.java new file mode 100644 index 00000000..60bd9416 --- /dev/null +++ b/spring-cloud-kubernetes-discovery/src/test/java/org/springframework/cloud/kubernetes/discovery/KubernetesDiscoveryClientTests.java @@ -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 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(); + } + +} diff --git a/spring-cloud-kubernetes-discovery/src/test/java/org/springframework/cloud/kubernetes/discovery/KubernetesReactiveDiscoveryClientTests.java b/spring-cloud-kubernetes-discovery/src/test/java/org/springframework/cloud/kubernetes/discovery/KubernetesReactiveDiscoveryClientTests.java new file mode 100644 index 00000000..9f6adf6b --- /dev/null +++ b/spring-cloud-kubernetes-discovery/src/test/java/org/springframework/cloud/kubernetes/discovery/KubernetesReactiveDiscoveryClientTests.java @@ -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 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); + } + +} diff --git a/spring-cloud-kubernetes-integration-tests/run.sh b/spring-cloud-kubernetes-integration-tests/run.sh index 390a31f1..58bb4c7b 100755 --- a/spring-cloud-kubernetes-integration-tests/run.sh +++ b/spring-cloud-kubernetes-integration-tests/run.sh @@ -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 diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-discoverclient-it/k8s/deployment-it.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-discoverclient-it/k8s/deployment-it.yaml new file mode 100644 index 00000000..8a9932a9 --- /dev/null +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-discoverclient-it/k8s/deployment-it.yaml @@ -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: {} diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-discoverclient-it/k8s/service-it.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-discoverclient-it/k8s/service-it.yaml new file mode 100644 index 00000000..518bb63b --- /dev/null +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-discoverclient-it/k8s/service-it.yaml @@ -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: {} diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-discoverclient-it/pom.xml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-discoverclient-it/pom.xml new file mode 100644 index 00000000..03cad216 --- /dev/null +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-discoverclient-it/pom.xml @@ -0,0 +1,143 @@ + + + + spring-cloud-kubernetes-integration-tests + org.springframework.cloud + 2.1.0-SNAPSHOT + + 4.0.0 + + spring-cloud-kubernetes-discoverclient-it + + + 1.8.0 + openjdk:8u222-slim + + + + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.cloud + spring-cloud-starter-kubernetes-discoveryclient + + + org.springframework.boot + spring-boot-starter-actuator + + + org.springframework.cloud + spring-cloud-kubernetes-test-support + + + io.kubernetes + client-java + + + io.kubernetes + client-java-extended + + + com.github.docker-java + docker-java-core + test + + + com.github.docker-java + docker-java-transport-httpclient5 + test + + + + + + + + ../src/main/resources + true + + + src/main/resources + true + + + + + + + skaffold + + + + org.springframework.boot + spring-boot-maven-plugin + + + ${env.IMAGE} + + build-image + + + + package + + build-image + + + + + + + + + imagename + + + !env.IMAGE + + + + springcloud/${project.artifactId}:${project.version} + + + + jib + + + + com.google.cloud.tools + jib-maven-plugin + ${jib.version} + + + ${base.image} + + + spring-cloud/${project.artifactId} + + + nobody:nogroup + + + + + + + package + + dockerBuild + + + + + + + + + + diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-discoverclient-it/skaffold.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-discoverclient-it/skaffold.yaml new file mode 100644 index 00000000..d2dbefb9 --- /dev/null +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-discoverclient-it/skaffold.yaml @@ -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 diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-discoverclient-it/src/main/java/org/springframework/cloud/kubernetes/discoveryclient/it/KubernetesDiscoveryClientApplicationIt.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-discoverclient-it/src/main/java/org/springframework/cloud/kubernetes/discoveryclient/it/KubernetesDiscoveryClientApplicationIt.java new file mode 100644 index 00000000..b176c6a3 --- /dev/null +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-discoverclient-it/src/main/java/org/springframework/cloud/kubernetes/discoveryclient/it/KubernetesDiscoveryClientApplicationIt.java @@ -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 services() { + return discoveryClient.getServices(); + } + + @GetMapping("/service/{serviceId}") + public List service(@PathVariable String serviceId) { + return discoveryClient.getInstances(serviceId); + } + +} diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-discoverclient-it/src/main/resources/application.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-discoverclient-it/src/main/resources/application.yaml new file mode 100644 index 00000000..1b70c818 --- /dev/null +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-discoverclient-it/src/main/resources/application.yaml @@ -0,0 +1,13 @@ +spring: + cloud: + kubernetes: + discovery: + discoveryServerUrl: http://spring-cloud-kubernetes-discoveryserver +management: + endpoint: + health: + show-details: always + endpoints: + web: + exposure: + include: "*" diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-discoverclient-it/src/test/java/org/springframework/cloud/kubernetes/discoveryclient/it/DiscoveryClientIT.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-discoverclient-it/src/test/java/org/springframework/cloud/kubernetes/discoveryclient/it/DiscoveryClientIT.java new file mode 100644 index 00000000..3c39e601 --- /dev/null +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-discoverclient-it/src/test/java/org/springframework/cloud/kubernetes/discoveryclient/it/DiscoveryClientIT.java @@ -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 health = rest.getForObject("http://localhost:80/discoveryclient-it/actuator/health", + Map.class); + Map components = (Map) health.get("components"); + + Map 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; + } + +} diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-discoverclient-it/src/test/resources/spring-cloud-kubernetes-discoveryclient-it-deployment.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-discoverclient-it/src/test/resources/spring-cloud-kubernetes-discoveryclient-it-deployment.yaml new file mode 100644 index 00000000..d4f120a6 --- /dev/null +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-discoverclient-it/src/test/resources/spring-cloud-kubernetes-discoveryclient-it-deployment.yaml @@ -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 diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-discoverclient-it/src/test/resources/spring-cloud-kubernetes-discoveryclient-it-ingress.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-discoverclient-it/src/test/resources/spring-cloud-kubernetes-discoveryclient-it-ingress.yaml new file mode 100644 index 00000000..45aebedf --- /dev/null +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-discoverclient-it/src/test/resources/spring-cloud-kubernetes-discoveryclient-it-ingress.yaml @@ -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 diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-discoverclient-it/src/test/resources/spring-cloud-kubernetes-discoveryclient-it-service.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-discoverclient-it/src/test/resources/spring-cloud-kubernetes-discoveryclient-it-service.yaml new file mode 100644 index 00000000..32b4bd7e --- /dev/null +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-discoverclient-it/src/test/resources/spring-cloud-kubernetes-discoveryclient-it-service.yaml @@ -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 diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-discoverclient-it/src/test/resources/spring-cloud-kubernetes-discoveryserver-deployment.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-discoverclient-it/src/test/resources/spring-cloud-kubernetes-discoveryserver-deployment.yaml new file mode 100644 index 00000000..3672520d --- /dev/null +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-discoverclient-it/src/test/resources/spring-cloud-kubernetes-discoveryserver-deployment.yaml @@ -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 diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-discoverclient-it/src/test/resources/spring-cloud-kubernetes-discoveryserver-ingress.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-discoverclient-it/src/test/resources/spring-cloud-kubernetes-discoveryserver-ingress.yaml new file mode 100644 index 00000000..d58cf1b4 --- /dev/null +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-discoverclient-it/src/test/resources/spring-cloud-kubernetes-discoveryserver-ingress.yaml @@ -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 diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-discoverclient-it/src/test/resources/spring-cloud-kubernetes-discoveryserver-service.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-discoverclient-it/src/test/resources/spring-cloud-kubernetes-discoveryserver-service.yaml new file mode 100644 index 00000000..ba996118 --- /dev/null +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-discoverclient-it/src/test/resources/spring-cloud-kubernetes-discoveryserver-service.yaml @@ -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 diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-reactive-discoveryclient-it/k8s/deployment-it.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-reactive-discoveryclient-it/k8s/deployment-it.yaml new file mode 100644 index 00000000..7e9b54fb --- /dev/null +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-reactive-discoveryclient-it/k8s/deployment-it.yaml @@ -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: {} diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-reactive-discoveryclient-it/k8s/service-it.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-reactive-discoveryclient-it/k8s/service-it.yaml new file mode 100644 index 00000000..5b253f93 --- /dev/null +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-reactive-discoveryclient-it/k8s/service-it.yaml @@ -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: {} diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-reactive-discoveryclient-it/pom.xml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-reactive-discoveryclient-it/pom.xml new file mode 100644 index 00000000..5f4a342c --- /dev/null +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-reactive-discoveryclient-it/pom.xml @@ -0,0 +1,141 @@ + + + + spring-cloud-kubernetes-integration-tests + org.springframework.cloud + 2.1.0-SNAPSHOT + + 4.0.0 + + spring-cloud-kubernetes-reactive-discoveryclient-it + + + 1.8.0 + openjdk:8u222-slim + + + + + org.springframework.boot + spring-boot-starter-webflux + + + org.springframework.cloud + spring-cloud-starter-kubernetes-discoveryclient + + + org.springframework.boot + spring-boot-starter-actuator + + + org.springframework.cloud + spring-cloud-kubernetes-test-support + + + io.kubernetes + client-java + + + io.kubernetes + client-java-extended + + + com.github.docker-java + docker-java-core + test + + + com.github.docker-java + docker-java-transport-httpclient5 + test + + + + + + + ../src/main/resources + true + + + src/main/resources + true + + + + + + + skaffold + + + + org.springframework.boot + spring-boot-maven-plugin + + + ${env.IMAGE} + + build-image + + + + package + + build-image + + + + + + + + + imagename + + + !env.IMAGE + + + + springcloud/${project.artifactId}:${project.version} + + + + jib + + + + com.google.cloud.tools + jib-maven-plugin + ${jib.version} + + + ${base.image} + + + spring-cloud/${project.artifactId} + + + nobody:nogroup + + + + + + + package + + dockerBuild + + + + + + + + + + diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-reactive-discoveryclient-it/skaffold.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-reactive-discoveryclient-it/skaffold.yaml new file mode 100644 index 00000000..599b2ebf --- /dev/null +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-reactive-discoveryclient-it/skaffold.yaml @@ -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 diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-reactive-discoveryclient-it/src/main/java/org/springframework/cloud/kubernetes/reactive/discoveryclient/it/KubernetesReactiveDiscoveryClientApplicationIt.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-reactive-discoveryclient-it/src/main/java/org/springframework/cloud/kubernetes/reactive/discoveryclient/it/KubernetesReactiveDiscoveryClientApplicationIt.java new file mode 100644 index 00000000..c36208f8 --- /dev/null +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-reactive-discoveryclient-it/src/main/java/org/springframework/cloud/kubernetes/reactive/discoveryclient/it/KubernetesReactiveDiscoveryClientApplicationIt.java @@ -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> services() { + return discoveryClient.getServices().collect(Collectors.toList()); + } + + @GetMapping("/service/{serviceId}") + public Flux service(@PathVariable String serviceId) { + return discoveryClient.getInstances(serviceId); + } + +} diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-reactive-discoveryclient-it/src/main/resources/application.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-reactive-discoveryclient-it/src/main/resources/application.yaml new file mode 100644 index 00000000..1b70c818 --- /dev/null +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-reactive-discoveryclient-it/src/main/resources/application.yaml @@ -0,0 +1,13 @@ +spring: + cloud: + kubernetes: + discovery: + discoveryServerUrl: http://spring-cloud-kubernetes-discoveryserver +management: + endpoint: + health: + show-details: always + endpoints: + web: + exposure: + include: "*" diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-reactive-discoveryclient-it/src/test/java/org/springframework/cloud/kubernetes/reactive/discoveryclient/it/ReactiveDiscoveryClientIT.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-reactive-discoveryclient-it/src/test/java/org/springframework/cloud/kubernetes/reactive/discoveryclient/it/ReactiveDiscoveryClientIT.java new file mode 100644 index 00000000..0f74ddae --- /dev/null +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-reactive-discoveryclient-it/src/test/java/org/springframework/cloud/kubernetes/reactive/discoveryclient/it/ReactiveDiscoveryClientIT.java @@ -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 health = rest.getForObject("http://localhost:80/discoveryclient-it/actuator/health", + Map.class); + Map components = (Map) health.get("components"); + + Map 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; + } + +} diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-reactive-discoveryclient-it/src/test/resources/spring-cloud-kubernetes-discoveryclient-it-deployment.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-reactive-discoveryclient-it/src/test/resources/spring-cloud-kubernetes-discoveryclient-it-deployment.yaml new file mode 100644 index 00000000..e42d0c39 --- /dev/null +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-reactive-discoveryclient-it/src/test/resources/spring-cloud-kubernetes-discoveryclient-it-deployment.yaml @@ -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 diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-reactive-discoveryclient-it/src/test/resources/spring-cloud-kubernetes-discoveryclient-it-ingress.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-reactive-discoveryclient-it/src/test/resources/spring-cloud-kubernetes-discoveryclient-it-ingress.yaml new file mode 100644 index 00000000..45aebedf --- /dev/null +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-reactive-discoveryclient-it/src/test/resources/spring-cloud-kubernetes-discoveryclient-it-ingress.yaml @@ -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 diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-reactive-discoveryclient-it/src/test/resources/spring-cloud-kubernetes-discoveryclient-it-service.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-reactive-discoveryclient-it/src/test/resources/spring-cloud-kubernetes-discoveryclient-it-service.yaml new file mode 100644 index 00000000..32b4bd7e --- /dev/null +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-reactive-discoveryclient-it/src/test/resources/spring-cloud-kubernetes-discoveryclient-it-service.yaml @@ -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 diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-reactive-discoveryclient-it/src/test/resources/spring-cloud-kubernetes-discoveryserver-deployment.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-reactive-discoveryclient-it/src/test/resources/spring-cloud-kubernetes-discoveryserver-deployment.yaml new file mode 100644 index 00000000..3672520d --- /dev/null +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-reactive-discoveryclient-it/src/test/resources/spring-cloud-kubernetes-discoveryserver-deployment.yaml @@ -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 diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-reactive-discoveryclient-it/src/test/resources/spring-cloud-kubernetes-discoveryserver-ingress.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-reactive-discoveryclient-it/src/test/resources/spring-cloud-kubernetes-discoveryserver-ingress.yaml new file mode 100644 index 00000000..d58cf1b4 --- /dev/null +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-reactive-discoveryclient-it/src/test/resources/spring-cloud-kubernetes-discoveryserver-ingress.yaml @@ -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 diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-reactive-discoveryclient-it/src/test/resources/spring-cloud-kubernetes-discoveryserver-service.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-reactive-discoveryclient-it/src/test/resources/spring-cloud-kubernetes-discoveryserver-service.yaml new file mode 100644 index 00000000..ba996118 --- /dev/null +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-reactive-discoveryclient-it/src/test/resources/spring-cloud-kubernetes-discoveryserver-service.yaml @@ -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 diff --git a/spring-cloud-starter-kubernetes-discoveryclient/pom.xml b/spring-cloud-starter-kubernetes-discoveryclient/pom.xml new file mode 100644 index 00000000..80fb9bf1 --- /dev/null +++ b/spring-cloud-starter-kubernetes-discoveryclient/pom.xml @@ -0,0 +1,25 @@ + + + + spring-cloud-kubernetes + org.springframework.cloud + 2.1.0-SNAPSHOT + + 4.0.0 + + spring-cloud-starter-kubernetes-discoveryclient + + + + org.springframework.cloud + spring-cloud-commons + + + org.springframework.cloud + spring-cloud-kubernetes-discovery + + + + diff --git a/src/checkstyle/checkstyle-suppressions.xml b/src/checkstyle/checkstyle-suppressions.xml index d2aa4f0d..e3eec7eb 100644 --- a/src/checkstyle/checkstyle-suppressions.xml +++ b/src/checkstyle/checkstyle-suppressions.xml @@ -13,5 +13,6 @@ +