diff --git a/README.adoc b/README.adoc index d9d090f5b4..b1ec7f7a9f 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] @@ -1391,6 +1405,342 @@ spring: ---- ==== +[#spring-cloud-kubernetes-configserver] +## Spring Cloud Kubernetes Config Server + +The Spring Cloud Kubernetes Config Server, is based on https://spring.io/projects/spring-cloud-config[Spring Cloud Config Server] and adds an https://docs.spring.io/spring-cloud-config/docs/current/reference/html/#_environment_repository[environment repository] for Kubernetes +https://kubernetes.io/docs/concepts/configuration/configmap/[Config Maps] and https://kubernetes.io/docs/concepts/configuration/secret/[Secrets]. + +This is component is completely optional. However, it allows you to continue to leverage configuration +you may have stored in existing environment repositories (Git, SVN, Vault, etc) with applications that you are running on Kubernetes. + +A default image is located on Docker Hub which will allow you to easily get a Config Server deployed on Kubernetes without building +the code and image yourself. However, if you need to customize the config server behavior you can easily build your own +image from the source code on GitHub and use that. + +### Configuration + +#### Enabling The Kubernetes Environment Repository +To enable the Kubernetes environment repository the `kubernetes` profile must be included in the list of active profiles. +You may activate other profiles as well to use other environment repository implementations. + +#### Config Map and Secret PropertySources +By default, only Config Map data will be fetched. To enable Secrets as well you will need to set `spring.cloud.kubernetes.secrets.enableApi=true`. +You can disable the Config Map `PropertySource` by setting `spring.cloud.kubernetes.config.enableApi=false`. + +#### Fetching Config Map and Secret Data From Additional Namespaces +By default, the Kubernetes environment repository will only fetch Config Map and Secrets from the namespace in which it is deployed. +If you want to include data from other namespaces you can set `spring.cloud.kubernetes.configserver.config-map-namespaces` and/or `spring.cloud.kubernetes.configserver.secrets-namespaces` to a comma separated +list of namespace values. + +NOTE: If you set `spring.cloud.kubernetes.configserver.config-map-namespaces` and/or `spring.cloud.kubernetes.configserver.secrets-namespaces` +you will need to include the namespace in which the Config Server is deployed in order to continue to fetch Config Map and Secret data from that namespace. + +#### Kubernetes Access Controls +The Kubernetes Config Server uses the Kubernetes API server to fetch Config Map and Secret data. In order for it to do that +it needs ability to `get` and `list` Config Map and Secrets (depending on what you enable/disable). + +### Deployment Yaml + +Below is a sample deployment, service and permissions configuration you can use to deploy a basic Config Server to Kubernetes. + +==== +[source,yaml] +---- +--- +apiVersion: v1 +kind: List +items: + - apiVersion: v1 + kind: Service + metadata: + labels: + app: spring-cloud-kubernetes-configserver + name: spring-cloud-kubernetes-configserver + spec: + ports: + - name: http + port: 8888 + targetPort: 8888 + selector: + app: spring-cloud-kubernetes-configserver + type: ClusterIP + - apiVersion: v1 + kind: ServiceAccount + metadata: + labels: + app: spring-cloud-kubernetes-configserver + name: spring-cloud-kubernetes-configserver + - apiVersion: rbac.authorization.k8s.io/v1 + kind: RoleBinding + metadata: + labels: + app: spring-cloud-kubernetes-configserver + name: spring-cloud-kubernetes-configserver:view + roleRef: + kind: Role + apiGroup: rbac.authorization.k8s.io + name: namespace-reader + subjects: + - kind: ServiceAccount + name: spring-cloud-kubernetes-configserver + - apiVersion: rbac.authorization.k8s.io/v1 + kind: Role + metadata: + namespace: default + name: namespace-reader + rules: + - apiGroups: ["", "extensions", "apps"] + resources: ["configmaps", "secrets"] + verbs: ["get", "list"] + - apiVersion: apps/v1 + kind: Deployment + metadata: + name: spring-cloud-kubernetes-configserver-deployment + spec: + selector: + matchLabels: + app: spring-cloud-kubernetes-configserver + template: + metadata: + labels: + app: spring-cloud-kubernetes-configserver + spec: + serviceAccount: spring-cloud-kubernetes-configserver + containers: + - name: spring-cloud-kubernetes-configserver + image: springcloud/spring-cloud-kubernetes-configserver + imagePullPolicy: IfNotPresent + env: + - name: SPRING_PROFILES_INCLUDE + value: "kubernetes" + readinessProbe: + httpGet: + port: 8888 + path: /actuator/health/readiness + livenessProbe: + httpGet: + port: 8888 + path: /actuator/health/liveness + ports: + - containerPort: 8888 + +---- +==== + +[#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 @@ -1451,23 +1801,9 @@ the `.mvn` configuration, so if you find you have to do it to make a build succeed, please raise a ticket to get the settings added to source control. -For hints on how to build the project look in `.travis.yml` if there -is one. There should be a "script" and maybe "install" command. Also -look at the "services" section to see if any services need to be -running locally (e.g. mongo or rabbit). Ignore the git-related bits -that you might find in "before_install" since they're related to setting git -credentials and you already have those. - -The projects that require middleware generally include a -`docker-compose.yml`, so consider using -https://docs.docker.com/compose/[Docker Compose] to run the middeware servers -in Docker containers. See the README in the -https://github.com/spring-cloud-samples/scripts[scripts demo -repository] for specific instructions about the common cases of mongo, -rabbit and redis. - -NOTE: If all else fails, build with the command from `.travis.yml` (usually -`./mvnw install`). +The projects that require middleware (i.e. Redis) for testing generally +require that a local instance of [Docker](https://www.docker.com/get-started) is installed and running. + === Documentation diff --git a/docs/src/main/asciidoc/discovery-client.adoc b/docs/src/main/asciidoc/discovery-client.adoc index 2a56f3a682..cb49eac9bc 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/property-source-config.adoc b/docs/src/main/asciidoc/property-source-config.adoc index 351aad9d8b..591b252208 100644 --- a/docs/src/main/asciidoc/property-source-config.adoc +++ b/docs/src/main/asciidoc/property-source-config.adoc @@ -353,7 +353,7 @@ Notice that `spring.cloud.kubernetes.config.useNameAsPrefix` has a _lower_ prior This allows you to set a "default" strategy for all sources, at the same time allowing to override only a few. If using the config map name is not an option, you can specify a different strategy, called : `explicitPrefix`. Since this is an _explicit_ prefix that -you select, it can only be supplied to the `sources` level. At the same time it has a higher priority than `useNameASPrefix`. Let's suppose we have a third config map with these entries: +you select, it can only be supplied to the `sources` level. At the same time it has a higher priority than `useNameAsPrefix`. Let's suppose we have a third config map with these entries: ==== @@ -400,6 +400,50 @@ will result in three properties being generated: - `config-map-three.greetings.message` equal to `Say Hello from three`. +By default, besides reading the config map that is specified in the `sources` configuration, Spring will also try to read +all properties from "profile aware" sources. The easiest way to explain this is via an example. Let's suppose your application +enables a profile called "dev" and you have a configuration like the one below: + +==== +[source,yaml] +---- +spring: + application: + name: spring-k8s + cloud: + kubernetes: + config: + namespace: default-namespace + sources: + - name: config-map-one +---- +==== + +Besides reading the `config-map-one`, Spring will also try to read `config-map-one-dev`; in this particular order. Each active profile +generates such a profile aware config map. + +Though your application should not be impacted by such a config map, it can be disabled if needed: + +==== +[source,yaml] +---- +spring: + application: + name: spring-k8s + cloud: + kubernetes: + config: + includeProfileSpecificSources: false + namespace: default-namespace + sources: + - name: config-map-one + includeProfileSpecificSources: false +---- +==== + +Notice that just like before, there are two levels where you can specify this property: for all config maps or +for individual ones; the latter having a higher priority. + NOTE: You should check the security configuration section. To access config maps from inside a pod you need to have the correct Kubernetes service accounts, roles and role bindings. diff --git a/docs/src/main/asciidoc/spring-cloud-kubernetes-configserver.adoc b/docs/src/main/asciidoc/spring-cloud-kubernetes-configserver.adoc new file mode 100644 index 0000000000..88446cbe28 --- /dev/null +++ b/docs/src/main/asciidoc/spring-cloud-kubernetes-configserver.adoc @@ -0,0 +1,122 @@ +[#spring-cloud-kubernetes-configserver] +## Spring Cloud Kubernetes Config Server + +The Spring Cloud Kubernetes Config Server, is based on https://spring.io/projects/spring-cloud-config[Spring Cloud Config Server] and adds an https://docs.spring.io/spring-cloud-config/docs/current/reference/html/#_environment_repository[environment repository] for Kubernetes +https://kubernetes.io/docs/concepts/configuration/configmap/[Config Maps] and https://kubernetes.io/docs/concepts/configuration/secret/[Secrets]. + +This is component is completely optional. However, it allows you to continue to leverage configuration +you may have stored in existing environment repositories (Git, SVN, Vault, etc) with applications that you are running on Kubernetes. + +A default image is located on https://hub.docker.com/r/springcloud/spring-cloud-kubernetes-configserver[Docker Hub] which will allow you to easily get a Config Server deployed on Kubernetes without building +the code and image yourself. However, if you need to customize the config server behavior you can easily build your own +image from the source code on GitHub and use that. + +### Configuration + +#### Enabling The Kubernetes Environment Repository +To enable the Kubernetes environment repository the `kubernetes` profile must be included in the list of active profiles. +You may activate other profiles as well to use other environment repository implementations. + +#### Config Map and Secret PropertySources +By default, only Config Map data will be fetched. To enable Secrets as well you will need to set `spring.cloud.kubernetes.secrets.enableApi=true`. +You can disable the Config Map `PropertySource` by setting `spring.cloud.kubernetes.config.enableApi=false`. + +#### Fetching Config Map and Secret Data From Additional Namespaces +By default, the Kubernetes environment repository will only fetch Config Map and Secrets from the namespace in which it is deployed. +If you want to include data from other namespaces you can set `spring.cloud.kubernetes.configserver.config-map-namespaces` and/or `spring.cloud.kubernetes.configserver.secrets-namespaces` to a comma separated +list of namespace values. + +NOTE: If you set `spring.cloud.kubernetes.configserver.config-map-namespaces` and/or `spring.cloud.kubernetes.configserver.secrets-namespaces` +you will need to include the namespace in which the Config Server is deployed in order to continue to fetch Config Map and Secret data from that namespace. + +#### Kubernetes Access Controls +The Kubernetes Config Server uses the Kubernetes API server to fetch Config Map and Secret data. In order for it to do that +it needs ability to `get` and `list` Config Map and Secrets (depending on what you enable/disable). + +### Deployment Yaml + +Below is a sample deployment, service and permissions configuration you can use to deploy a basic Config Server to Kubernetes. + +==== +[source,yaml] +---- +--- +apiVersion: v1 +kind: List +items: + - apiVersion: v1 + kind: Service + metadata: + labels: + app: spring-cloud-kubernetes-configserver + name: spring-cloud-kubernetes-configserver + spec: + ports: + - name: http + port: 8888 + targetPort: 8888 + selector: + app: spring-cloud-kubernetes-configserver + type: ClusterIP + - apiVersion: v1 + kind: ServiceAccount + metadata: + labels: + app: spring-cloud-kubernetes-configserver + name: spring-cloud-kubernetes-configserver + - apiVersion: rbac.authorization.k8s.io/v1 + kind: RoleBinding + metadata: + labels: + app: spring-cloud-kubernetes-configserver + name: spring-cloud-kubernetes-configserver:view + roleRef: + kind: Role + apiGroup: rbac.authorization.k8s.io + name: namespace-reader + subjects: + - kind: ServiceAccount + name: spring-cloud-kubernetes-configserver + - apiVersion: rbac.authorization.k8s.io/v1 + kind: Role + metadata: + namespace: default + name: namespace-reader + rules: + - apiGroups: ["", "extensions", "apps"] + resources: ["configmaps", "secrets"] + verbs: ["get", "list"] + - apiVersion: apps/v1 + kind: Deployment + metadata: + name: spring-cloud-kubernetes-configserver-deployment + spec: + selector: + matchLabels: + app: spring-cloud-kubernetes-configserver + template: + metadata: + labels: + app: spring-cloud-kubernetes-configserver + spec: + serviceAccount: spring-cloud-kubernetes-configserver + containers: + - name: spring-cloud-kubernetes-configserver + image: springcloud/spring-cloud-kubernetes-configserver + imagePullPolicy: IfNotPresent + env: + - name: SPRING_PROFILES_INCLUDE + value: "kubernetes" + readinessProbe: + httpGet: + port: 8888 + path: /actuator/health/readiness + livenessProbe: + httpGet: + port: 8888 + path: /actuator/health/liveness + ports: + - containerPort: 8888 + +---- +==== 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 0000000000..849351c3eb --- /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 https://hub.docker.com/r/springcloud/spring-cloud-kubernetes-discoveryserver[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 b91696f708..2f24a4409d 100644 --- a/docs/src/main/asciidoc/spring-cloud-kubernetes.adoc +++ b/docs/src/main/asciidoc/spring-cloud-kubernetes.adoc @@ -31,6 +31,10 @@ include::service-registry.adoc[] 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 3d7c4c68ed..cdc939ac0a 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 a5c611462e..c7d1b14e1e 100755 --- a/scripts/deploy.sh +++ b/scripts/deploy.sh @@ -3,3 +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-config/pom.xml b/spring-cloud-kubernetes-client-config/pom.xml index bfae6d99a8..a08c420e10 100644 --- a/spring-cloud-kubernetes-client-config/pom.xml +++ b/spring-cloud-kubernetes-client-config/pom.xml @@ -68,6 +68,7 @@ spring-boot-starter-test test + org.springframework.cloud spring-cloud-kubernetes-test-support @@ -86,6 +87,18 @@ test + + org.springframework.boot + spring-boot-starter-web + test + + + + org.springframework.boot + spring-boot-starter-webflux + test + + org.springframework.retry spring-retry diff --git a/spring-cloud-kubernetes-client-config/src/main/java/org/springframework/cloud/kubernetes/client/config/KubernetesClientConfigMapPropertySource.java b/spring-cloud-kubernetes-client-config/src/main/java/org/springframework/cloud/kubernetes/client/config/KubernetesClientConfigMapPropertySource.java index 6b7b9a95bb..0fdd0513db 100644 --- a/spring-cloud-kubernetes-client-config/src/main/java/org/springframework/cloud/kubernetes/client/config/KubernetesClientConfigMapPropertySource.java +++ b/spring-cloud-kubernetes-client-config/src/main/java/org/springframework/cloud/kubernetes/client/config/KubernetesClientConfigMapPropertySource.java @@ -40,19 +40,26 @@ public class KubernetesClientConfigMapPropertySource extends ConfigMapPropertySo private static final Log LOG = LogFactory.getLog(KubernetesClientConfigMapPropertySource.class); + @Deprecated public KubernetesClientConfigMapPropertySource(CoreV1Api coreV1Api, String name, String namespace, - Environment environment, String prefix, boolean failFast) { - super(getName(name, namespace), getData(coreV1Api, name, namespace, environment, prefix, failFast)); + Environment environment) { + super(getName(name, namespace), getData(coreV1Api, name, namespace, environment, "", true, false)); + } + + public KubernetesClientConfigMapPropertySource(CoreV1Api coreV1Api, String name, String namespace, + Environment environment, String prefix, boolean includeProfileSpecificSources, boolean failFast) { + super(getName(name, namespace), + getData(coreV1Api, name, namespace, environment, prefix, includeProfileSpecificSources, failFast)); } private static Map getData(CoreV1Api coreV1Api, String name, String namespace, - Environment environment, String prefix, boolean failFast) { + Environment environment, String prefix, boolean includeProfileSpecificSources, boolean failFast) { LOG.info("Loading ConfigMap with name '" + name + "' in namespace '" + namespace + "'"); try { Set names = new HashSet<>(); names.add(name); - if (environment != null) { + if (environment != null && includeProfileSpecificSources) { for (String activeProfile : environment.getActiveProfiles()) { names.add(name + "-" + activeProfile); } diff --git a/spring-cloud-kubernetes-client-config/src/main/java/org/springframework/cloud/kubernetes/client/config/KubernetesClientConfigMapPropertySourceLocator.java b/spring-cloud-kubernetes-client-config/src/main/java/org/springframework/cloud/kubernetes/client/config/KubernetesClientConfigMapPropertySourceLocator.java index d9ccde5d09..a35d167751 100644 --- a/spring-cloud-kubernetes-client-config/src/main/java/org/springframework/cloud/kubernetes/client/config/KubernetesClientConfigMapPropertySourceLocator.java +++ b/spring-cloud-kubernetes-client-config/src/main/java/org/springframework/cloud/kubernetes/client/config/KubernetesClientConfigMapPropertySourceLocator.java @@ -89,7 +89,8 @@ else if (kubernetesClientProperties != null) { } return new KubernetesClientConfigMapPropertySource(coreV1Api, name, namespace, environment, - normalizedSource.getPrefix(), this.properties.isFailFast()); + normalizedSource.getPrefix(), normalizedSource.isIncludeProfileSpecificSources(), + this.properties.isFailFast()); } } diff --git a/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/KubernetesClientConfigMapIncludeProfileSpecificSourcesTests.java b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/KubernetesClientConfigMapIncludeProfileSpecificSourcesTests.java new file mode 100644 index 0000000000..12c1d26d9f --- /dev/null +++ b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/KubernetesClientConfigMapIncludeProfileSpecificSourcesTests.java @@ -0,0 +1,120 @@ +/* + * Copyright 2013-2020 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.client.config; + +import com.github.tomakehurst.wiremock.client.WireMock; +import org.hamcrest.Matchers; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.reactive.AutoConfigureWebTestClient; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.cloud.kubernetes.client.config.applications.include_profile_specific_sources.IncludeProfileSpecificSourcesApp; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.junit.jupiter.SpringExtension; +import org.springframework.test.web.reactive.server.WebTestClient; + +/** + * The stub data for this test is in : IncludeProfileSpecificSourcesConfigurationStub + * + * @author wind57 + */ +@ExtendWith(SpringExtension.class) +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, + classes = IncludeProfileSpecificSourcesApp.class, + properties = { "spring.cloud.bootstrap.name=include-profile-specific-sources", + "include.profile.specific.sources=true" }) +@AutoConfigureWebTestClient +@ActiveProfiles("dev") +class KubernetesClientConfigMapIncludeProfileSpecificSourcesTests { + + @Autowired + private WebTestClient webClient; + + @AfterEach + public void afterEach() { + WireMock.reset(); + } + + @AfterAll + public static void afterAll() { + WireMock.shutdownServer(); + } + + /** + *
+	 *   'spring.cloud.kubernetes.config.includeProfileSpecificSources=false'
+	 *   'spring.cloud.kubernetes.config.sources[0].includeProfileSpecificSources=true'
+	 *   'spring.cloud.kubernetes.config.sources[0].name=config-map-one'
+	 *
+	 *   We do not define config-map 'config-map-one', but we do define 'config-map-one-dev'.
+	 *
+	 * 	 As such: @ConfigurationProperties("one") must be resolved from 'config-map-one-dev'
+	 * 
+ */ + @Test + public void testOne() { + this.webClient.get().uri("/profile-specific/one").exchange().expectStatus().isOk().expectBody(String.class) + .value(Matchers.equalTo("one")); + } + + /** + *
+	 *   'spring.cloud.kubernetes.config.includeProfileSpecificSources=false'
+	 *   'spring.cloud.kubernetes.config.sources[1].includeProfileSpecificSources=false'
+	 *   'spring.cloud.kubernetes.config.sources[1].name=config-map-two'
+	 *
+	 *   We define config-map 'config-map-two', but we also define 'config-map-two-dev'.
+	 *   This tests proves that data will be read from 'config-map-two' _only_, even if 'config-map-two-dev'
+	 *   also exists. This happens because of the 'includeProfileSpecificSources=false' property defined at the source level.
+	 *   If this would be incorrect, the value we read from '/profile-specific/two' would have been 'twoDev' and _not_ 'two',
+	 *   simply because 'config-map-two-dev' would override the property value.
+	 *
+	 * 	 As such: @ConfigurationProperties("two") must be resolved from 'config-map-two'
+	 * 
+ */ + @Test + public void testTwo() { + this.webClient.get().uri("/profile-specific/two").exchange().expectStatus().isOk().expectBody(String.class) + .value(Matchers.equalTo("two")); + } + + /** + *
+	 *   'spring.cloud.kubernetes.config.includeProfileSpecificSources=false'
+	 *   'spring.cloud.kubernetes.config.sources[2].name=config-map-three'
+	 *
+	 *   We define config-map 'config-map-three', but we also define 'config-map-three-dev'.
+	 *   This tests proves that data will be read from 'config-map-three' _only_, even if 'config-map-three-dev'
+	 *   also exists. This happens because the 'includeProfileSpecificSources'  property is not defined at the source level,
+	 *   but it is defaulted from the root level, where we set it to false.
+	 *   If this would be incorrect, the value we read from '/profile-specific/three' would have been 'threeDev' and _not_ 'three',
+	 *   simply because 'config-map-three-dev' would override the property value.
+	 *
+	 * 	 As such: @ConfigurationProperties("three") must be resolved from 'config-map-three'
+	 * 
+ */ + @Test + public void testThree() { + this.webClient.get().uri("/profile-specific/three").exchange().expectStatus().isOk().expectBody(String.class) + .value(Matchers.equalTo("three")); + } + +} diff --git a/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/KubernetesClientConfigMapNameAsPrefixTests.java b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/KubernetesClientConfigMapNameAsPrefixTests.java new file mode 100644 index 0000000000..8ad5656ead --- /dev/null +++ b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/KubernetesClientConfigMapNameAsPrefixTests.java @@ -0,0 +1,102 @@ +/* + * Copyright 2013-2020 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.client.config; + +import com.github.tomakehurst.wiremock.client.WireMock; +import org.hamcrest.Matchers; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.reactive.AutoConfigureWebTestClient; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.cloud.kubernetes.client.config.applications.config_map_name_as_prefix.WithPrefixApp; +import org.springframework.test.context.junit.jupiter.SpringExtension; +import org.springframework.test.web.reactive.server.WebTestClient; + +/** + * The stub data for this test is in : ConfigMapNameAsPrefixConfigurationStub + * + * @author wind57 + */ +@ExtendWith(SpringExtension.class) +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, classes = WithPrefixApp.class, + properties = { "spring.cloud.bootstrap.name=config-map-name-as-prefix", "config.map.name.as.prefix.stub=true" }) +@AutoConfigureWebTestClient +public class KubernetesClientConfigMapNameAsPrefixTests { + + @Autowired + private WebTestClient webClient; + + @AfterEach + public void afterEach() { + WireMock.reset(); + } + + @AfterAll + public static void afterAll() { + WireMock.shutdownServer(); + } + + /** + *
+	 *   'spring.cloud.kubernetes.config.useNameAsPrefix=true'
+	 *   'spring.cloud.kubernetes.config.sources[0].useNameAsPrefix=false'
+	 * 	 ("one.property", "one")
+	 *
+	 * 	 As such: @ConfigurationProperties("one")
+	 * 
+ */ + @Test + public void testOne() { + this.webClient.get().uri("/prefix/one").exchange().expectStatus().isOk().expectBody(String.class) + .value(Matchers.equalTo("one")); + } + + /** + *
+	 *   'spring.cloud.kubernetes.config.useNameAsPrefix=true'
+	 *   'spring.cloud.kubernetes.config.sources[1].explicitPrefix=two'
+	 * 	 ("property", "two")
+	 *
+	 * 	 As such: @ConfigurationProperties("two")
+	 * 
+ */ + @Test + public void testTwo() { + this.webClient.get().uri("/prefix/two").exchange().expectStatus().isOk().expectBody(String.class) + .value(Matchers.equalTo("two")); + } + + /** + *
+	 *   'spring.cloud.kubernetes.config.useNameAsPrefix=true'
+	 *   'spring.cloud.kubernetes.config.sources[2].name=config-map-three'
+	 * 	 ("property", "three")
+	 *
+	 * 	 As such: @ConfigurationProperties(prefix = "config-map-three")
+	 * 
+ */ + @Test + public void testThree() { + this.webClient.get().uri("/prefix/three").exchange().expectStatus().isOk().expectBody(String.class) + .value(Matchers.equalTo("three")); + } + +} diff --git a/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/KubernetesClientConfigMapPropertySourceLocatorTests.java b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/KubernetesClientConfigMapPropertySourceLocatorTests.java index f97b8a4f07..1f18984b12 100644 --- a/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/KubernetesClientConfigMapPropertySourceLocatorTests.java +++ b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/KubernetesClientConfigMapPropertySourceLocatorTests.java @@ -65,8 +65,6 @@ class KubernetesClientConfigMapPropertySourceLocatorTests { + "logging.level.org.springframework.cloud.kubernetes=TRACE") .build()); - private static final String API = "/api/v1/namespaces/default/configmaps"; - private static WireMockServer wireMockServer; @BeforeAll @@ -94,7 +92,7 @@ public void afterEach() { @Test void locateWithoutSources() { CoreV1Api api = new CoreV1Api(); - stubFor(get(API) + stubFor(get("/api/v1/namespaces/default/configmaps") .willReturn(aResponse().withStatus(200).withBody(new JSON().serialize(PROPERTIES_CONFIGMAP_LIST)))); ConfigMapConfigProperties configMapConfigProperties = new ConfigMapConfigProperties(); configMapConfigProperties.setName("bootstrap-640"); @@ -109,7 +107,7 @@ void locateWithoutSources() { @Test void locateWithSources() { CoreV1Api api = new CoreV1Api(); - stubFor(get(API) + stubFor(get("/api/v1/namespaces/default/configmaps") .willReturn(aResponse().withStatus(200).withBody(new JSON().serialize(PROPERTIES_CONFIGMAP_LIST)))); ConfigMapConfigProperties configMapConfigProperties = new ConfigMapConfigProperties(); configMapConfigProperties.setName("fake-name"); @@ -137,7 +135,7 @@ void locateWithSources() { @Test void testLocateWithoutNamespaceDeprecatedConstructor() { CoreV1Api api = new CoreV1Api(); - stubFor(get(API) + stubFor(get("/api/v1/namespaces/default/configmaps") .willReturn(aResponse().withStatus(200).withBody(new JSON().serialize(PROPERTIES_CONFIGMAP_LIST)))); ConfigMapConfigProperties configMapConfigProperties = new ConfigMapConfigProperties(); configMapConfigProperties.setName("bootstrap-640"); @@ -159,7 +157,7 @@ void testLocateWithoutNamespaceDeprecatedConstructor() { @Test void testLocateWithoutNamespace() { CoreV1Api api = new CoreV1Api(); - stubFor(get(API) + stubFor(get("/api/v1/namespaces/default/configmaps") .willReturn(aResponse().withStatus(200).withBody(new JSON().serialize(PROPERTIES_CONFIGMAP_LIST)))); ConfigMapConfigProperties configMapConfigProperties = new ConfigMapConfigProperties(); configMapConfigProperties.setName("bootstrap-640"); @@ -173,7 +171,8 @@ void testLocateWithoutNamespace() { @Test public void locateShouldThrowExceptionOnFailureWhenFailFastIsEnabled() { CoreV1Api api = new CoreV1Api(); - stubFor(get(API).willReturn(aResponse().withStatus(500).withBody("Internal Server Error"))); + stubFor(get("/api/v1/namespaces/default/configmaps") + .willReturn(aResponse().withStatus(500).withBody("Internal Server Error"))); ConfigMapConfigProperties configMapConfigProperties = new ConfigMapConfigProperties(); configMapConfigProperties.setName("bootstrap-640"); @@ -190,7 +189,8 @@ public void locateShouldThrowExceptionOnFailureWhenFailFastIsEnabled() { @Test public void locateShouldNotThrowExceptionOnFailureWhenFailFastIsDisabled() { CoreV1Api api = new CoreV1Api(); - stubFor(get(API).willReturn(aResponse().withStatus(500).withBody("Internal Server Error"))); + stubFor(get("/api/v1/namespaces/default/configmaps") + .willReturn(aResponse().withStatus(500).withBody("Internal Server Error"))); ConfigMapConfigProperties configMapConfigProperties = new ConfigMapConfigProperties(); configMapConfigProperties.setName("bootstrap-640"); diff --git a/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/KubernetesClientConfigMapPropertySourceTests.java b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/KubernetesClientConfigMapPropertySourceTests.java index b582a23658..1bbd563eb7 100644 --- a/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/KubernetesClientConfigMapPropertySourceTests.java +++ b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/KubernetesClientConfigMapPropertySourceTests.java @@ -68,8 +68,6 @@ class KubernetesClientConfigMapPropertySourceTests { "dummy:\n property:\n string2: \"a\"\n int2: 1\n bool2: true\n") .build()); - private static final String API = "/api/v1/namespaces/default/configmaps"; - private static WireMockServer wireMockServer; @BeforeAll @@ -97,11 +95,11 @@ public void afterEach() { @Test public void propertiesFile() { CoreV1Api api = new CoreV1Api(); - stubFor(get(API) + stubFor(get("/api/v1/namespaces/default/configmaps") .willReturn(aResponse().withStatus(200).withBody(new JSON().serialize(PROPERTIES_CONFIGMAP_LIST)))); KubernetesClientConfigMapPropertySource propertySource = new KubernetesClientConfigMapPropertySource(api, - "bootstrap-640", "default", new MockEnvironment(), "", false); - verify(getRequestedFor(urlEqualTo(API))); + "bootstrap-640", "default", new MockEnvironment(), "", true, false); + verify(getRequestedFor(urlEqualTo("/api/v1/namespaces/default/configmaps"))); assertThat(propertySource.containsProperty("spring.cloud.kubernetes.configuration.watcher.refreshDelay")) .isTrue(); assertThat(propertySource.getProperty("spring.cloud.kubernetes.configuration.watcher.refreshDelay")) @@ -114,10 +112,11 @@ public void propertiesFile() { @Test public void yamlFile() { CoreV1Api api = new CoreV1Api(); - stubFor(get(API).willReturn(aResponse().withStatus(200).withBody(new JSON().serialize(YAML_CONFIGMAP_LIST)))); + stubFor(get("/api/v1/namespaces/default/configmaps") + .willReturn(aResponse().withStatus(200).withBody(new JSON().serialize(YAML_CONFIGMAP_LIST)))); KubernetesClientConfigMapPropertySource propertySource = new KubernetesClientConfigMapPropertySource(api, - "bootstrap-641", "default", new MockEnvironment(), "", false); - verify(getRequestedFor(urlEqualTo(API))); + "bootstrap-641", "default", new MockEnvironment(), "", true, false); + verify(getRequestedFor(urlEqualTo("/api/v1/namespaces/default/configmaps"))); assertThat(propertySource.containsProperty("dummy.property.string2")).isTrue(); assertThat(propertySource.getProperty("dummy.property.string2")).isEqualTo("a"); assertThat(propertySource.containsProperty("dummy.property.int2")).isTrue(); @@ -130,11 +129,11 @@ public void yamlFile() { @Test public void propertiesFileWithPrefix() { CoreV1Api api = new CoreV1Api(); - stubFor(get(API) + stubFor(get("/api/v1/namespaces/default/configmaps") .willReturn(aResponse().withStatus(200).withBody(new JSON().serialize(PROPERTIES_CONFIGMAP_LIST)))); KubernetesClientConfigMapPropertySource propertySource = new KubernetesClientConfigMapPropertySource(api, - "bootstrap-640", "default", new MockEnvironment(), "prefix", false); - verify(getRequestedFor(urlEqualTo(API))); + "bootstrap-640", "default", new MockEnvironment(), "prefix", true, false); + verify(getRequestedFor(urlEqualTo("/api/v1/namespaces/default/configmaps"))); assertThat(propertySource.containsProperty("prefix.spring.cloud.kubernetes.configuration.watcher.refreshDelay")) .isTrue(); assertThat(propertySource.getProperty("prefix.spring.cloud.kubernetes.configuration.watcher.refreshDelay")) @@ -148,22 +147,24 @@ public void propertiesFileWithPrefix() { @Test public void constructorShouldThrowExceptionOnFailureWhenFailFastIsEnabled() { CoreV1Api api = new CoreV1Api(); - stubFor(get(API).willReturn(aResponse().withStatus(500).withBody("Internal Server Error"))); + stubFor(get("/api/v1/namespaces/default/configmaps") + .willReturn(aResponse().withStatus(500).withBody("Internal Server Error"))); assertThatThrownBy(() -> new KubernetesClientConfigMapPropertySource(api, "my-config", "default", - new MockEnvironment(), "", true)).isInstanceOf(IllegalStateException.class) + new MockEnvironment(), "", false, true)).isInstanceOf(IllegalStateException.class) .hasMessage("Unable to read ConfigMap with name 'my-config' in namespace 'default'"); - verify(getRequestedFor(urlEqualTo(API))); + verify(getRequestedFor(urlEqualTo("/api/v1/namespaces/default/configmaps"))); } @Test public void constructorShouldNotThrowExceptionOnFailureWhenFailFastIsDisabled() { CoreV1Api api = new CoreV1Api(); - stubFor(get(API).willReturn(aResponse().withStatus(500).withBody("Internal Server Error"))); + stubFor(get("/api/v1/namespaces/default/configmaps") + .willReturn(aResponse().withStatus(500).withBody("Internal Server Error"))); assertThatNoException().isThrownBy((() -> new KubernetesClientConfigMapPropertySource(api, "my-config", - "default", new MockEnvironment(), "", false))); - verify(getRequestedFor(urlEqualTo(API))); + "default", new MockEnvironment(), "", false, false))); + verify(getRequestedFor(urlEqualTo("/api/v1/namespaces/default/configmaps"))); } } diff --git a/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/applications/config_map_name_as_prefix/WithPrefixApp.java b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/applications/config_map_name_as_prefix/WithPrefixApp.java new file mode 100644 index 0000000000..0bdbb15a00 --- /dev/null +++ b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/applications/config_map_name_as_prefix/WithPrefixApp.java @@ -0,0 +1,34 @@ +/* + * 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.client.config.applications.config_map_name_as_prefix; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.cloud.kubernetes.client.config.applications.config_map_name_as_prefix.properties.One; +import org.springframework.cloud.kubernetes.client.config.applications.config_map_name_as_prefix.properties.Three; +import org.springframework.cloud.kubernetes.client.config.applications.config_map_name_as_prefix.properties.Two; + +@SpringBootApplication +@EnableConfigurationProperties({ One.class, Two.class, Three.class }) +public class WithPrefixApp { + + public static void main(String[] args) { + SpringApplication.run(WithPrefixApp.class, args); + } + +} diff --git a/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/applications/config_map_name_as_prefix/controller/Controller.java b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/applications/config_map_name_as_prefix/controller/Controller.java new file mode 100644 index 0000000000..e4d655780c --- /dev/null +++ b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/applications/config_map_name_as_prefix/controller/Controller.java @@ -0,0 +1,55 @@ +/* + * Copyright 2013-2020 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.client.config.applications.config_map_name_as_prefix.controller; + +import org.springframework.cloud.kubernetes.client.config.applications.config_map_name_as_prefix.properties.One; +import org.springframework.cloud.kubernetes.client.config.applications.config_map_name_as_prefix.properties.Three; +import org.springframework.cloud.kubernetes.client.config.applications.config_map_name_as_prefix.properties.Two; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +public class Controller { + + private final One one; + + private final Two two; + + private final Three three; + + public Controller(One one, Two two, Three three) { + this.one = one; + this.two = two; + this.three = three; + } + + @GetMapping("/prefix/one") + public String one() { + return one.getProperty(); + } + + @GetMapping("/prefix/two") + public String two() { + return two.getProperty(); + } + + @GetMapping("/prefix/three") + public String three() { + return three.getProperty(); + } + +} diff --git a/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/applications/config_map_name_as_prefix/properties/One.java b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/applications/config_map_name_as_prefix/properties/One.java new file mode 100644 index 0000000000..4a9eb5e436 --- /dev/null +++ b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/applications/config_map_name_as_prefix/properties/One.java @@ -0,0 +1,34 @@ +/* + * 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.client.config.applications.config_map_name_as_prefix.properties; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +@ConfigurationProperties("one") +public class One { + + private String property; + + public String getProperty() { + return property; + } + + public void setProperty(String property) { + this.property = property; + } + +} diff --git a/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/applications/config_map_name_as_prefix/properties/Three.java b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/applications/config_map_name_as_prefix/properties/Three.java new file mode 100644 index 0000000000..ae83c9a944 --- /dev/null +++ b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/applications/config_map_name_as_prefix/properties/Three.java @@ -0,0 +1,34 @@ +/* + * 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.client.config.applications.config_map_name_as_prefix.properties; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +@ConfigurationProperties(prefix = "config-map-three") +public class Three { + + private String property; + + public String getProperty() { + return property; + } + + public void setProperty(String property) { + this.property = property; + } + +} diff --git a/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/applications/config_map_name_as_prefix/properties/Two.java b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/applications/config_map_name_as_prefix/properties/Two.java new file mode 100644 index 0000000000..7a3d9d8dc7 --- /dev/null +++ b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/applications/config_map_name_as_prefix/properties/Two.java @@ -0,0 +1,34 @@ +/* + * 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.client.config.applications.config_map_name_as_prefix.properties; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +@ConfigurationProperties("two") +public class Two { + + private String property; + + public String getProperty() { + return property; + } + + public void setProperty(String property) { + this.property = property; + } + +} diff --git a/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/applications/include_profile_specific_sources/IncludeProfileSpecificSourcesApp.java b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/applications/include_profile_specific_sources/IncludeProfileSpecificSourcesApp.java new file mode 100644 index 0000000000..d719c4220e --- /dev/null +++ b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/applications/include_profile_specific_sources/IncludeProfileSpecificSourcesApp.java @@ -0,0 +1,34 @@ +/* + * 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.client.config.applications.include_profile_specific_sources; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.cloud.kubernetes.client.config.applications.include_profile_specific_sources.properties.One; +import org.springframework.cloud.kubernetes.client.config.applications.include_profile_specific_sources.properties.Three; +import org.springframework.cloud.kubernetes.client.config.applications.include_profile_specific_sources.properties.Two; + +@SpringBootApplication +@EnableConfigurationProperties({ One.class, Two.class, Three.class }) +public class IncludeProfileSpecificSourcesApp { + + public static void main(String[] args) { + SpringApplication.run(IncludeProfileSpecificSourcesApp.class, args); + } + +} diff --git a/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/applications/include_profile_specific_sources/controller/IncludeProfileSpecificSourcesController.java b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/applications/include_profile_specific_sources/controller/IncludeProfileSpecificSourcesController.java new file mode 100644 index 0000000000..fd8e4db04d --- /dev/null +++ b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/applications/include_profile_specific_sources/controller/IncludeProfileSpecificSourcesController.java @@ -0,0 +1,55 @@ +/* + * 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.client.config.applications.include_profile_specific_sources.controller; + +import org.springframework.cloud.kubernetes.client.config.applications.include_profile_specific_sources.properties.One; +import org.springframework.cloud.kubernetes.client.config.applications.include_profile_specific_sources.properties.Three; +import org.springframework.cloud.kubernetes.client.config.applications.include_profile_specific_sources.properties.Two; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +public class IncludeProfileSpecificSourcesController { + + private final One one; + + private final Two two; + + private final Three three; + + public IncludeProfileSpecificSourcesController(One one, Two two, Three three) { + this.one = one; + this.two = two; + this.three = three; + } + + @GetMapping("/profile-specific/one") + public String one() { + return one.getProperty(); + } + + @GetMapping("/profile-specific/two") + public String two() { + return two.getProperty(); + } + + @GetMapping("/profile-specific/three") + public String three() { + return three.getProperty(); + } + +} diff --git a/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/applications/include_profile_specific_sources/properties/One.java b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/applications/include_profile_specific_sources/properties/One.java new file mode 100644 index 0000000000..1e59b034ed --- /dev/null +++ b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/applications/include_profile_specific_sources/properties/One.java @@ -0,0 +1,34 @@ +/* + * 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.client.config.applications.include_profile_specific_sources.properties; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +@ConfigurationProperties("one") +public class One { + + private String property; + + public String getProperty() { + return property; + } + + public void setProperty(String property) { + this.property = property; + } + +} diff --git a/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/applications/include_profile_specific_sources/properties/Three.java b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/applications/include_profile_specific_sources/properties/Three.java new file mode 100644 index 0000000000..e96da1ebd6 --- /dev/null +++ b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/applications/include_profile_specific_sources/properties/Three.java @@ -0,0 +1,34 @@ +/* + * 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.client.config.applications.include_profile_specific_sources.properties; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +@ConfigurationProperties("three") +public class Three { + + private String property; + + public String getProperty() { + return property; + } + + public void setProperty(String property) { + this.property = property; + } + +} diff --git a/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/applications/include_profile_specific_sources/properties/Two.java b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/applications/include_profile_specific_sources/properties/Two.java new file mode 100644 index 0000000000..4dde04d3a1 --- /dev/null +++ b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/applications/include_profile_specific_sources/properties/Two.java @@ -0,0 +1,34 @@ +/* + * 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.client.config.applications.include_profile_specific_sources.properties; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +@ConfigurationProperties("two") +public class Two { + + private String property; + + public String getProperty() { + return property; + } + + public void setProperty(String property) { + this.property = property; + } + +} diff --git a/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/boostrap/stubs/ConfigMapNameAsPrefixConfigurationStub.java b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/boostrap/stubs/ConfigMapNameAsPrefixConfigurationStub.java new file mode 100644 index 0000000000..a639d71378 --- /dev/null +++ b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/boostrap/stubs/ConfigMapNameAsPrefixConfigurationStub.java @@ -0,0 +1,91 @@ +/* + * Copyright 2013-2020 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.client.config.boostrap.stubs; + +import java.util.Arrays; +import java.util.Collections; + +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.V1ConfigMap; +import io.kubernetes.client.openapi.models.V1ConfigMapBuilder; +import io.kubernetes.client.openapi.models.V1ConfigMapList; +import io.kubernetes.client.openapi.models.V1ObjectMetaBuilder; +import io.kubernetes.client.util.ClientBuilder; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.annotation.Order; + +import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.options; + +/** + * A test bootstrap that takes care to initialize ApiClient _before_ our main bootstrap + * context; with some stub data already present. + * + * @author wind57 + */ +@Order(0) +@Configuration +@ConditionalOnProperty("config.map.name.as.prefix.stub") +public class ConfigMapNameAsPrefixConfigurationStub { + + @Bean + public WireMockServer wireMock() { + WireMockServer server = new WireMockServer(options().dynamicPort()); + server.start(); + WireMock.configureFor("localhost", server.port()); + return server; + } + + @Bean + public ApiClient apiClient(WireMockServer wireMockServer) { + ApiClient apiClient = new ClientBuilder().setBasePath("http://localhost:" + wireMockServer.port()).build(); + io.kubernetes.client.openapi.Configuration.setDefaultApiClient(apiClient); + apiClient.setDebugging(true); + stubData(); + return apiClient; + } + + private void stubData() { + V1ConfigMap one = new V1ConfigMapBuilder() + .withMetadata(new V1ObjectMetaBuilder().withName("config-map-one").withNamespace("spring-k8s") + .withResourceVersion("1").build()) + .addToData(Collections.singletonMap("one.property", "one")).build(); + + V1ConfigMap two = new V1ConfigMapBuilder() + .withMetadata(new V1ObjectMetaBuilder().withName("config-map-two").withNamespace("spring-k8s") + .withResourceVersion("1").build()) + .addToData(Collections.singletonMap("property", "two")).build(); + + V1ConfigMap three = new V1ConfigMapBuilder() + .withMetadata(new V1ObjectMetaBuilder().withName("config-map-three").withNamespace("spring-k8s") + .withResourceVersion("1").build()) + .addToData(Collections.singletonMap("property", "three")).build(); + + V1ConfigMapList allConfigMaps = new V1ConfigMapList(); + allConfigMaps.setItems(Arrays.asList(one, two, three)); + + // the actual stub for CoreV1Api calls + WireMock.stubFor(WireMock.get("/api/v1/namespaces/spring-k8s/configmaps") + .willReturn(WireMock.aResponse().withStatus(200).withBody(new JSON().serialize(allConfigMaps)))); + } + +} diff --git a/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/boostrap/stubs/IncludeProfileSpecificSourcesConfigurationStub.java b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/boostrap/stubs/IncludeProfileSpecificSourcesConfigurationStub.java new file mode 100644 index 0000000000..ee37d971c0 --- /dev/null +++ b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/boostrap/stubs/IncludeProfileSpecificSourcesConfigurationStub.java @@ -0,0 +1,101 @@ +/* + * Copyright 2013-2020 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.client.config.boostrap.stubs; + +import java.util.Arrays; +import java.util.Collections; + +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.V1ConfigMap; +import io.kubernetes.client.openapi.models.V1ConfigMapBuilder; +import io.kubernetes.client.openapi.models.V1ConfigMapList; +import io.kubernetes.client.openapi.models.V1ObjectMetaBuilder; +import io.kubernetes.client.util.ClientBuilder; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.annotation.Order; + +import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.options; + +/** + * A test bootstrap that takes care to initialize ApiClient _before_ our main bootstrap + * context; with some stub data already. + * + * @author wind57 + */ +@Order(0) +@Configuration +@ConditionalOnProperty("include.profile.specific.sources") +class IncludeProfileSpecificSourcesConfigurationStub { + + @Bean + public WireMockServer wireMock() { + WireMockServer server = new WireMockServer(options().dynamicPort()); + server.start(); + WireMock.configureFor("localhost", server.port()); + return server; + } + + @Bean + public ApiClient apiClient(WireMockServer wireMockServer) { + ApiClient apiClient = new ClientBuilder().setBasePath("http://localhost:" + wireMockServer.port()).build(); + io.kubernetes.client.openapi.Configuration.setDefaultApiClient(apiClient); + apiClient.setDebugging(true); + stubData(); + return apiClient; + } + + private void stubData() { + V1ConfigMap one = new V1ConfigMapBuilder() + .withMetadata(new V1ObjectMetaBuilder().withName("config-map-one-dev").withNamespace("spring-k8s") + .withResourceVersion("1").build()) + .addToData(Collections.singletonMap("one.property", "one")).build(); + + V1ConfigMap two = new V1ConfigMapBuilder() + .withMetadata(new V1ObjectMetaBuilder().withName("config-map-two").withNamespace("spring-k8s") + .withResourceVersion("1").build()) + .addToData(Collections.singletonMap("two.property", "two")).build(); + + V1ConfigMap twoDev = new V1ConfigMapBuilder() + .withMetadata(new V1ObjectMetaBuilder().withName("config-map-two-dev").withNamespace("spring-k8s") + .withResourceVersion("1").build()) + .addToData(Collections.singletonMap("two.property", "twoDev")).build(); + + V1ConfigMap three = new V1ConfigMapBuilder() + .withMetadata(new V1ObjectMetaBuilder().withName("config-map-three").withNamespace("spring-k8s") + .withResourceVersion("1").build()) + .addToData(Collections.singletonMap("three.property", "three")).build(); + + V1ConfigMap threeDev = new V1ConfigMapBuilder() + .withMetadata(new V1ObjectMetaBuilder().withName("config-map-three-dev").withNamespace("spring-k8s") + .withResourceVersion("1").build()) + .addToData(Collections.singletonMap("three.property", "threeDev")).build(); + + V1ConfigMapList allConfigMaps = new V1ConfigMapList(); + allConfigMaps.setItems(Arrays.asList(one, two, twoDev, three, threeDev)); + + // the actual stub for CoreV1Api calls + WireMock.stubFor(WireMock.get("/api/v1/namespaces/spring-k8s/configmaps") + .willReturn(WireMock.aResponse().withStatus(200).withBody(new JSON().serialize(allConfigMaps)))); + } + +} diff --git a/spring-cloud-kubernetes-client-config/src/test/resources/META-INF/spring.factories b/spring-cloud-kubernetes-client-config/src/test/resources/META-INF/spring.factories new file mode 100644 index 0000000000..5d0b2c08a6 --- /dev/null +++ b/spring-cloud-kubernetes-client-config/src/test/resources/META-INF/spring.factories @@ -0,0 +1,3 @@ +org.springframework.cloud.bootstrap.BootstrapConfiguration=\ +org.springframework.cloud.kubernetes.client.config.boostrap.stubs.IncludeProfileSpecificSourcesConfigurationStub, \ +org.springframework.cloud.kubernetes.client.config.boostrap.stubs.ConfigMapNameAsPrefixConfigurationStub diff --git a/spring-cloud-kubernetes-fabric8-config/src/test/resources/same-key-with-prefix.yaml b/spring-cloud-kubernetes-client-config/src/test/resources/config-map-name-as-prefix.yaml similarity index 100% rename from spring-cloud-kubernetes-fabric8-config/src/test/resources/same-key-with-prefix.yaml rename to spring-cloud-kubernetes-client-config/src/test/resources/config-map-name-as-prefix.yaml diff --git a/spring-cloud-kubernetes-client-config/src/test/resources/include-profile-specific-sources.yaml b/spring-cloud-kubernetes-client-config/src/test/resources/include-profile-specific-sources.yaml new file mode 100644 index 0000000000..54999aaa3a --- /dev/null +++ b/spring-cloud-kubernetes-client-config/src/test/resources/include-profile-specific-sources.yaml @@ -0,0 +1,14 @@ +spring: + application: + name: include-profile-specific-sources + cloud: + kubernetes: + config: + includeProfileSpecificSources: false + namespace: spring-k8s + sources: + - name: config-map-one + includeProfileSpecificSources: true + - name: config-map-two + includeProfileSpecificSources: false + - name: config-map-three diff --git a/spring-cloud-kubernetes-client-discovery/src/main/java/org/springframework/cloud/kubernetes/client/discovery/KubernetesDiscoveryClientAutoConfiguration.java b/spring-cloud-kubernetes-client-discovery/src/main/java/org/springframework/cloud/kubernetes/client/discovery/KubernetesDiscoveryClientAutoConfiguration.java index 826e7e23b3..263e1d739b 100644 --- a/spring-cloud-kubernetes-client-discovery/src/main/java/org/springframework/cloud/kubernetes/client/discovery/KubernetesDiscoveryClientAutoConfiguration.java +++ b/spring-cloud-kubernetes-client-discovery/src/main/java/org/springframework/cloud/kubernetes/client/discovery/KubernetesDiscoveryClientAutoConfiguration.java @@ -48,6 +48,7 @@ import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.core.env.Environment; @Configuration(proxyBeanMethods = false) @ConditionalOnKubernetesDiscoveryEnabled @@ -80,11 +81,15 @@ public static class KubernetesInformerDiscoveryConfiguration { @Bean @ConditionalOnMissingBean public SpringCloudKubernetesInformerFactoryProcessor discoveryInformerConfigurer( - KubernetesNamespaceProvider kubernetesNamespaceProvider, - KubernetesDiscoveryProperties kubernetesDiscoveryProperties, ApiClient apiClient, - CatalogSharedInformerFactory sharedInformerFactory) { - return new SpringCloudKubernetesInformerFactoryProcessor(kubernetesDiscoveryProperties, - kubernetesNamespaceProvider, apiClient, sharedInformerFactory); + KubernetesNamespaceProvider kubernetesNamespaceProvider, ApiClient apiClient, + CatalogSharedInformerFactory sharedInformerFactory, Environment environment) { + // Injecting KubernetesDiscoveryProperties here would cause it to be + // initialize too early + // Instead get the all-namespaces property value from the Environment directly + boolean allNamespaces = environment.getProperty("spring.cloud.kubernetes.discovery.all-namespaces", + Boolean.class, false); + return new SpringCloudKubernetesInformerFactoryProcessor(kubernetesNamespaceProvider, apiClient, + sharedInformerFactory, allNamespaces); } @Bean 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 23c4afbc38..32a7d7f41b 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 @@ -102,7 +102,7 @@ public List getInstances(String serviceId) { V1Service service = properties.isAllNamespaces() ? this.serviceLister.list().stream() .filter(svc -> serviceId.equals(svc.getMetadata().getName())).findFirst().orElse(null) : this.serviceLister.namespace(this.namespace).get(serviceId); - if (service == null) { + if (service == null || !matchServiceLabels(service)) { // no such service present in the cluster return new ArrayList<>(); } @@ -163,7 +163,8 @@ public List getInstances(String serviceId) { 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()); } @@ -205,8 +206,8 @@ private int findEndpointPort(List endpointPorts, String primaryP public List getServices() { List services = this.properties.isAllNamespaces() ? this.serviceLister.list() : this.serviceLister.namespace(this.namespace).list(); - return services.stream().filter(s -> s.getMetadata() != null) // safeguard - .map(s -> s.getMetadata().getName()).collect(Collectors.toList()); + return services.stream().filter(this::matchServiceLabels).map(s -> s.getMetadata().getName()) + .collect(Collectors.toList()); } @Override @@ -230,4 +231,28 @@ public void afterPropertiesSet() throws Exception { + " services) , discovery client is now available"); } + private boolean matchServiceLabels(V1Service service) { + if (log.isDebugEnabled()) { + log.debug("Kubernetes Service Label Properties:"); + if (this.properties.getServiceLabels() != null) { + this.properties.getServiceLabels().forEach((key, value) -> log.debug(key + ":" + value)); + } + log.debug("Service " + service.getMetadata().getName() + " labels:"); + if (service.getMetadata() != null && service.getMetadata().getLabels() != null) { + service.getMetadata().getLabels().forEach((key, value) -> log.debug(key + ":" + value)); + } + } + // safeguard + if (service.getMetadata() == null) { + return false; + } + if (properties.getServiceLabels() == null || properties.getServiceLabels().isEmpty()) { + return true; + } + return properties.getServiceLabels().keySet().stream() + .allMatch(k -> service.getMetadata().getLabels() != null + && service.getMetadata().getLabels().containsKey(k) + && service.getMetadata().getLabels().get(k).equals(properties.getServiceLabels().get(k))); + } + } diff --git a/spring-cloud-kubernetes-client-discovery/src/main/java/org/springframework/cloud/kubernetes/client/discovery/SpringCloudKubernetesInformerFactoryProcessor.java b/spring-cloud-kubernetes-client-discovery/src/main/java/org/springframework/cloud/kubernetes/client/discovery/SpringCloudKubernetesInformerFactoryProcessor.java index c3a5ecf303..ef4d574cef 100644 --- a/spring-cloud-kubernetes-client-discovery/src/main/java/org/springframework/cloud/kubernetes/client/discovery/SpringCloudKubernetesInformerFactoryProcessor.java +++ b/spring-cloud-kubernetes-client-discovery/src/main/java/org/springframework/cloud/kubernetes/client/discovery/SpringCloudKubernetesInformerFactoryProcessor.java @@ -38,7 +38,6 @@ import org.springframework.beans.factory.support.BeanDefinitionRegistry; import org.springframework.beans.factory.support.RootBeanDefinition; import org.springframework.cloud.kubernetes.commons.KubernetesNamespaceProvider; -import org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscoveryProperties; import org.springframework.core.ResolvableType; /** @@ -54,26 +53,24 @@ class SpringCloudKubernetesInformerFactoryProcessor extends KubernetesInformerFa private final SharedInformerFactory sharedInformerFactory; - private final KubernetesDiscoveryProperties kubernetesDiscoveryProperties; + private final boolean allNamespaces; private final KubernetesNamespaceProvider kubernetesNamespaceProvider; @Autowired - SpringCloudKubernetesInformerFactoryProcessor(KubernetesDiscoveryProperties kubernetesDiscoveryProperties, - KubernetesNamespaceProvider kubernetesNamespaceProvider, ApiClient apiClient, - SharedInformerFactory sharedInformerFactory) { + SpringCloudKubernetesInformerFactoryProcessor(KubernetesNamespaceProvider kubernetesNamespaceProvider, + ApiClient apiClient, SharedInformerFactory sharedInformerFactory, boolean allNamespaces) { super(); this.apiClient = apiClient; this.sharedInformerFactory = sharedInformerFactory; this.kubernetesNamespaceProvider = kubernetesNamespaceProvider; - this.kubernetesDiscoveryProperties = kubernetesDiscoveryProperties; + this.allNamespaces = allNamespaces; } @Override public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException { - String namespace = kubernetesDiscoveryProperties.isAllNamespaces() ? Namespaces.NAMESPACE_ALL - : kubernetesNamespaceProvider.getNamespace() == null ? Namespaces.NAMESPACE_DEFAULT - : kubernetesNamespaceProvider.getNamespace(); + String namespace = allNamespaces ? Namespaces.NAMESPACE_ALL : kubernetesNamespaceProvider.getNamespace() == null + ? Namespaces.NAMESPACE_DEFAULT : kubernetesNamespaceProvider.getNamespace(); this.apiClient.setHttpClient(this.apiClient.getHttpClient().newBuilder().readTimeout(Duration.ZERO).build()); 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 c6adf38483..c435047b5c 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 @@ -17,6 +17,7 @@ package org.springframework.cloud.kubernetes.client.discovery; import java.util.HashMap; +import java.util.Map; import io.kubernetes.client.informer.SharedInformerFactory; import io.kubernetes.client.informer.cache.Cache; @@ -59,6 +60,11 @@ public class KubernetesInformerDiscoveryClientTests { .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 testEndpoints1 = new V1Endpoints() .metadata(new V1ObjectMeta().name("test-svc-1").namespace("namespace1")) .addSubsetsItem(new V1EndpointSubset().addPortsItem(new V1EndpointPort().port(8080)) @@ -91,6 +97,11 @@ public class KubernetesInformerDiscoveryClientTests { .addPortsItem(new V1EndpointPort().name("tcp2").port(443)) .addAddressesItem(new V1EndpointAddress().ip("1.1.1.1"))); + private static final V1Endpoints testEndpoints3 = new V1Endpoints() + .metadata(new V1ObjectMeta().name("test-svc-3").namespace("namespace1")) + .addSubsetsItem(new V1EndpointSubset().addPortsItem(new V1EndpointPort().port(8080)) + .addAddressesItem(new V1EndpointAddress().ip("2.2.2.2"))); + @Test public void testDiscoveryGetServicesAllNamespaceShouldWork() { Lister serviceLister = setupServiceLister(testService1, testService2); @@ -106,6 +117,44 @@ public void testDiscoveryGetServicesAllNamespaceShouldWork() { verify(kubernetesDiscoveryProperties, times(1)).isAllNamespaces(); } + @Test + public void testDiscoveryWithServiceLabels() { + Lister serviceLister = setupServiceLister(testService1, testService2, testService3); + + Map labels = new HashMap<>(); + labels.put("k8s", "true"); + labels.put("spring", "true"); + + when(kubernetesDiscoveryProperties.getServiceLabels()).thenReturn(labels); + + KubernetesInformerDiscoveryClient discoveryClient = new KubernetesInformerDiscoveryClient("", + sharedInformerFactory, serviceLister, null, null, null, kubernetesDiscoveryProperties); + + assertThat(discoveryClient.getServices().toArray()).containsOnly(testService3.getMetadata().getName()); + + verify(kubernetesDiscoveryProperties, times(1)).isAllNamespaces(); + } + + @Test + public void testDiscoveryInstancesWithServiceLabels() { + Lister serviceLister = setupServiceLister(testService1, testService2, testService3); + Lister endpointsLister = setupEndpointsLister(testEndpoints1, testEndpoints3); + + Map labels = new HashMap<>(); + labels.put("k8s", "true"); + labels.put("spring", "true"); + + when(kubernetesDiscoveryProperties.isAllNamespaces()).thenReturn(true); + when(kubernetesDiscoveryProperties.getServiceLabels()).thenReturn(labels); + + KubernetesInformerDiscoveryClient discoveryClient = new KubernetesInformerDiscoveryClient("", + 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, "namespace1", null)); + } + @Test public void testDiscoveryGetServicesOneNamespaceShouldWork() { Lister serviceLister = setupServiceLister(testService1, testService2); @@ -130,8 +179,8 @@ public void testDiscoveryGetInstanceAllNamespaceShouldWork() { 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(); @@ -147,8 +196,8 @@ public void testDiscoveryGetInstanceOneNamespaceShouldWork() { 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(); } @@ -180,8 +229,8 @@ public void testDiscoveryGetInstanceWithNotReadyAddressesIncludedShouldWork() { 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(); @@ -226,8 +275,8 @@ public void instanceWithMultiplePortsAndPrimaryPortNameConfiguredWithLabelShould 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(); @@ -245,8 +294,8 @@ public void instanceWithMultiplePortsAndMisconfiguredPrimaryPortNameInLabelShoul 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(); } @@ -262,8 +311,8 @@ public void instanceWithMultiplePortsAndGenericPrimaryPortNameConfiguredShouldWo 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(); @@ -281,8 +330,8 @@ public void instanceWithMultiplePortsAndMisconfiguredGenericPrimaryPortNameShoul 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(); } @@ -297,8 +346,8 @@ public void instanceWithMultiplePortsAndWithoutPrimaryPortNameSpecifiedShouldFal 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(); } @@ -313,8 +362,8 @@ public void instanceWithMultiplePortsAndWithoutPrimaryPortNameSpecifiedOrHttpsPo 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(); } @@ -330,8 +379,8 @@ public void instanceWithMultiplePortsAndWithoutAnyConfigurationShouldPickTheFirs 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 519f2bb801..84dcaf9b81 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 void testDiscoveryGetInstanceAllNamespaceShouldWork() { 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 void testDiscoveryGetInstanceOneNamespaceShouldWork() { 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 e7c682de51..ff8123c8e1 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 @@ -51,6 +55,7 @@ org.springframework.boot spring-boot-starter-logging + true org.springframework.retry diff --git a/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/AbstractConfigProperties.java b/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/AbstractConfigProperties.java index 13213719d1..d225831008 100644 --- a/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/AbstractConfigProperties.java +++ b/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/AbstractConfigProperties.java @@ -30,8 +30,12 @@ public abstract class AbstractConfigProperties { protected String namespace; + // use config map name to prefix properties protected boolean useNameAsPrefix; + // use profile name to append config map name + protected boolean includeProfileSpecificSources = true; + protected boolean failFast = false; protected RetryProperties retry = new RetryProperties(); @@ -70,6 +74,14 @@ public void setUseNameAsPrefix(boolean useNameAsPrefix) { this.useNameAsPrefix = useNameAsPrefix; } + public boolean isIncludeProfileSpecificSources() { + return includeProfileSpecificSources; + } + + public void setIncludeProfileSpecificSources(boolean includeProfileSpecificSources) { + this.includeProfileSpecificSources = includeProfileSpecificSources; + } + public boolean isFailFast() { return failFast; } diff --git a/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/ConfigMapConfigProperties.java b/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/ConfigMapConfigProperties.java index a4af233047..1be1f2b929 100644 --- a/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/ConfigMapConfigProperties.java +++ b/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/ConfigMapConfigProperties.java @@ -88,10 +88,11 @@ public List determineSources() { "'spring.cloud.kubernetes.config.useNameAsPrefix' is set to 'true', but 'spring.cloud.kubernetes.config.sources'" + " is empty; as such will default 'useNameAsPrefix' to 'false'"); } - return Collections.singletonList(new NormalizedSource(name, namespace, "")); + return Collections.singletonList(new NormalizedSource(name, namespace, "", includeProfileSpecificSources)); } - return sources.stream().map(s -> s.normalize(name, namespace, useNameAsPrefix)).collect(Collectors.toList()); + return sources.stream().map(s -> s.normalize(name, namespace, useNameAsPrefix, includeProfileSpecificSources)) + .collect(Collectors.toList()); } @Override @@ -120,6 +121,12 @@ public static class Source { */ private Boolean useNameAsPrefix; + /** + * Use profile name to append to a config map name. Can't be a primitive, we need + * to know if it was explicitly set or not + */ + protected Boolean includeProfileSpecificSources; + /** * An explicit prefix to be used for properties. */ @@ -166,6 +173,14 @@ public void setExplicitPrefix(String explicitPrefix) { this.explicitPrefix = explicitPrefix; } + public Boolean getIncludeProfileSpecificSources() { + return includeProfileSpecificSources; + } + + public void setIncludeProfileSpecificSources(Boolean includeProfileSpecificSources) { + this.includeProfileSpecificSources = includeProfileSpecificSources; + } + public boolean isEmpty() { return !StringUtils.hasLength(this.name) && !StringUtils.hasLength(this.namespace); } @@ -175,15 +190,18 @@ public boolean isEmpty() { public NormalizedSource normalize(String defaultName, String defaultNamespace) { String normalizedName = StringUtils.hasLength(this.name) ? this.name : defaultName; String normalizedNamespace = StringUtils.hasLength(this.namespace) ? this.namespace : defaultNamespace; - return new NormalizedSource(normalizedName, normalizedNamespace, ""); + return new NormalizedSource(normalizedName, normalizedNamespace, "", true); } - public NormalizedSource normalize(String defaultName, String defaultNamespace, boolean defaultUseNameAsPrefix) { + public NormalizedSource normalize(String defaultName, String defaultNamespace, boolean defaultUseNameAsPrefix, + boolean defaultIncludeProfileSpecificSources) { String normalizedName = StringUtils.hasLength(this.name) ? this.name : defaultName; String normalizedNamespace = StringUtils.hasLength(this.namespace) ? this.namespace : defaultNamespace; String prefix = ConfigUtils.findPrefix(this.explicitPrefix, useNameAsPrefix, defaultUseNameAsPrefix, normalizedName); - return new NormalizedSource(normalizedName, normalizedNamespace, prefix); + boolean includeProfileSpecificSources = ConfigUtils.includeProfileSpecificSources( + defaultIncludeProfileSpecificSources, this.includeProfileSpecificSources); + return new NormalizedSource(normalizedName, normalizedNamespace, prefix, includeProfileSpecificSources); } @Override @@ -213,18 +231,22 @@ public static class NormalizedSource { private final String prefix; + private final boolean includeProfileSpecificSources; + // not used, but not removed because of potential compatibility reasons @Deprecated NormalizedSource(String name, String namespace) { this.name = name; this.namespace = namespace; this.prefix = ""; + this.includeProfileSpecificSources = true; } - NormalizedSource(String name, String namespace, String prefix) { + NormalizedSource(String name, String namespace, String prefix, boolean includeProfileSpecificSources) { this.name = name; this.namespace = namespace; this.prefix = Objects.requireNonNull(prefix); + this.includeProfileSpecificSources = includeProfileSpecificSources; } public String getName() { @@ -239,6 +261,10 @@ public String getPrefix() { return prefix; } + public boolean isIncludeProfileSpecificSources() { + return includeProfileSpecificSources; + } + @Override public String toString() { return "{ config-map name : '" + name + "', namespace : '" + namespace + "', prefix : '" + prefix + "' }"; diff --git a/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/ConfigUtils.java b/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/ConfigUtils.java index ac92e045d1..0d5070762d 100644 --- a/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/ConfigUtils.java +++ b/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/ConfigUtils.java @@ -84,4 +84,19 @@ public static String findPrefix(String explicitPrefix, Boolean useNameAsPrefix, return ""; } + /** + * @param defaultIncludeProfileSpecificSources value of + * 'spring.cloud.kubernetes.config.includeProfileSpecificSources' + * @param includeProfileSpecificSources value of + * 'spring.cloud.kubernetes.config.sources.includeProfileSpecificSources' + * @return useProfileNameAsPrefix to be used in normalized sources + */ + public static boolean includeProfileSpecificSources(boolean defaultIncludeProfileSpecificSources, + Boolean includeProfileSpecificSources) { + if (includeProfileSpecificSources != null) { + return includeProfileSpecificSources; + } + return defaultIncludeProfileSpecificSources; + } + } 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 085c0ad728..afa78de605 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 KubernetesServiceInstance(String instanceId, String serviceId, String hos 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 @@ private URI createUri(String scheme, String host, int port) { } 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 boolean equals(Object o) { 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-commons/src/test/java/org/springframework/cloud/kubernetes/commons/KubernetesNamespaceProviderTests.java b/spring-cloud-kubernetes-commons/src/test/java/org/springframework/cloud/kubernetes/commons/KubernetesNamespaceProviderTests.java index 8b7a4c769c..f10a557058 100644 --- a/spring-cloud-kubernetes-commons/src/test/java/org/springframework/cloud/kubernetes/commons/KubernetesNamespaceProviderTests.java +++ b/spring-cloud-kubernetes-commons/src/test/java/org/springframework/cloud/kubernetes/commons/KubernetesNamespaceProviderTests.java @@ -56,7 +56,7 @@ public void getNamespace() { environment.setProperty(NAMESPACE_PROPERTY, "mynamespace"); KubernetesNamespaceProvider p1 = new KubernetesNamespaceProvider(environment); assertThat(p1.getNamespace()).isEqualTo("mynamespace"); - paths.verify(times(0), () -> Paths.get(PATH)); + paths.verify(() -> Paths.get(PATH), times(0)); } diff --git a/spring-cloud-kubernetes-commons/src/test/java/org/springframework/cloud/kubernetes/commons/config/ConfigMapConfigPropertiesTests.java b/spring-cloud-kubernetes-commons/src/test/java/org/springframework/cloud/kubernetes/commons/config/ConfigMapConfigPropertiesTests.java index 054061657a..d665fbb475 100644 --- a/spring-cloud-kubernetes-commons/src/test/java/org/springframework/cloud/kubernetes/commons/config/ConfigMapConfigPropertiesTests.java +++ b/spring-cloud-kubernetes-commons/src/test/java/org/springframework/cloud/kubernetes/commons/config/ConfigMapConfigPropertiesTests.java @@ -218,4 +218,114 @@ public void testMultipleCases() { Assertions.assertEquals(sources.get(3).getPrefix(), ""); } + /** + *
+	 * 	spring:
+	 *	  cloud:
+	 *      kubernetes:
+	 *        config:
+	 *          name: config-map-a
+	 *        	namespace: spring-k8s
+	 * 
+ * + * a config as above will result in a NormalizedSource where + * includeProfileSpecificSources will be true (this test proves that the change we + * added is not a breaking change for the already existing functionality) + */ + @Test + public void testUseIncludeProfileSpecificSourcesNoChanges() { + ConfigMapConfigProperties properties = new ConfigMapConfigProperties(); + properties.setSources(Collections.emptyList()); + properties.setName("config-map-a"); + properties.setNamespace("spring-k8s"); + + List sources = properties.determineSources(); + Assertions.assertEquals(sources.size(), 1, "empty sources must generate a List with a single NormalizedSource"); + + Assertions.assertTrue(sources.get(0).isIncludeProfileSpecificSources()); + } + + /** + *
+	 * 	spring:
+	 *	  cloud:
+	 *      kubernetes:
+	 *        config:
+	 *          includeProfileSpecificSources: false
+	 *          name: config-map-a
+	 *        	namespace: spring-k8s
+	 * 
+ * + * a config as above will result in a NormalizedSource where + * includeProfileSpecificSources will be false. Even if we did not define any sources + * explicitly, one will still be created, by default. That one might "flatMap" into + * multiple other, because of multiple profiles. As such this setting still matters + * and must be propagated to the normalized source. + */ + @Test + public void testUseIncludeProfileSpecificSourcesDefaultChanged() { + ConfigMapConfigProperties properties = new ConfigMapConfigProperties(); + properties.setSources(Collections.emptyList()); + properties.setName("config-map-a"); + properties.setNamespace("spring-k8s"); + properties.setIncludeProfileSpecificSources(false); + + List sources = properties.determineSources(); + Assertions.assertEquals(sources.size(), 1, "empty sources must generate a List with a single NormalizedSource"); + + Assertions.assertFalse(sources.get(0).isIncludeProfileSpecificSources()); + } + + /** + *
+	 * 	spring:
+	 *	  cloud:
+	 *      kubernetes:
+	 *        config:
+	 *          includeProfileSpecificSources: false
+	 *          name: config-map-a
+	 *        	namespace: spring-k8s
+	 *        sources:
+	 *          - name: one
+	 *            includeProfileSpecificSources: true
+	 *          - name: two
+	 *          - name: three
+	 *            includeProfileSpecificSources: false
+	 * 
+ * + *
+	 * 	source "one" will have "includeProfileSpecificSources = true".
+	 * 	source "two" will have "includeProfileSpecificSources = false".
+	 * 	source "three" will have "includeProfileSpecificSources = false".
+	 * 
+ */ + @Test + public void testUseIncludeProfileSpecificSourcesDefaultChangedSourceOverride() { + ConfigMapConfigProperties properties = new ConfigMapConfigProperties(); + properties.setSources(Collections.emptyList()); + properties.setName("config-map-a"); + properties.setNamespace("spring-k8s"); + properties.setIncludeProfileSpecificSources(false); + + ConfigMapConfigProperties.Source one = new ConfigMapConfigProperties.Source(); + one.setName("config-map-one"); + one.setIncludeProfileSpecificSources(true); + + ConfigMapConfigProperties.Source two = new ConfigMapConfigProperties.Source(); + two.setName("config-map-two"); + + ConfigMapConfigProperties.Source three = new ConfigMapConfigProperties.Source(); + three.setName("config-map-three"); + three.setIncludeProfileSpecificSources(false); + + properties.setSources(Arrays.asList(one, two, three)); + + List sources = properties.determineSources(); + Assertions.assertEquals(sources.size(), 3); + + Assertions.assertTrue(sources.get(0).isIncludeProfileSpecificSources()); + Assertions.assertFalse(sources.get(1).isIncludeProfileSpecificSources()); + Assertions.assertFalse(sources.get(2).isIncludeProfileSpecificSources()); + } + } diff --git a/spring-cloud-kubernetes-commons/src/test/java/org/springframework/cloud/kubernetes/commons/config/ConfigUtilsTests.java b/spring-cloud-kubernetes-commons/src/test/java/org/springframework/cloud/kubernetes/commons/config/ConfigUtilsTests.java index d513737dad..5bed6bfe92 100644 --- a/spring-cloud-kubernetes-commons/src/test/java/org/springframework/cloud/kubernetes/commons/config/ConfigUtilsTests.java +++ b/spring-cloud-kubernetes-commons/src/test/java/org/springframework/cloud/kubernetes/commons/config/ConfigUtilsTests.java @@ -54,4 +54,55 @@ public void testNoMatch() { Assertions.assertEquals(result, ""); } + /** + *
+	 *   spring:
+	 *     cloud:
+	 *        kubernetes:
+	 *           config:
+	 *              includeProfileSpecificSources: true
+	 * 
+ * + * above will generate "true" for a normalized source + */ + @Test + public void testUseIncludeProfileSpecificSourcesOnlyDefaultSet() { + Assertions.assertTrue(ConfigUtils.includeProfileSpecificSources(true, null)); + } + + /** + *
+	 *   spring:
+	 *     cloud:
+	 *        kubernetes:
+	 *           config:
+	 *              includeProfileSpecificSources: true
+	 * 
+ * + * above will generate "false" for a normalized source + */ + @Test + public void testUseIncludeProfileSpecificSourcesOnlyDefaultNotSet() { + Assertions.assertFalse(ConfigUtils.includeProfileSpecificSources(false, null)); + } + + /** + *
+	 *   spring:
+	 *     cloud:
+	 *        kubernetes:
+	 *           config:
+	 *              includeProfileSpecificSources: true
+	 *           sources:
+	 *           - name: one
+	 *             includeProfileSpecificSources: false
+	 * 
+ * + * above will generate "false" for a normalized source + */ + @Test + public void testUseIncludeProfileSpecificSourcesSourcesOverridesDefault() { + Assertions.assertFalse(ConfigUtils.includeProfileSpecificSources(true, false)); + } + } diff --git a/spring-cloud-kubernetes-controllers/pom.xml b/spring-cloud-kubernetes-controllers/pom.xml index 9594985d0b..0d75a5c6b1 100644 --- a/spring-cloud-kubernetes-controllers/pom.xml +++ b/spring-cloud-kubernetes-controllers/pom.xml @@ -14,6 +14,8 @@ spring-cloud-kubernetes-configuration-watcher + spring-cloud-kubernetes-discoveryserver + spring-cloud-kubernetes-configserver diff --git a/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configserver/k8s/deployment.yaml b/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configserver/k8s/deployment.yaml new file mode 100644 index 0000000000..563cf2b531 --- /dev/null +++ b/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configserver/k8s/deployment.yaml @@ -0,0 +1,79 @@ +--- +apiVersion: v1 +kind: List +items: + - apiVersion: v1 + kind: Service + metadata: + labels: + app: spring-cloud-kubernetes-configserver + name: spring-cloud-kubernetes-configserver + spec: + ports: + - name: http + port: 8888 + targetPort: 8888 + selector: + app: spring-cloud-kubernetes-configserver + type: LoadBalancer + - apiVersion: v1 + kind: ServiceAccount + metadata: + labels: + app: spring-cloud-kubernetes-configserver + name: spring-cloud-kubernetes-configserver + - apiVersion: rbac.authorization.k8s.io/v1 + kind: RoleBinding + metadata: + labels: + app: spring-cloud-kubernetes-configserver + name: spring-cloud-kubernetes-configserver:view + roleRef: + kind: Role + apiGroup: rbac.authorization.k8s.io + name: namespace-reader + subjects: + - kind: ServiceAccount + name: spring-cloud-kubernetes-configserver + - apiVersion: rbac.authorization.k8s.io/v1 + kind: Role + metadata: + namespace: default + name: namespace-reader + rules: + - apiGroups: ["", "extensions", "apps"] + resources: ["configmaps", "secrets"] + verbs: ["get", "list"] + - apiVersion: apps/v1 + kind: Deployment + metadata: + name: spring-cloud-kubernetes-configserver-deployment + spec: + selector: + matchLabels: + app: spring-cloud-kubernetes-configserver + template: + metadata: + labels: + app: spring-cloud-kubernetes-configserver + spec: + serviceAccount: spring-cloud-kubernetes-configserver + containers: + - name: spring-cloud-kubernetes-configserver + image: springcloud/spring-cloud-kubernetes-configserver:2.0.4-SNAPSHOT + imagePullPolicy: IfNotPresent + env: + - name: SPRING_PROFILES_INCLUDE + value: "kubernetes" + - name: SPRING_CLOUD_KUBERNETES_SECRETS_ENABLEAPI + value: "true" + readinessProbe: + httpGet: + port: 8888 + path: /actuator/health/readiness + livenessProbe: + httpGet: + port: 8888 + path: /actuator/health/liveness + ports: + - containerPort: 8888 diff --git a/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configserver/pom.xml b/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configserver/pom.xml new file mode 100644 index 0000000000..5eca21df74 --- /dev/null +++ b/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configserver/pom.xml @@ -0,0 +1,145 @@ + + + + spring-cloud-kubernetes-controllers + org.springframework.cloud + 2.1.0-SNAPSHOT + + 4.0.0 + + spring-cloud-kubernetes-configserver + + + 1.8.0 + openjdk:8u222-slim + springcloud + 4.1.0 + + + + + org.springframework.cloud + spring-cloud-config-server + + + org.springframework.boot + spring-boot-starter-actuator + + + org.springframework.cloud + spring-cloud-starter-kubernetes-client-config + + + + org.springframework.boot + spring-boot-starter-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-configserver/skaffold.yaml b/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configserver/skaffold.yaml new file mode 100644 index 0000000000..138b208c53 --- /dev/null +++ b/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configserver/skaffold.yaml @@ -0,0 +1,20 @@ +apiVersion: skaffold/v2alpha3 +kind: Config +metadata: + name: spring-cloud-kubernetes-configserver +build: + artifacts: + - image: springcloud/spring-cloud-kubernetes-configserver + 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-configserver/src/main/java/org/springframework/cloud/kubernetes/configserver/KubernetesConfigServerApplication.java b/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configserver/src/main/java/org/springframework/cloud/kubernetes/configserver/KubernetesConfigServerApplication.java new file mode 100644 index 0000000000..e7b44f9a3f --- /dev/null +++ b/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configserver/src/main/java/org/springframework/cloud/kubernetes/configserver/KubernetesConfigServerApplication.java @@ -0,0 +1,35 @@ +/* + * 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.configserver; + +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.builder.SpringApplicationBuilder; +import org.springframework.cloud.config.server.ConfigServerApplication; +import org.springframework.cloud.config.server.EnableConfigServer; + +/** + * @author Ryan Baxter + */ +@SpringBootApplication +@EnableConfigServer +public class KubernetesConfigServerApplication { + + public static void main(String[] args) { + new SpringApplicationBuilder(ConfigServerApplication.class).run(args); + } + +} diff --git a/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configserver/src/main/java/org/springframework/cloud/kubernetes/configserver/KubernetesConfigServerAutoConfiguration.java b/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configserver/src/main/java/org/springframework/cloud/kubernetes/configserver/KubernetesConfigServerAutoConfiguration.java new file mode 100644 index 0000000000..038fac8128 --- /dev/null +++ b/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configserver/src/main/java/org/springframework/cloud/kubernetes/configserver/KubernetesConfigServerAutoConfiguration.java @@ -0,0 +1,91 @@ +/* + * 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.configserver; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; + +import io.kubernetes.client.openapi.apis.CoreV1Api; + +import org.springframework.boot.autoconfigure.AutoConfigureAfter; +import org.springframework.boot.autoconfigure.AutoConfigureBefore; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.cloud.config.server.config.ConfigServerAutoConfiguration; +import org.springframework.cloud.config.server.environment.EnvironmentRepository; +import org.springframework.cloud.kubernetes.client.KubernetesClientAutoConfiguration; +import org.springframework.cloud.kubernetes.client.config.KubernetesClientConfigMapPropertySource; +import org.springframework.cloud.kubernetes.client.config.KubernetesClientSecretsPropertySource; +import org.springframework.cloud.kubernetes.commons.ConditionalOnKubernetesConfigEnabled; +import org.springframework.cloud.kubernetes.commons.ConditionalOnKubernetesEnabled; +import org.springframework.cloud.kubernetes.commons.ConditionalOnKubernetesSecretsEnabled; +import org.springframework.cloud.kubernetes.commons.KubernetesNamespaceProvider; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Profile; +import org.springframework.core.env.MapPropertySource; + +import static org.springframework.cloud.kubernetes.configserver.KubernetesPropertySourceSupplier.namespaceSplitter; + +/** + * @author Ryan Baxter + */ +@Configuration +@AutoConfigureAfter({ KubernetesClientAutoConfiguration.class }) +@AutoConfigureBefore({ ConfigServerAutoConfiguration.class }) +@ConditionalOnKubernetesEnabled +@EnableConfigurationProperties(KubernetesConfigServerProperties.class) +public class KubernetesConfigServerAutoConfiguration { + + @Bean + @Profile("kubernetes") + public EnvironmentRepository kubernetesEnvironmentRepository(CoreV1Api coreV1Api, + List kubernetesPropertySourceSuppliers, + KubernetesNamespaceProvider kubernetesNamespaceProvider) { + return new KubernetesEnvironmentRepository(coreV1Api, kubernetesPropertySourceSuppliers, + kubernetesNamespaceProvider.getNamespace()); + } + + @Bean + @ConditionalOnKubernetesConfigEnabled + @ConditionalOnProperty(value = "spring.cloud.kubernetes.config.enableApi", matchIfMissing = true) + public KubernetesPropertySourceSupplier configMapPropertySourceSupplier( + KubernetesConfigServerProperties properties) { + return (coreApi, applicationName, namespace, springEnv) -> { + List namespaces = namespaceSplitter(properties.getSecretsNamespaces(), namespace); + List propertySources = new ArrayList<>(); + namespaces.forEach(space -> propertySources.add(new KubernetesClientConfigMapPropertySource(coreApi, + applicationName, space, springEnv, "", true, false))); + return propertySources; + }; + } + + @Bean + @ConditionalOnKubernetesSecretsEnabled + @ConditionalOnProperty("spring.cloud.kubernetes.secrets.enableApi") + public KubernetesPropertySourceSupplier secretsPropertySourceSupplier(KubernetesConfigServerProperties properties) { + return (coreApi, applicationName, namespace, springEnv) -> { + List namespaces = namespaceSplitter(properties.getSecretsNamespaces(), namespace); + List propertySources = new ArrayList<>(); + namespaces.forEach(space -> propertySources.add(new KubernetesClientSecretsPropertySource(coreApi, + applicationName, space, springEnv, new HashMap<>(), false))); + return propertySources; + }; + } + +} diff --git a/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configserver/src/main/java/org/springframework/cloud/kubernetes/configserver/KubernetesConfigServerProperties.java b/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configserver/src/main/java/org/springframework/cloud/kubernetes/configserver/KubernetesConfigServerProperties.java new file mode 100644 index 0000000000..bbb88b81ed --- /dev/null +++ b/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configserver/src/main/java/org/springframework/cloud/kubernetes/configserver/KubernetesConfigServerProperties.java @@ -0,0 +1,48 @@ +/* + * 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.configserver; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * @author Ryan Baxter + */ + +@ConfigurationProperties("spring.cloud.kubernetes.configserver") +public class KubernetesConfigServerProperties { + + private String conigMapNamespaces = ""; + + private String secretsNamespaces = ""; + + public String getConigMapNamespaces() { + return conigMapNamespaces; + } + + public void setConigMapNamespaces(String conigMapNamespaces) { + this.conigMapNamespaces = conigMapNamespaces; + } + + public String getSecretsNamespaces() { + return secretsNamespaces; + } + + public void setSecretsNamespaces(String secretsNamespaces) { + this.secretsNamespaces = secretsNamespaces; + } + +} diff --git a/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configserver/src/main/java/org/springframework/cloud/kubernetes/configserver/KubernetesEnvironmentRepository.java b/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configserver/src/main/java/org/springframework/cloud/kubernetes/configserver/KubernetesEnvironmentRepository.java new file mode 100644 index 0000000000..e4e7c3c274 --- /dev/null +++ b/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configserver/src/main/java/org/springframework/cloud/kubernetes/configserver/KubernetesEnvironmentRepository.java @@ -0,0 +1,94 @@ +/* + * 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.configserver; + +import java.util.List; + +import io.kubernetes.client.openapi.apis.CoreV1Api; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.cloud.config.environment.Environment; +import org.springframework.cloud.config.environment.PropertySource; +import org.springframework.cloud.config.server.environment.EnvironmentRepository; +import org.springframework.core.env.MapPropertySource; +import org.springframework.core.env.StandardEnvironment; +import org.springframework.util.StringUtils; + +/** + * @author Ryan Baxter + */ +public class KubernetesEnvironmentRepository implements EnvironmentRepository { + + private static final Log LOG = LogFactory.getLog(KubernetesEnvironmentRepository.class); + + private CoreV1Api coreApi; + + private List kubernetesPropertySourceSuppliers; + + private String namespace; + + public KubernetesEnvironmentRepository(CoreV1Api coreApi, + List kubernetesPropertySourceSuppliers, String namespace) { + this.coreApi = coreApi; + this.kubernetesPropertySourceSuppliers = kubernetesPropertySourceSuppliers; + this.namespace = namespace; + } + + @Override + public Environment findOne(String application, String profile, String label) { + return findOne(application, profile, label, true); + } + + @Override + public Environment findOne(String application, String profile, String label, boolean includeOrigin) { + String[] profiles = StringUtils.commaDelimitedListToStringArray(profile); + LOG.info("Profiles: " + profile); + LOG.info("Application: " + application); + LOG.info("Label: " + label); + Environment environment = new Environment(application, profiles, label, null, null); + try { + StandardEnvironment springEnv = new StandardEnvironment(); + springEnv.setActiveProfiles(profiles); + addApplicationConfiguration(environment, springEnv, "application"); + if (!"application".equalsIgnoreCase(application)) { + addApplicationConfiguration(environment, springEnv, application); + } + return environment; + } + catch (Exception e) { + LOG.warn(e); + } + return environment; + } + + private void addApplicationConfiguration(Environment environment, StandardEnvironment springEnv, + String applicationName) { + kubernetesPropertySourceSuppliers.stream().forEach(supplier -> { + List propertySources = supplier.get(coreApi, applicationName, namespace, springEnv); + propertySources.forEach(propertySource -> { + if (propertySource.getPropertyNames().length > 0) { + LOG.debug("Adding PropertySource " + propertySource.getName()); + LOG.debug("PropertySource Names: " + + StringUtils.arrayToCommaDelimitedString(propertySource.getPropertyNames())); + environment.add(new PropertySource(propertySource.getName(), propertySource.getSource())); + } + }); + }); + } + +} diff --git a/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configserver/src/main/java/org/springframework/cloud/kubernetes/configserver/KubernetesPropertySourceSupplier.java b/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configserver/src/main/java/org/springframework/cloud/kubernetes/configserver/KubernetesPropertySourceSupplier.java new file mode 100644 index 0000000000..a73af430dd --- /dev/null +++ b/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configserver/src/main/java/org/springframework/cloud/kubernetes/configserver/KubernetesPropertySourceSupplier.java @@ -0,0 +1,45 @@ +/* + * 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.configserver; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import io.kubernetes.client.openapi.apis.CoreV1Api; + +import org.springframework.core.env.Environment; +import org.springframework.core.env.MapPropertySource; +import org.springframework.util.StringUtils; + +/** + * @author Ryan Baxter + */ +public interface KubernetesPropertySourceSupplier { + + List get(CoreV1Api coreV1Api, String name, String namespace, Environment environment); + + static List namespaceSplitter(String namespacesString, String currentNamespace) { + List namespaces = Collections.singletonList(currentNamespace); + String[] namespacesArray = StringUtils.commaDelimitedListToStringArray(namespacesString); + if (namespacesArray.length > 0) { + namespaces = Arrays.asList(namespacesArray); + } + return namespaces; + } + +} diff --git a/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configserver/src/main/resources/META-INF/spring.factories b/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configserver/src/main/resources/META-INF/spring.factories new file mode 100644 index 0000000000..453ef5808a --- /dev/null +++ b/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configserver/src/main/resources/META-INF/spring.factories @@ -0,0 +1,3 @@ +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ +org.springframework.cloud.kubernetes.configserver.KubernetesConfigServerAutoConfiguration + diff --git a/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configserver/src/main/resources/application.yaml b/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configserver/src/main/resources/application.yaml new file mode 100644 index 0000000000..aed3a8c7e1 --- /dev/null +++ b/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configserver/src/main/resources/application.yaml @@ -0,0 +1,6 @@ +spring: + application: + name: kubernetesconfigserver + +server: + port: 8888 diff --git a/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configserver/src/test/java/org/springframework/cloud/kubernetes/configserver/ConfigServerIntegrationTest.java b/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configserver/src/test/java/org/springframework/cloud/kubernetes/configserver/ConfigServerIntegrationTest.java new file mode 100644 index 0000000000..690f9b13ae --- /dev/null +++ b/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configserver/src/test/java/org/springframework/cloud/kubernetes/configserver/ConfigServerIntegrationTest.java @@ -0,0 +1,88 @@ +/* + * 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.configserver; + +import com.github.tomakehurst.wiremock.WireMockServer; +import com.github.tomakehurst.wiremock.client.WireMock; +import io.kubernetes.client.openapi.JSON; +import io.kubernetes.client.openapi.models.V1ConfigMapBuilder; +import io.kubernetes.client.openapi.models.V1ConfigMapList; +import io.kubernetes.client.openapi.models.V1ListMetaBuilder; +import io.kubernetes.client.openapi.models.V1ObjectMetaBuilder; +import io.kubernetes.client.openapi.models.V1SecretBuilder; +import io.kubernetes.client.openapi.models.V1SecretList; +import io.kubernetes.client.openapi.models.V1SecretListBuilder; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.web.client.TestRestTemplate; +import org.springframework.cloud.config.environment.Environment; + +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.urlMatching; +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Ryan Baxter + */ +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, + properties = { "spring.cloud.kubernetes.client.namespace=default", "spring.profiles.include=kubernetes", + "spring.cloud.kubernetes.secrets.enableApi=true", "debug=true" }, + classes = { KubernetesConfigServerApplication.class }) +public class ConfigServerIntegrationTest { + + @Autowired + private TestRestTemplate testRestTemplate; + + public static WireMockServer wireMockServer; + + @BeforeEach + public void beforeEach() { + V1ConfigMapList TEST_CONFIGMAP = new V1ConfigMapList().addItemsItem(new V1ConfigMapBuilder().withMetadata( + new V1ObjectMetaBuilder().withName("test-cm").withNamespace("default").withResourceVersion("1").build()) + .addToData("app.name", "test").build()); + + V1SecretList TEST_SECRET = new V1SecretListBuilder() + .withMetadata(new V1ListMetaBuilder().withResourceVersion("1").build()) + .addToItems(new V1SecretBuilder() + .withMetadata(new V1ObjectMetaBuilder().withName("test-cm").withResourceVersion("0") + .withNamespace("default").build()) + .addToData("password", "p455w0rd".getBytes()).addToData("username", "user".getBytes()).build()) + .build(); + + WireMock.stubFor(get(urlMatching("^/api/v1/namespaces/default/configmaps.*")) + .willReturn(aResponse().withStatus(200).withBody(new JSON().serialize(TEST_CONFIGMAP)))); + + WireMock.stubFor(get(urlMatching("^/api/v1/namespaces/default/secrets.*")) + .willReturn(aResponse().withStatus(200).withBody(new JSON().serialize(TEST_SECRET)))); + } + + @Test + public void enabled() throws Exception { + Environment env = testRestTemplate.getForObject("/test-cm/default", Environment.class); + assertThat(env.getPropertySources().size()).isEqualTo(2); + assertThat(env.getPropertySources().get(0).getName().equals("configmap.test-cm.default")).isTrue(); + assertThat(env.getPropertySources().get(0).getSource().get("app.name")).isEqualTo("test"); + assertThat(env.getPropertySources().get(1).getName().equals("secrets.test-cm.default")).isTrue(); + assertThat(env.getPropertySources().get(1).getSource().get("password")).isEqualTo("p455w0rd"); + assertThat(env.getPropertySources().get(1).getSource().get("username")).isEqualTo("user"); + } + +} diff --git a/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configserver/src/test/java/org/springframework/cloud/kubernetes/configserver/KubernetesConfigServerAutoConfigurationTests.java b/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configserver/src/test/java/org/springframework/cloud/kubernetes/configserver/KubernetesConfigServerAutoConfigurationTests.java new file mode 100644 index 0000000000..0ae3aaa7dc --- /dev/null +++ b/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configserver/src/test/java/org/springframework/cloud/kubernetes/configserver/KubernetesConfigServerAutoConfigurationTests.java @@ -0,0 +1,174 @@ +/* + * 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.configserver; + +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.cloud.config.server.environment.EnvironmentRepository; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Profile; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; + +/** + * @author Ryan Baxter + */ +public class KubernetesConfigServerAutoConfigurationTests { + + @Configuration + static class MockConfig { + + @Bean + @Profile("kubernetesdisabled") + public EnvironmentRepository environmentRepository() { + return mock(EnvironmentRepository.class); + } + + } + + @Nested + @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, + properties = { "spring.profiles.include=kubernetes,kubernetesdisabled", + "spring.cloud.kubernetes.enabled=false", "debug=true" }, + classes = { KubernetesConfigServerApplication.class, MockConfig.class }) + public class KubernetesDisabled { + + @Autowired + ConfigurableApplicationContext context; + + @Test + void runTest() { + assertThat(context.getBeanNamesForType(KubernetesEnvironmentRepository.class)).hasSize(0); + } + + } + + @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, + classes = { KubernetesConfigServerApplication.class, MockConfig.class }, + properties = { "spring.cloud.kubernetes.enabled=false", + "spring.profiles.include=kubernetes,kubernetesdisabled", "debug=true" }) + @Nested + class KubernetesProfileMissing { + + @Autowired + ConfigurableApplicationContext context; + + @Test + void runTest() { + assertThat(context.getBeanNamesForType(KubernetesEnvironmentRepository.class)).hasSize(0); + } + + } + + @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, + classes = { KubernetesConfigServerApplication.class }, properties = { "spring.profiles.include=kubernetes", + "debug=true", "spring.cloud.kubernetes.client.namespace=default" }) + @Nested + class KubernetesEnabledProfileIncluded { + + @Autowired + ConfigurableApplicationContext context; + + @Test + void runTest() { + assertThat(context.getBeanNamesForType(KubernetesEnvironmentRepository.class)).hasSize(1); + assertThat(context.getBeanNamesForType(KubernetesPropertySourceSupplier.class)).hasSize(1); + } + + } + + @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, + classes = { KubernetesConfigServerApplication.class }, properties = { "spring.profiles.include=kubernetes", + "debug=true", "spring.cloud.kubernetes.config.enabled=false" }) + @Nested + class KubernetesEnabledProfileIncludedConfigMapDisabled { + + @Autowired + ConfigurableApplicationContext context; + + @Test + void runTest() { + assertThat(context.getBeanNamesForType(KubernetesEnvironmentRepository.class)).hasSize(1); + assertThat(context.getBeanNamesForType(KubernetesPropertySourceSupplier.class)).hasSize(0); + } + + } + + @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, + classes = { KubernetesConfigServerApplication.class }, properties = { "spring.profiles.include=kubernetes", + "debug=true", "spring.cloud.kubernetes.client.namespace=default" }) + @Nested + class KubernetesEnabledProfileIncludedSecretsApiDisabled { + + @Autowired + ConfigurableApplicationContext context; + + @Test + void runTest() { + assertThat(context.getBeanNamesForType(KubernetesEnvironmentRepository.class)).hasSize(1); + assertThat(context.getBeanNamesForType(KubernetesPropertySourceSupplier.class)).hasSize(1); + assertThat(context.getBeanNamesForType(KubernetesPropertySourceSupplier.class)[0]) + .isEqualTo("configMapPropertySourceSupplier"); + } + + } + + @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, + classes = { KubernetesConfigServerApplication.class }, + properties = { "spring.profiles.include=kubernetes", "debug=true", + "spring.cloud.kubernetes.client.namespace=default", + "spring.cloud.kubernetes.secrets.enableApi=true" }) + @Nested + class KubernetesEnabledProfileIncludedSecretsApiEnabled { + + @Autowired + ConfigurableApplicationContext context; + + @Test + void runTest() { + assertThat(context.getBeanNamesForType(KubernetesEnvironmentRepository.class)).hasSize(1); + assertThat(context.getBeanNamesForType(KubernetesPropertySourceSupplier.class)).hasSize(2); + } + + } + + @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, + classes = { KubernetesConfigServerApplication.class }, + properties = { "spring.profiles.include=kubernetes", "debug=true", + "spring.cloud.kubernetes.client.namespace=default", + "spring.cloud.kubernetes.config.enableApi=false" }) + @Nested + class KubernetesEnabledProfileIncludedConfigApiDisabled { + + @Autowired + ConfigurableApplicationContext context; + + @Test + void runTest() { + assertThat(context.getBeanNamesForType(KubernetesEnvironmentRepository.class)).hasSize(1); + assertThat(context.getBeanNamesForType(KubernetesPropertySourceSupplier.class)).hasSize(0); + } + + } + +} diff --git a/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configserver/src/test/java/org/springframework/cloud/kubernetes/configserver/KubernetesEnvironmentRepositoryTests.java b/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configserver/src/test/java/org/springframework/cloud/kubernetes/configserver/KubernetesEnvironmentRepositoryTests.java new file mode 100644 index 0000000000..7feb0df379 --- /dev/null +++ b/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configserver/src/test/java/org/springframework/cloud/kubernetes/configserver/KubernetesEnvironmentRepositoryTests.java @@ -0,0 +1,247 @@ +/* + * 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.configserver; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; + +import io.kubernetes.client.openapi.ApiException; +import io.kubernetes.client.openapi.apis.CoreV1Api; +import io.kubernetes.client.openapi.models.V1ConfigMapBuilder; +import io.kubernetes.client.openapi.models.V1ConfigMapList; +import io.kubernetes.client.openapi.models.V1ObjectMetaBuilder; +import io.kubernetes.client.openapi.models.V1SecretBuilder; +import io.kubernetes.client.openapi.models.V1SecretList; +import io.kubernetes.client.openapi.models.V1SecretListBuilder; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import org.springframework.cloud.config.environment.Environment; +import org.springframework.cloud.kubernetes.client.config.KubernetesClientConfigMapPropertySource; +import org.springframework.cloud.kubernetes.client.config.KubernetesClientSecretsPropertySource; +import org.springframework.core.env.MapPropertySource; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +/** + * @author Ryan Baxter + */ +class KubernetesEnvironmentRepositoryTests { + + private static List kubernetesPropertySourceSuppliers = new ArrayList<>(); + + private static final V1ConfigMapList CONFIGMAP_DEFAULT_LIST = new V1ConfigMapList() + .addItemsItem(new V1ConfigMapBuilder() + .withMetadata(new V1ObjectMetaBuilder().withName("application").withNamespace("default") + .withResourceVersion("1").build()) + .addToData("application.yaml", + "dummy:\n property:\n string2: \"a\"\n int2: 1\n bool2: true\n") + .build()) + .addItemsItem(new V1ConfigMapBuilder() + .withMetadata(new V1ObjectMetaBuilder().withName("stores").withNamespace("default") + .withResourceVersion("1").build()) + .addToData("application.yaml", + "dummy:\n property:\n string2: \"a\"\n int2: 1\n bool2: true\n") + .build()) + .addItemsItem(new V1ConfigMapBuilder() + .withMetadata(new V1ObjectMetaBuilder().withName("stores-dev").withNamespace("default") + .withResourceVersion("1").build()) + .addToData("application.yaml", + "dummy:\n property:\n string1: \"a\"\n string2: \"b\"\n int2: 2\n bool2: false\n") + .build()); + + private static final V1ConfigMapList CONFIGMAP_DEV_LIST = new V1ConfigMapList() + .addItemsItem(new V1ConfigMapBuilder() + .withMetadata(new V1ObjectMetaBuilder().withName("stores").withNamespace("dev") + .withResourceVersion("1").build()) + .addToData("application.yaml", + "dummy:\n property:\n string2: \"dev\"\n int2: 1\n bool2: true\n") + .build()); + + private static final V1SecretList SECRET_LIST = new V1SecretListBuilder() + .addToItems(new V1SecretBuilder() + .withMetadata(new V1ObjectMetaBuilder().withName("application").withResourceVersion("0") + .withNamespace("default").build()) + .addToData("password", "p455w0rd".getBytes()).addToData("username", "user".getBytes()).build()) + .addToItems(new V1SecretBuilder() + .withMetadata(new V1ObjectMetaBuilder().withName("stores").withResourceVersion("0") + .withNamespace("default").build()) + .addToData("password", "p455w0rd".getBytes()).addToData("username", "stores".getBytes()).build()) + .addToItems(new V1SecretBuilder() + .withMetadata(new V1ObjectMetaBuilder().withName("stores-dev").withResourceVersion("0") + .withNamespace("default").build()) + .addToData("password", "p455w0rd".getBytes()).addToData("username", "stores-dev".getBytes()) + .build()) + .build(); + + @BeforeAll + public static void before() { + kubernetesPropertySourceSuppliers.add((coreApi, applicationName, namespace, springEnv) -> { + List propertySources = new ArrayList<>(); + propertySources.add(new KubernetesClientConfigMapPropertySource(coreApi, applicationName, "default", + springEnv, "", true, false)); + propertySources.add(new KubernetesClientConfigMapPropertySource(coreApi, applicationName, "dev", springEnv, + "", true, false)); + return propertySources; + }); + kubernetesPropertySourceSuppliers.add((coreApi, applicationName, namespace, springEnv) -> { + List propertySources = new ArrayList<>(); + propertySources.add(new KubernetesClientSecretsPropertySource(coreApi, applicationName, "default", + springEnv, new HashMap<>(), false)); + return propertySources; + }); + } + + @Test + public void testApplicationCase() throws ApiException { + CoreV1Api coreApi = mock(CoreV1Api.class); + when(coreApi.listNamespacedConfigMap(eq("default"), eq(null), eq(null), eq(null), eq(null), eq(null), eq(null), + eq(null), eq(null), eq(null), eq(null))).thenReturn(CONFIGMAP_DEFAULT_LIST); + when(coreApi.listNamespacedSecret(eq("default"), eq(null), eq(null), eq(null), eq(null), eq(null), eq(null), + eq(null), eq(null), eq(null), eq(null))).thenReturn(SECRET_LIST); + when(coreApi.listNamespacedConfigMap(eq("dev"), eq(null), eq(null), eq(null), eq(null), eq(null), eq(null), + eq(null), eq(null), eq(null), eq(null))).thenReturn(CONFIGMAP_DEV_LIST); + KubernetesEnvironmentRepository environmentRepository = new KubernetesEnvironmentRepository(coreApi, + kubernetesPropertySourceSuppliers, "default"); + Environment environment = environmentRepository.findOne("application", "", ""); + assertThat(environment.getPropertySources().size()).isEqualTo(2); + environment.getPropertySources().forEach(propertySource -> { + assertThat(propertySource.getName().equals("configmap.application.default") + || propertySource.getName().equals("secrets.application.default")).isTrue(); + if (propertySource.getName().equals("configmap.application.default")) { + assertThat(propertySource.getSource().size()).isEqualTo(3); + assertThat(propertySource.getSource().get("dummy.property.int2")).isEqualTo(1); + assertThat(propertySource.getSource().get("dummy.property.bool2")).isEqualTo(true); + assertThat(propertySource.getSource().get("dummy.property.string2")).isEqualTo("a"); + } + if (propertySource.getName().equals("secrets.application.default")) { + assertThat(propertySource.getSource().size()).isEqualTo(2); + assertThat(propertySource.getSource().get("username")).isEqualTo("user"); + assertThat(propertySource.getSource().get("password")).isEqualTo("p455w0rd"); + } + }); + } + + @Test + public void testStoresCase() throws ApiException { + CoreV1Api coreApi = mock(CoreV1Api.class); + when(coreApi.listNamespacedConfigMap(eq("default"), eq(null), eq(null), eq(null), eq(null), eq(null), eq(null), + eq(null), eq(null), eq(null), eq(null))).thenReturn(CONFIGMAP_DEFAULT_LIST); + when(coreApi.listNamespacedConfigMap(eq("dev"), eq(null), eq(null), eq(null), eq(null), eq(null), eq(null), + eq(null), eq(null), eq(null), eq(null))).thenReturn(CONFIGMAP_DEV_LIST); + when(coreApi.listNamespacedSecret(eq("default"), eq(null), eq(null), eq(null), eq(null), eq(null), eq(null), + eq(null), eq(null), eq(null), eq(null))).thenReturn(SECRET_LIST); + KubernetesEnvironmentRepository environmentRepository = new KubernetesEnvironmentRepository(coreApi, + kubernetesPropertySourceSuppliers, "default"); + Environment environment = environmentRepository.findOne("stores", "", ""); + assertThat(environment.getPropertySources().size()).isEqualTo(5); + environment.getPropertySources().forEach(propertySource -> { + assertThat(propertySource.getName().equals("configmap.application.default") + || propertySource.getName().equals("secrets.application.default") + || propertySource.getName().equals("configmap.stores.default") + || propertySource.getName().equals("configmap.stores.dev") + || propertySource.getName().equals("secrets.stores.default")).isTrue(); + if (propertySource.getName().equals("configmap.application.default")) { + assertThat(propertySource.getSource().size()).isEqualTo(3); + assertThat(propertySource.getSource().get("dummy.property.int2")).isEqualTo(1); + assertThat(propertySource.getSource().get("dummy.property.bool2")).isEqualTo(true); + assertThat(propertySource.getSource().get("dummy.property.string2")).isEqualTo("a"); + } + if (propertySource.getName().equals("secrets.application.default")) { + assertThat(propertySource.getSource().size()).isEqualTo(2); + assertThat(propertySource.getSource().get("username")).isEqualTo("user"); + assertThat(propertySource.getSource().get("password")).isEqualTo("p455w0rd"); + } + if (propertySource.getName().equals("configmap.stores.default")) { + assertThat(propertySource.getSource().size()).isEqualTo(3); + assertThat(propertySource.getSource().get("dummy.property.int2")).isEqualTo(1); + assertThat(propertySource.getSource().get("dummy.property.bool2")).isEqualTo(true); + assertThat(propertySource.getSource().get("dummy.property.string2")).isEqualTo("a"); + } + if (propertySource.getName().equals("configmap.stores.dev")) { + assertThat(propertySource.getSource().size()).isEqualTo(3); + assertThat(propertySource.getSource().get("dummy.property.int2")).isEqualTo(1); + assertThat(propertySource.getSource().get("dummy.property.bool2")).isEqualTo(true); + assertThat(propertySource.getSource().get("dummy.property.string2")).isEqualTo("dev"); + } + if (propertySource.getName().equals("secrets.stores.default")) { + assertThat(propertySource.getSource().size()).isEqualTo(2); + assertThat(propertySource.getSource().get("username")).isEqualTo("stores"); + assertThat(propertySource.getSource().get("password")).isEqualTo("p455w0rd"); + } + }); + } + + @Test + public void testStoresProfileCase() throws ApiException { + CoreV1Api coreApi = mock(CoreV1Api.class); + when(coreApi.listNamespacedConfigMap(eq("default"), eq(null), eq(null), eq(null), eq(null), eq(null), eq(null), + eq(null), eq(null), eq(null), eq(null))).thenReturn(CONFIGMAP_DEFAULT_LIST); + when(coreApi.listNamespacedSecret(eq("default"), eq(null), eq(null), eq(null), eq(null), eq(null), eq(null), + eq(null), eq(null), eq(null), eq(null))).thenReturn(SECRET_LIST); + when(coreApi.listNamespacedConfigMap(eq("dev"), eq(null), eq(null), eq(null), eq(null), eq(null), eq(null), + eq(null), eq(null), eq(null), eq(null))).thenReturn(CONFIGMAP_DEV_LIST); + KubernetesEnvironmentRepository environmentRepository = new KubernetesEnvironmentRepository(coreApi, + kubernetesPropertySourceSuppliers, "default"); + Environment environment = environmentRepository.findOne("stores", "dev", ""); + assertThat(environment.getPropertySources().size()).isEqualTo(5); + environment.getPropertySources().forEach(propertySource -> { + assertThat(propertySource.getName().equals("configmap.application.default") + || propertySource.getName().equals("secrets.application.default") + || propertySource.getName().equals("configmap.stores.default") + || propertySource.getName().equals("configmap.stores.dev") + || propertySource.getName().equals("secrets.stores.default")).isTrue(); + if (propertySource.getName().equals("configmap.application.default")) { + assertThat(propertySource.getSource().size()).isEqualTo(3); + assertThat(propertySource.getSource().get("dummy.property.int2")).isEqualTo(1); + assertThat(propertySource.getSource().get("dummy.property.bool2")).isEqualTo(true); + assertThat(propertySource.getSource().get("dummy.property.string2")).isEqualTo("a"); + } + if (propertySource.getName().equals("secrets.application.default")) { + assertThat(propertySource.getSource().size()).isEqualTo(2); + assertThat(propertySource.getSource().get("username")).isEqualTo("user"); + assertThat(propertySource.getSource().get("password")).isEqualTo("p455w0rd"); + } + if (propertySource.getName().equals("configmap.stores.default")) { + assertThat(propertySource.getSource().size()).isEqualTo(4); + assertThat(propertySource.getSource().get("dummy.property.int2")).isEqualTo(2); + assertThat(propertySource.getSource().get("dummy.property.bool2")).isEqualTo(false); + assertThat(propertySource.getSource().get("dummy.property.string2")).isEqualTo("b"); + assertThat(propertySource.getSource().get("dummy.property.string1")).isEqualTo("a"); + } + if (propertySource.getName().equals("configmap.stores.dev")) { + assertThat(propertySource.getSource().size()).isEqualTo(3); + assertThat(propertySource.getSource().get("dummy.property.int2")).isEqualTo(1); + assertThat(propertySource.getSource().get("dummy.property.bool2")).isEqualTo(true); + assertThat(propertySource.getSource().get("dummy.property.string2")).isEqualTo("dev"); + } + // Currently KubernetesClientSecretsPropertySource does not take into account + // profiles, so that plays no role at the moment + // See https://github.com/spring-cloud/spring-cloud-kubernetes/issues/880 + if (propertySource.getName().equals("secrets.stores.default")) { + assertThat(propertySource.getSource().size()).isEqualTo(2); + assertThat(propertySource.getSource().get("username")).isEqualTo("stores"); + assertThat(propertySource.getSource().get("password")).isEqualTo("p455w0rd"); + } + }); + } + +} diff --git a/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configserver/src/test/java/org/springframework/cloud/kubernetes/configserver/TestBootstrapConfig.java b/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configserver/src/test/java/org/springframework/cloud/kubernetes/configserver/TestBootstrapConfig.java new file mode 100644 index 0000000000..b93a81ed50 --- /dev/null +++ b/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configserver/src/test/java/org/springframework/cloud/kubernetes/configserver/TestBootstrapConfig.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.configserver; + +import com.github.tomakehurst.wiremock.WireMockServer; +import com.github.tomakehurst.wiremock.client.WireMock; +import io.kubernetes.client.openapi.ApiClient; +import io.kubernetes.client.openapi.apis.CoreV1Api; +import io.kubernetes.client.util.ClientBuilder; + +import org.springframework.context.annotation.Bean; + +import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.options; + +/** + * @author Ryan Baxter + */ +public class TestBootstrapConfig { + + @Bean + WireMockServer wireMockServer() { + WireMockServer wireMockServer = new WireMockServer(options().dynamicPort()); + wireMockServer.start(); + WireMock.configureFor(wireMockServer.port()); + return wireMockServer; + } + + @Bean + ApiClient apiClient(WireMockServer wireMockServer) { + ApiClient apiClient = new ClientBuilder().setBasePath(wireMockServer.baseUrl()).build(); + apiClient.setDebugging(true); + return apiClient; + } + + @Bean + CoreV1Api coreApi(ApiClient apiClient) { + return new CoreV1Api(apiClient); + } + +} diff --git a/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configserver/src/test/resources/META-INF/spring.factories b/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configserver/src/test/resources/META-INF/spring.factories new file mode 100644 index 0000000000..86565476e6 --- /dev/null +++ b/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configserver/src/test/resources/META-INF/spring.factories @@ -0,0 +1,4 @@ +org.springframework.cloud.bootstrap.BootstrapConfiguration=\ +org.springframework.cloud.kubernetes.configserver.TestBootstrapConfig +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ +org.springframework.cloud.kubernetes.configserver.KubernetesConfigServerAutoConfiguration 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 0000000000..1ed29eecf2 --- /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 0000000000..d0656c7715 --- /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 0000000000..8a0467ee52 --- /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 0000000000..84415f740c --- /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 0000000000..612a361a96 --- /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 0000000000..58e7ca9849 --- /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 0000000000..bb29224afa --- /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 0000000000..4167a0fa8f --- /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 a42f9362f0..2e419b1c3c 100644 --- a/spring-cloud-kubernetes-dependencies/pom.xml +++ b/spring-cloud-kubernetes-dependencies/pom.xml @@ -147,6 +147,12 @@ ${project.version}
+ + org.springframework.cloud + spring-cloud-kubernetes-discovery + ${project.version} + + org.springframework.cloud @@ -196,6 +202,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 0000000000..e9b615ef7d --- /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 0000000000..ed173af393 --- /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 0000000000..25f23e7256 --- /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 0000000000..d40e06e6ed --- /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 0000000000..43884142eb --- /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 0000000000..e0e830b3bf --- /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 0000000000..f397da609c --- /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 0000000000..81619e215e --- /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 0000000000..a82e049f4f --- /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 0000000000..60bc45e34c --- /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 0000000000..60bd941616 --- /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 0000000000..9f6adf6b4d --- /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-fabric8-autoconfig/pom.xml b/spring-cloud-kubernetes-fabric8-autoconfig/pom.xml index 926bcf9498..4030dec7a2 100644 --- a/spring-cloud-kubernetes-fabric8-autoconfig/pom.xml +++ b/spring-cloud-kubernetes-fabric8-autoconfig/pom.xml @@ -66,6 +66,13 @@ org.springframework.boot spring-boot-starter-json + + + + org.springframework.boot + spring-boot-starter-logging + + diff --git a/spring-cloud-kubernetes-fabric8-config/src/main/java/org/springframework/cloud/kubernetes/fabric8/config/Fabric8ConfigMapPropertySource.java b/spring-cloud-kubernetes-fabric8-config/src/main/java/org/springframework/cloud/kubernetes/fabric8/config/Fabric8ConfigMapPropertySource.java index d8581634ee..f45b8894a6 100644 --- a/spring-cloud-kubernetes-fabric8-config/src/main/java/org/springframework/cloud/kubernetes/fabric8/config/Fabric8ConfigMapPropertySource.java +++ b/spring-cloud-kubernetes-fabric8-config/src/main/java/org/springframework/cloud/kubernetes/fabric8/config/Fabric8ConfigMapPropertySource.java @@ -45,7 +45,7 @@ public class Fabric8ConfigMapPropertySource extends ConfigMapPropertySource { private static final Log LOG = LogFactory.getLog(Fabric8ConfigMapPropertySource.class); public Fabric8ConfigMapPropertySource(KubernetesClient client, String name) { - this(client, name, null, null, "", false); + this(client, name, null, null, "", true, false); } /** @@ -53,28 +53,30 @@ public Fabric8ConfigMapPropertySource(KubernetesClient client, String name) { * discouraged. */ @Deprecated - public Fabric8ConfigMapPropertySource(KubernetesClient client, String applicationName, String namespace, + public Fabric8ConfigMapPropertySource(KubernetesClient client, String name, String namespace, Environment environment) { - this(client, applicationName, namespace, environment, "", false); + super(getName(name, getApplicationNamespace(client, namespace)), + getData(client, name, getApplicationNamespace(client, namespace), environment, "", true, false)); } - public Fabric8ConfigMapPropertySource(KubernetesClient client, String applicationName, String namespace, - Environment environment, String prefix, boolean failFast) { - super(getName(applicationName, getApplicationNamespace(client, namespace)), getData(client, applicationName, - getApplicationNamespace(client, namespace), environment, prefix, failFast)); + public Fabric8ConfigMapPropertySource(KubernetesClient client, String name, String namespace, + Environment environment, String prefix, boolean includeProfileSpecificSources, boolean failFast) { + super(getName(name, getApplicationNamespace(client, namespace)), + getData(client, name, getApplicationNamespace(client, namespace), environment, prefix, + includeProfileSpecificSources, failFast)); } - private static Map getData(KubernetesClient client, String applicationName, String namespace, - Environment environment, String prefix, boolean failFast) { + private static Map getData(KubernetesClient client, String name, String namespace, + Environment environment, String prefix, boolean includeProfileSpecificSources, boolean failFast) { - LOG.info("Loading ConfigMap with name '" + applicationName + "' in namespace '" + namespace + "'"); + LOG.info("Loading ConfigMap with name '" + name + "' in namespace '" + namespace + "'"); try { - Map data = getConfigMapData(client, namespace, applicationName); + Map data = getConfigMapData(client, namespace, name); Map result = new HashMap<>(processAllEntries(data, environment)); - if (environment != null) { + if (environment != null && includeProfileSpecificSources) { for (String activeProfile : environment.getActiveProfiles()) { - String mapNameWithProfile = applicationName + "-" + activeProfile; + String mapNameWithProfile = name + "-" + activeProfile; Map dataWithProfile = getConfigMapData(client, namespace, mapNameWithProfile); result.putAll(processAllEntries(dataWithProfile, environment)); } @@ -92,12 +94,10 @@ private static Map getData(KubernetesClient client, String appli catch (Exception e) { if (failFast) { throw new IllegalStateException( - "Unable to read ConfigMap with name '" + applicationName + "' in namespace '" + namespace + "'", - e); + "Unable to read ConfigMap with name '" + name + "' in namespace '" + namespace + "'", e); } - LOG.warn("Can't read configMap with name: [" + applicationName + "] in namespace: [" + namespace - + "]. Ignoring.", e); + LOG.warn("Can't read configMap with name: [" + name + "] in namespace: [" + namespace + "]. Ignoring.", e); } return Collections.emptyMap(); diff --git a/spring-cloud-kubernetes-fabric8-config/src/main/java/org/springframework/cloud/kubernetes/fabric8/config/Fabric8ConfigMapPropertySourceLocator.java b/spring-cloud-kubernetes-fabric8-config/src/main/java/org/springframework/cloud/kubernetes/fabric8/config/Fabric8ConfigMapPropertySourceLocator.java index 55761bda7b..21c6b79a2a 100644 --- a/spring-cloud-kubernetes-fabric8-config/src/main/java/org/springframework/cloud/kubernetes/fabric8/config/Fabric8ConfigMapPropertySourceLocator.java +++ b/spring-cloud-kubernetes-fabric8-config/src/main/java/org/springframework/cloud/kubernetes/fabric8/config/Fabric8ConfigMapPropertySourceLocator.java @@ -70,7 +70,8 @@ protected MapPropertySource getMapPropertySource(String applicationName, Normali String namespace = getApplicationNamespace(this.client, normalizedSource.getNamespace(), configurationTarget, provider); return new Fabric8ConfigMapPropertySource(this.client, applicationName, namespace, environment, - normalizedSource.getPrefix(), this.properties.isFailFast()); + normalizedSource.getPrefix(), normalizedSource.isIncludeProfileSpecificSources(), + this.properties.isFailFast()); } } diff --git a/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/ConfigMapWithIncludeProfileSpecificSourcesTests.java b/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/ConfigMapWithIncludeProfileSpecificSourcesTests.java new file mode 100644 index 0000000000..a6b87fce47 --- /dev/null +++ b/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/ConfigMapWithIncludeProfileSpecificSourcesTests.java @@ -0,0 +1,153 @@ +/* + * 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.fabric8.config; + +import java.util.HashMap; +import java.util.Map; + +import io.fabric8.kubernetes.api.model.ConfigMapBuilder; +import io.fabric8.kubernetes.client.Config; +import io.fabric8.kubernetes.client.KubernetesClient; +import io.fabric8.kubernetes.client.server.mock.EnableKubernetesMockClient; +import org.hamcrest.Matchers; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.reactive.AutoConfigureWebTestClient; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.cloud.kubernetes.fabric8.config.include_profile_specific_sources.IncludeProfileSpecificSourcesApp; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.junit.jupiter.SpringExtension; +import org.springframework.test.web.reactive.server.WebTestClient; + +/** + * @author wind57 + */ +@ExtendWith(SpringExtension.class) +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, + classes = IncludeProfileSpecificSourcesApp.class, + properties = { "spring.cloud.bootstrap.name=include-profile-specific-sources" }) +@AutoConfigureWebTestClient +@EnableKubernetesMockClient(crud = true, https = false) +@ActiveProfiles("dev") +class ConfigMapWithIncludeProfileSpecificSourcesTests { + + private static KubernetesClient mockClient; + + @Autowired + private WebTestClient webClient; + + @BeforeAll + public static void setUpBeforeClass() { + + // Configure the kubernetes master url to point to the mock server + System.setProperty(Config.KUBERNETES_MASTER_SYSTEM_PROPERTY, mockClient.getConfiguration().getMasterUrl()); + System.setProperty(Config.KUBERNETES_TRUST_CERT_SYSTEM_PROPERTY, "true"); + System.setProperty(Config.KUBERNETES_AUTH_TRYKUBECONFIG_SYSTEM_PROPERTY, "false"); + System.setProperty(Config.KUBERNETES_AUTH_TRYSERVICEACCOUNT_SYSTEM_PROPERTY, "false"); + System.setProperty(Config.KUBERNETES_NAMESPACE_SYSTEM_PROPERTY, "test"); + System.setProperty(Config.KUBERNETES_HTTP2_DISABLE, "true"); + + Map one = new HashMap<>(); + one.put("one.property", "one"); + createConfigmap("config-map-one-dev", one); + + Map two = new HashMap<>(); + two.put("two.property", "two"); + createConfigmap("config-map-two", two); + + Map twoDev = new HashMap<>(); + twoDev.put("two.property", "twoDev"); + createConfigmap("config-map-two-dev", twoDev); + + Map three = new HashMap<>(); + three.put("three.property", "three"); + createConfigmap("config-map-three", three); + + Map threeDev = new HashMap<>(); + threeDev.put("three.property", "threeDev"); + createConfigmap("config-map-three-dev", threeDev); + + } + + private static void createConfigmap(String name, Map data) { + mockClient.configMaps().inNamespace("spring-k8s") + .create(new ConfigMapBuilder().withNewMetadata().withName(name).endMetadata().addToData(data).build()); + } + + /** + *
+	 *   'spring.cloud.kubernetes.config.includeProfileSpecificSources=false'
+	 *   'spring.cloud.kubernetes.config.sources[0].includeProfileSpecificSources=true'
+	 *   'spring.cloud.kubernetes.config.sources[0].name=config-map-one'
+	 *
+	 *   We do not define config-map 'config-map-one', but we do define 'config-map-one-dev'.
+	 *
+	 * 	 As such: @ConfigurationProperties("one") must be resolved from 'config-map-one-dev'
+	 * 
+ */ + @Test + public void testOne() { + this.webClient.get().uri("/profile-specific/one").exchange().expectStatus().isOk().expectBody(String.class) + .value(Matchers.equalTo("one")); + } + + /** + *
+	 *   'spring.cloud.kubernetes.config.includeProfileSpecificSources=false'
+	 *   'spring.cloud.kubernetes.config.sources[1].includeProfileSpecificSources=false'
+	 *   'spring.cloud.kubernetes.config.sources[1].name=config-map-two'
+	 *
+	 *   We define config-map 'config-map-two', but we also define 'config-map-two-dev'.
+	 *   This tests proves that data will be read from 'config-map-two' _only_, even if 'config-map-two-dev'
+	 *   also exists. This happens because of the 'includeProfileSpecificSources=false' property defined at the source level.
+	 *   If this would be incorrect, the value we read from '/profile-specific/two' would have been 'twoDev' and _not_ 'two',
+	 *   simply because 'config-map-two-dev' would override the property value.
+	 *
+	 * 	 As such: @ConfigurationProperties("two") must be resolved from 'config-map-two'
+	 * 
+ */ + @Test + public void testTwo() { + this.webClient.get().uri("/profile-specific/two").exchange().expectStatus().isOk().expectBody(String.class) + .value(Matchers.equalTo("two")); + } + + /** + *
+	 *   'spring.cloud.kubernetes.config.includeProfileSpecificSources=false'
+	 *   'spring.cloud.kubernetes.config.sources[2].name=config-map-three'
+	 *
+	 *   We define config-map 'config-map-three', but we also define 'config-map-three-dev'.
+	 *   This tests proves that data will be read from 'config-map-three' _only_, even if 'config-map-three-dev'
+	 *   also exists. This happens because the 'includeProfileSpecificSources'  property is not defined at the source level,
+	 *   but it is defaulted from the root level, where we set it to false.
+	 *   If this would be incorrect, the value we read from '/profile-specific/three' would have been 'threeDev' and _not_ 'three',
+	 *   simply because 'config-map-three-dev' would override the property value.
+	 *
+	 * 	 As such: @ConfigurationProperties("three") must be resolved from 'config-map-three'
+	 * 
+ */ + @Test + public void testThree() { + this.webClient.get().uri("/profile-specific/three").exchange().expectStatus().isOk().expectBody(String.class) + .value(Matchers.equalTo("three")); + } + +} diff --git a/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/ConfigMapWithPrefixTests.java b/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/ConfigMapWithPrefixTests.java index 28c97f0939..beb4551b45 100644 --- a/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/ConfigMapWithPrefixTests.java +++ b/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/ConfigMapWithPrefixTests.java @@ -40,10 +40,10 @@ */ @ExtendWith(SpringExtension.class) @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, classes = WithPrefixApp.class, - properties = { "spring.cloud.bootstrap.name=same-key-with-prefix" }) + properties = { "spring.cloud.bootstrap.name=config-map-name-as-prefix" }) @AutoConfigureWebTestClient @EnableKubernetesMockClient(crud = true, https = false) -public class ConfigMapWithPrefixTests { +class ConfigMapWithPrefixTests { private static KubernetesClient mockClient; @@ -63,21 +63,20 @@ public static void setUpBeforeClass() { Map one = new HashMap<>(); one.put("one.property", "one"); - createConfigmap(mockClient, "config-map-one", one); + createConfigmap("config-map-one", one); Map two = new HashMap<>(); two.put("property", "two"); - createConfigmap(mockClient, "config-map-two", two); + createConfigmap("config-map-two", two); Map three = new HashMap<>(); three.put("property", "three"); - createConfigmap(mockClient, "config-map-three", three); + createConfigmap("config-map-three", three); } - private static void createConfigmap(KubernetesClient client, String name, Map data) { - - client.configMaps().inNamespace("spring-k8s") + private static void createConfigmap(String name, Map data) { + mockClient.configMaps().inNamespace("spring-k8s") .create(new ConfigMapBuilder().withNewMetadata().withName(name).endMetadata().addToData(data).build()); } @@ -92,7 +91,7 @@ private static void createConfigmap(KubernetesClient client, String name, Map new Fabric8ConfigMapPropertySource(mockClient, name, namespace, new MockEnvironment(), "", true)) - .isInstanceOf(IllegalStateException.class).hasMessage( - "Unable to read ConfigMap with name '" + name + "' in namespace '" + namespace + "'"); + assertThatThrownBy(() -> new Fabric8ConfigMapPropertySource(mockClient, name, namespace, new MockEnvironment(), + "", false, true)).isInstanceOf(IllegalStateException.class).hasMessage( + "Unable to read ConfigMap with name '" + name + "' in namespace '" + namespace + "'"); } @Test @@ -57,7 +56,7 @@ public void constructorShouldNotThrowExceptionOnFailureWhenFailFastIsDisabled() mockServer.expect().withPath(path).andReturn(500, "Internal Server Error").once(); assertThatNoException().isThrownBy(() -> new Fabric8ConfigMapPropertySource(mockClient, name, namespace, - new MockEnvironment(), "", false)); + new MockEnvironment(), "", false, false)); } } diff --git a/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/include_profile_specific_sources/IncludeProfileSpecificSourcesApp.java b/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/include_profile_specific_sources/IncludeProfileSpecificSourcesApp.java new file mode 100644 index 0000000000..992de0d88e --- /dev/null +++ b/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/include_profile_specific_sources/IncludeProfileSpecificSourcesApp.java @@ -0,0 +1,34 @@ +/* + * 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.fabric8.config.include_profile_specific_sources; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.cloud.kubernetes.fabric8.config.include_profile_specific_sources.properties.One; +import org.springframework.cloud.kubernetes.fabric8.config.include_profile_specific_sources.properties.Three; +import org.springframework.cloud.kubernetes.fabric8.config.include_profile_specific_sources.properties.Two; + +@SpringBootApplication +@EnableConfigurationProperties({ One.class, Two.class, Three.class }) +public class IncludeProfileSpecificSourcesApp { + + public static void main(String[] args) { + SpringApplication.run(IncludeProfileSpecificSourcesApp.class, args); + } + +} diff --git a/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/include_profile_specific_sources/controller/IncludeProfileSpecificSourcesController.java b/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/include_profile_specific_sources/controller/IncludeProfileSpecificSourcesController.java new file mode 100644 index 0000000000..e6aa453c74 --- /dev/null +++ b/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/include_profile_specific_sources/controller/IncludeProfileSpecificSourcesController.java @@ -0,0 +1,55 @@ +/* + * 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.fabric8.config.include_profile_specific_sources.controller; + +import org.springframework.cloud.kubernetes.fabric8.config.include_profile_specific_sources.properties.One; +import org.springframework.cloud.kubernetes.fabric8.config.include_profile_specific_sources.properties.Three; +import org.springframework.cloud.kubernetes.fabric8.config.include_profile_specific_sources.properties.Two; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +public class IncludeProfileSpecificSourcesController { + + private final One one; + + private final Two two; + + private final Three three; + + public IncludeProfileSpecificSourcesController(One one, Two two, Three three) { + this.one = one; + this.two = two; + this.three = three; + } + + @GetMapping("/profile-specific/one") + public String one() { + return one.getProperty(); + } + + @GetMapping("/profile-specific/two") + public String two() { + return two.getProperty(); + } + + @GetMapping("/profile-specific/three") + public String three() { + return three.getProperty(); + } + +} diff --git a/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/include_profile_specific_sources/properties/One.java b/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/include_profile_specific_sources/properties/One.java new file mode 100644 index 0000000000..af88a5c0f8 --- /dev/null +++ b/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/include_profile_specific_sources/properties/One.java @@ -0,0 +1,34 @@ +/* + * 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.fabric8.config.include_profile_specific_sources.properties; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +@ConfigurationProperties("one") +public class One { + + private String property; + + public String getProperty() { + return property; + } + + public void setProperty(String property) { + this.property = property; + } + +} diff --git a/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/include_profile_specific_sources/properties/Three.java b/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/include_profile_specific_sources/properties/Three.java new file mode 100644 index 0000000000..239f4374dc --- /dev/null +++ b/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/include_profile_specific_sources/properties/Three.java @@ -0,0 +1,34 @@ +/* + * 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.fabric8.config.include_profile_specific_sources.properties; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +@ConfigurationProperties("three") +public class Three { + + private String property; + + public String getProperty() { + return property; + } + + public void setProperty(String property) { + this.property = property; + } + +} diff --git a/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/include_profile_specific_sources/properties/Two.java b/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/include_profile_specific_sources/properties/Two.java new file mode 100644 index 0000000000..c14b1b0f40 --- /dev/null +++ b/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/include_profile_specific_sources/properties/Two.java @@ -0,0 +1,34 @@ +/* + * 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.fabric8.config.include_profile_specific_sources.properties; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +@ConfigurationProperties("two") +public class Two { + + private String property; + + public String getProperty() { + return property; + } + + public void setProperty(String property) { + this.property = property; + } + +} diff --git a/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/with_prefix/controller/Controller.java b/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/with_prefix/controller/Controller.java index 86886d1082..adbed9f95f 100644 --- a/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/with_prefix/controller/Controller.java +++ b/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/with_prefix/controller/Controller.java @@ -37,17 +37,17 @@ public Controller(One one, Two two, Three three) { this.three = three; } - @GetMapping("/one") + @GetMapping("/prefix/one") public String one() { return one.getProperty(); } - @GetMapping("/two") + @GetMapping("/prefix/two") public String two() { return two.getProperty(); } - @GetMapping("/three") + @GetMapping("/prefix/three") public String three() { return three.getProperty(); } diff --git a/spring-cloud-kubernetes-fabric8-config/src/test/resources/config-map-name-as-prefix.yaml b/spring-cloud-kubernetes-fabric8-config/src/test/resources/config-map-name-as-prefix.yaml new file mode 100644 index 0000000000..b52095e4d5 --- /dev/null +++ b/spring-cloud-kubernetes-fabric8-config/src/test/resources/config-map-name-as-prefix.yaml @@ -0,0 +1,14 @@ +spring: + application: + name: with-prefix + cloud: + kubernetes: + config: + useNameAsPrefix: true + namespace: spring-k8s + sources: + - name: config-map-one + useNameAsPrefix: false + - name: config-map-two + explicitPrefix: two + - name: config-map-three diff --git a/spring-cloud-kubernetes-fabric8-config/src/test/resources/include-profile-specific-sources.yaml b/spring-cloud-kubernetes-fabric8-config/src/test/resources/include-profile-specific-sources.yaml new file mode 100644 index 0000000000..54999aaa3a --- /dev/null +++ b/spring-cloud-kubernetes-fabric8-config/src/test/resources/include-profile-specific-sources.yaml @@ -0,0 +1,14 @@ +spring: + application: + name: include-profile-specific-sources + cloud: + kubernetes: + config: + includeProfileSpecificSources: false + namespace: spring-k8s + sources: + - name: config-map-one + includeProfileSpecificSources: true + - name: config-map-two + includeProfileSpecificSources: false + - name: config-map-three diff --git a/spring-cloud-kubernetes-integration-tests/run.sh b/spring-cloud-kubernetes-integration-tests/run.sh index 390a31f158..58bb4c7b02 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 0000000000..8a9932a9ba --- /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 0000000000..518bb63b79 --- /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 0000000000..03cad21681 --- /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 0000000000..d2dbefb9d2 --- /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 0000000000..b176c6a334 --- /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 0000000000..1b70c818c6 --- /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 0000000000..02d1570e1d --- /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 0000000000..d4f120a649 --- /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 0000000000..45aebedf46 --- /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 0000000000..32b4bd7e37 --- /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 0000000000..3672520dbd --- /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 0000000000..d58cf1b482 --- /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 0000000000..ba99611826 --- /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 0000000000..7e9b54fb55 --- /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 0000000000..5b253f9390 --- /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 0000000000..5f4a342cfb --- /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 0000000000..599b2ebfe6 --- /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 0000000000..c36208f89d --- /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 0000000000..1b70c818c6 --- /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 0000000000..0f74ddaee4 --- /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 0000000000..e42d0c3955 --- /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 0000000000..45aebedf46 --- /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 0000000000..32b4bd7e37 --- /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 0000000000..3672520dbd --- /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 0000000000..d58cf1b482 --- /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 0000000000..ba99611826 --- /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 0000000000..80fb9bf1f7 --- /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 b7714c419a..e3eec7eb07 100644 --- a/src/checkstyle/checkstyle-suppressions.xml +++ b/src/checkstyle/checkstyle-suppressions.xml @@ -13,4 +13,6 @@ + +