diff --git a/.circleci/config.yml b/.circleci/config.yml index 4f44fd06..205df285 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -14,12 +14,11 @@ # limitations under the License. # -version: 2 +orbs: + kube-orb: circleci/kubernetes@0.11.0 +version: 2.1 jobs: build: - branches: - ignore: - - gh-pages machine: true environment: _JAVA_OPTIONS: "-Xms1024m -Xmx2048m" @@ -34,7 +33,7 @@ jobs: - run: name: dependencies command: | - ./mvnw -s .settings.xml -U --fail-never dependency:go-offline -Dservice.occurence=${_SERVICE_OCCURENCE} || true + ./mvnw -U --fail-never dependency:go-offline -Dservice.occurence=${_SERVICE_OCCURENCE} || true - save_cache: paths: - ~/.m2 @@ -42,7 +41,7 @@ jobs: - run: name: Run regular tests command: | - ./mvnw -s .settings.xml clean install -Dservice.occurence=${_SERVICE_OCCURENCE} #org.jacoco:jacoco-maven-plugin:prepare-agent install -U -P sonar -nsu --batch-mode -Dmaven.test.redirectTestOutputToFile=true -Dorg.slf4j.simpleLogger.log.org.apache.maven.cli.transfer.Slf4jMavenTransferListener=warn + ./mvnw clean install -Dservice.occurence=${_SERVICE_OCCURENCE} #org.jacoco:jacoco-maven-plugin:prepare-agent install -U -P sonar -nsu --batch-mode -Dmaven.test.redirectTestOutputToFile=true -Dorg.slf4j.simpleLogger.log.org.apache.maven.cli.transfer.Slf4jMavenTransferListener=warn mkdir -p $HOME/artifacts/junit/ find . -type f -regex ".*/spring-cloud-*.*/target/*.*" -exec cp {} $HOME/artifacts/ \; find . -type f -regex ".*/target/.*-reports/.*" -exec cp {} $HOME/artifacts/junit/ \; @@ -52,6 +51,12 @@ jobs: command: | sudo apt update sudo apt install snapd + - kube-orb/install-kubectl + - run: + name: Run Kind Integration Tests + command: | + cd spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-configuration-watcher-it + ./run.sh - run: name: Launch Kubernetes with microk8s command: | diff --git a/README.adoc b/README.adoc index 0c5184f5..b90afb99 100644 --- a/README.adoc +++ b/README.adoc @@ -625,6 +625,10 @@ https://github.com/fabric8-quickstarts/spring-boot-camel-config[spring-boot-came === `PropertySource` Reload +WARNING: This functionality has been deprecated in the 2020.0 release. Please see +the <> controller for an alternative way +to achieve the same functionality. + Some applications may need to detect changes on external property sources and update their internal status to reflect the new configuration. The reload feature of Spring Cloud Kubernetes is able to trigger an application reload when a related `ConfigMap` or `Secret` changes. @@ -862,6 +866,187 @@ In Kubernetes service registration is controlled by the platform, the applicatio registration as it may do in other platforms. For this reason using `spring.cloud.service-registry.auto-registration.enabled` or setting `@EnableDiscoveryClient(autoRegister=false)` will have no effect in Spring Cloud Kubernetes. +[#spring-cloud-kubernetes-configuration-watcher] +## Spring Cloud Kubernetes Configuration Watcher + +Kubernetes provides the ability to https://kubernetes.io/docs/tasks/configure-pod-container/configure-pod-configmap/#add-configmap-data-to-a-volume[mount a ConfigMap or Secret as a volume] +in the container of your application. When the contents of the ConfigMap or Secret changes, the https://kubernetes.io/docs/tasks/configure-pod-container/configure-pod-configmap/#mounted-configmaps-are-updated-automatically[mounted volume will be updated with those changes]. + +However, Spring Boot will not automatically update those changes unless you restart the application. Spring Cloud +provides the ability refresh the application context without restarting the application by either hitting the +actuator endpoint `/refresh` or via publishing a `RefreshRemoteApplicationEvent` using Spring Cloud Bus. + +To achieve this configuration refresh of a Spring Cloud app running on Kubernetes, you can deploy the Spring Cloud +Kubernetes Configuration Watcher controller into your Kubernetes cluster. + +The application is published as a container and is available on https://hub.docker.com/repository/docker/springcloud/spring-cloud-kubernetes-configuration-watcher[Docker Hub]. + +Spring Cloud Kubernetes Configuration Watcher can send refresh notifications to applications in two ways. + +1. Over HTTP in which case the application being notified must of the `/refresh` actuator endpoint exposed and accessible from within the cluster +2. Using Spring Cloud Bus, in which case you will need a message broker deployed to your custer for the application to use. + +### Deployment YAML + +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-configuration-watcher + name: spring-cloud-kubernetes-configuration-watcher + spec: + ports: + - name: http + port: 8888 + targetPort: 8888 + selector: + app: spring-cloud-kubernetes-configuration-watcher + type: ClusterIP + - apiVersion: v1 + kind: ServiceAccount + metadata: + labels: + app: spring-cloud-kubernetes-configuration-watcher + name: spring-cloud-kubernetes-configuration-watcher + - apiVersion: rbac.authorization.k8s.io/v1 + kind: RoleBinding + metadata: + labels: + app: spring-cloud-kubernetes-configuration-watcher + name: spring-cloud-kubernetes-configuration-watcher:view + roleRef: + kind: Role + apiGroup: rbac.authorization.k8s.io + name: namespace-reader + subjects: + - kind: ServiceAccount + name: spring-cloud-kubernetes-configuration-watcher + - 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-configuration-watcher-deployment + spec: + selector: + matchLabels: + app: spring-cloud-kubernetes-configuration-watcher + template: + metadata: + labels: + app: spring-cloud-kubernetes-configuration-watcher + spec: + serviceAccount: spring-cloud-kubernetes-configuration-watcher + containers: + - name: spring-cloud-kubernetes-configuration-watcher + image: springcloud/spring-cloud-kubernetes-configuration-watcher:2.0.0-SNAPSHOT + imagePullPolicy: IfNotPresent + readinessProbe: + httpGet: + port: 8888 + path: /actuator/health/readiness + livenessProbe: + httpGet: + port: 8888 + path: /actuator/health/liveness + ports: + - containerPort: 8888 + +---- +==== + +The Service Account and associated Role Binding is important for Spring Cloud Kubernetes Configuration to work properly. +The controller needs access to read data about ConfigMaps, Pods, Services, Endpoints and Secrets in the Kubernetes cluster. + +### Monitoring ConfigMaps and Secrets + +Spring Cloud Kubernetes Configuration Watcher will react to changes in ConfigMaps with a label of `spring.cloud.kubernetes.config` with the value `true` +or any Secret with a label of `spring.cloud.kubernetes.secret` with the value `true`. If the ConfigMap or Secret does not have either of those labels +or the values of those labels is not `true` then any changes will be ignored. + +The labels Spring Cloud Kubernetes Configuration Watcher looks for on ConfigMaps and Secrets can be changed by setting +`spring.cloud.kubernetes.configuration.watcher.configLabel` and `spring.cloud.kubernetes.configuration.watcher.secretLabel` respectively. + +If a change is made to a ConfigMap or Secret with valid labels then Spring Cloud Kubernetes Configuration Watcher will take the name of the ConfigMap or Secret +and send a notification to the application with that name. + +### HTTP Implementation + +The HTTP implementation is what is used by default. When this implementation is used Spring Cloud Kubernetes Configuration Watcher and a +change to a ConfigMap or Secret occurs then the HTTP implementation will use the Spring Cloud Kubernetes Discovery Client to fetch all +instances of the application which match the name of the ConfigMap or Secret and send an HTTP POST request to the application's actuator +`/refresh` endpoint. By default it will send the post request to `/actuator/refresh` using the port registered in the discovery client. + +#### Non-Default Management Port and Actuator Path + +If the application is using a non-default actuator path and/or using a different port for the management endpoints, the Kubernetes service for the application +can add an annotation called `boot.spring.io/actuator` and set its value to the path and port used by the application. For example + +==== +[source,yaml] +---- +apiVersion: v1 +kind: Service +metadata: + labels: + app: config-map-demo + name: config-map-demo + annotations: + boot.spring.io/actuator: http://:9090/myactuator/home +spec: + ports: + - name: http + port: 8080 + targetPort: 8080 + selector: + app: config-map-demo +---- +==== + + +Another way you can choose to configure the actuator path and/or management port is by setting +`spring.cloud.kubernetes.configuration.watcher.actuatorPath` and `spring.cloud.kubernetes.configuration.watcher.actuatorPort`. + +### Messaging Implementation + +The messaging implementation can be enabled by setting the profile `bus` when the Spring Cloud Kubernetes Configuration Watcher +application is deployed to Kubernetes. + +WARNING: Currently the only supported message broker is RabbitMQ + +### Configuring RabbitMQ + +When the `bus` profile is enabled you will need to configure Spring RabbitMQ to point it to the location of the RabbitMQ +instance you would like to use as well as any credentials necessary to authenticate. This can be done +by setting the standard Spring RabbitMQ properties, for example + +==== +[source,yaml] +---- +spring: + rabbitmq: + username: user + password: password + host: rabbitmq +---- +==== + == Examples Spring Cloud Kubernetes tries to make it transparent for your applications to consume Kubernetes Native Services by diff --git a/docs/src/main/asciidoc/property-source-config.adoc b/docs/src/main/asciidoc/property-source-config.adoc index 72b435b3..9e2f3184 100644 --- a/docs/src/main/asciidoc/property-source-config.adoc +++ b/docs/src/main/asciidoc/property-source-config.adoc @@ -466,6 +466,10 @@ https://github.com/fabric8-quickstarts/spring-boot-camel-config[spring-boot-came === `PropertySource` Reload +WARNING: This functionality has been deprecated in the 2020.0 release. Please see +the <> controller for an alternative way +to achieve the same functionality. + Some applications may need to detect changes on external property sources and update their internal status to reflect the new configuration. The reload feature of Spring Cloud Kubernetes is able to trigger an application reload when a related `ConfigMap` or `Secret` changes. diff --git a/docs/src/main/asciidoc/spring-cloud-kubernetes-configuration-watcher.adoc b/docs/src/main/asciidoc/spring-cloud-kubernetes-configuration-watcher.adoc new file mode 100644 index 00000000..d1f737d9 --- /dev/null +++ b/docs/src/main/asciidoc/spring-cloud-kubernetes-configuration-watcher.adoc @@ -0,0 +1,180 @@ +[#spring-cloud-kubernetes-configuration-watcher] +## Spring Cloud Kubernetes Configuration Watcher + +Kubernetes provides the ability to https://kubernetes.io/docs/tasks/configure-pod-container/configure-pod-configmap/#add-configmap-data-to-a-volume[mount a ConfigMap or Secret as a volume] +in the container of your application. When the contents of the ConfigMap or Secret changes, the https://kubernetes.io/docs/tasks/configure-pod-container/configure-pod-configmap/#mounted-configmaps-are-updated-automatically[mounted volume will be updated with those changes]. + +However, Spring Boot will not automatically update those changes unless you restart the application. Spring Cloud +provides the ability refresh the application context without restarting the application by either hitting the +actuator endpoint `/refresh` or via publishing a `RefreshRemoteApplicationEvent` using Spring Cloud Bus. + +To achieve this configuration refresh of a Spring Cloud app running on Kubernetes, you can deploy the Spring Cloud +Kubernetes Configuration Watcher controller into your Kubernetes cluster. + +The application is published as a container and is available on https://hub.docker.com/repository/docker/springcloud/spring-cloud-kubernetes-configuration-watcher[Docker Hub]. + +Spring Cloud Kubernetes Configuration Watcher can send refresh notifications to applications in two ways. + +1. Over HTTP in which case the application being notified must of the `/refresh` actuator endpoint exposed and accessible from within the cluster +2. Using Spring Cloud Bus, in which case you will need a message broker deployed to your custer for the application to use. + +### Deployment YAML + +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-configuration-watcher + name: spring-cloud-kubernetes-configuration-watcher + spec: + ports: + - name: http + port: 8888 + targetPort: 8888 + selector: + app: spring-cloud-kubernetes-configuration-watcher + type: ClusterIP + - apiVersion: v1 + kind: ServiceAccount + metadata: + labels: + app: spring-cloud-kubernetes-configuration-watcher + name: spring-cloud-kubernetes-configuration-watcher + - apiVersion: rbac.authorization.k8s.io/v1 + kind: RoleBinding + metadata: + labels: + app: spring-cloud-kubernetes-configuration-watcher + name: spring-cloud-kubernetes-configuration-watcher:view + roleRef: + kind: Role + apiGroup: rbac.authorization.k8s.io + name: namespace-reader + subjects: + - kind: ServiceAccount + name: spring-cloud-kubernetes-configuration-watcher + - 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-configuration-watcher-deployment + spec: + selector: + matchLabels: + app: spring-cloud-kubernetes-configuration-watcher + template: + metadata: + labels: + app: spring-cloud-kubernetes-configuration-watcher + spec: + serviceAccount: spring-cloud-kubernetes-configuration-watcher + containers: + - name: spring-cloud-kubernetes-configuration-watcher + image: springcloud/spring-cloud-kubernetes-configuration-watcher:2.0.0-SNAPSHOT + imagePullPolicy: IfNotPresent + readinessProbe: + httpGet: + port: 8888 + path: /actuator/health/readiness + livenessProbe: + httpGet: + port: 8888 + path: /actuator/health/liveness + ports: + - containerPort: 8888 + +---- +==== + +The Service Account and associated Role Binding is important for Spring Cloud Kubernetes Configuration to work properly. +The controller needs access to read data about ConfigMaps, Pods, Services, Endpoints and Secrets in the Kubernetes cluster. + +### Monitoring ConfigMaps and Secrets + +Spring Cloud Kubernetes Configuration Watcher will react to changes in ConfigMaps with a label of `spring.cloud.kubernetes.config` with the value `true` +or any Secret with a label of `spring.cloud.kubernetes.secret` with the value `true`. If the ConfigMap or Secret does not have either of those labels +or the values of those labels is not `true` then any changes will be ignored. + +The labels Spring Cloud Kubernetes Configuration Watcher looks for on ConfigMaps and Secrets can be changed by setting +`spring.cloud.kubernetes.configuration.watcher.configLabel` and `spring.cloud.kubernetes.configuration.watcher.secretLabel` respectively. + +If a change is made to a ConfigMap or Secret with valid labels then Spring Cloud Kubernetes Configuration Watcher will take the name of the ConfigMap or Secret +and send a notification to the application with that name. + +### HTTP Implementation + +The HTTP implementation is what is used by default. When this implementation is used Spring Cloud Kubernetes Configuration Watcher and a +change to a ConfigMap or Secret occurs then the HTTP implementation will use the Spring Cloud Kubernetes Discovery Client to fetch all +instances of the application which match the name of the ConfigMap or Secret and send an HTTP POST request to the application's actuator +`/refresh` endpoint. By default it will send the post request to `/actuator/refresh` using the port registered in the discovery client. + +#### Non-Default Management Port and Actuator Path + +If the application is using a non-default actuator path and/or using a different port for the management endpoints, the Kubernetes service for the application +can add an annotation called `boot.spring.io/actuator` and set its value to the path and port used by the application. For example + +==== +[source,yaml] +---- +apiVersion: v1 +kind: Service +metadata: + labels: + app: config-map-demo + name: config-map-demo + annotations: + boot.spring.io/actuator: http://:9090/myactuator/home +spec: + ports: + - name: http + port: 8080 + targetPort: 8080 + selector: + app: config-map-demo +---- +==== + + +Another way you can choose to configure the actuator path and/or management port is by setting +`spring.cloud.kubernetes.configuration.watcher.actuatorPath` and `spring.cloud.kubernetes.configuration.watcher.actuatorPort`. + +### Messaging Implementation + +The messaging implementation can be enabled by setting the profile `bus` when the Spring Cloud Kubernetes Configuration Watcher +application is deployed to Kubernetes. + +WARNING: Currently the only supported message broker is RabbitMQ + +### Configuring RabbitMQ + +When the `bus` profile is enabled you will need to configure Spring RabbitMQ to point it to the location of the RabbitMQ +instance you would like to use as well as any credentials necessary to authenticate. This can be done +by setting the standard Spring RabbitMQ properties, for example + +==== +[source,yaml] +---- +spring: + rabbitmq: + username: user + password: password + host: rabbitmq +---- +==== diff --git a/docs/src/main/asciidoc/spring-cloud-kubernetes.adoc b/docs/src/main/asciidoc/spring-cloud-kubernetes.adoc index 2b194831..83839184 100644 --- a/docs/src/main/asciidoc/spring-cloud-kubernetes.adoc +++ b/docs/src/main/asciidoc/spring-cloud-kubernetes.adoc @@ -26,6 +26,8 @@ include::security-service-accounts.adoc[] include::service-registry.adoc[] +include::spring-cloud-kubernetes-configuration-watcher.adoc[] + include::examples.adoc[] include::other-resources.adoc[] diff --git a/pom.xml b/pom.xml index 9acd9f4b..02f1dcd1 100644 --- a/pom.xml +++ b/pom.xml @@ -65,12 +65,14 @@ 3.0.0-SNAPSHOT 3.0.0-SNAPSHOT + 3.0.0-SNAPSHOT + 3.0.0-SNAPSHOT 3.5 2.8.2 - 2.18.1 - 2.21.0 + 2.22.2 + 2.22.2 4.4.0 2.4.12 3.0.2 @@ -94,6 +96,7 @@ spring-cloud-kubernetes-examples spring-cloud-kubernetes-leader spring-cloud-kubernetes-istio + spring-cloud-kubernetes-controllers spring-cloud-kubernetes-integration-tests docs spring-cloud-kubernetes-loadbalancer @@ -127,6 +130,22 @@ import + + org.springframework.cloud + spring-cloud-bus-dependencies + ${spring-cloud-bus.version} + pom + import + + + + org.springframework.cloud + spring-cloud-contract-dependencies + ${spring-cloud-contract.version} + pom + import + + org.codehaus.groovy groovy-all diff --git a/spring-cloud-kubernetes-config/pom.xml b/spring-cloud-kubernetes-config/pom.xml index c5df2ea0..c6fc4afd 100644 --- a/spring-cloud-kubernetes-config/pom.xml +++ b/spring-cloud-kubernetes-config/pom.xml @@ -54,7 +54,12 @@ org.springframework.cloud - spring-cloud-context + spring-cloud-starter + + + + org.springframework.cloud + spring-cloud-starter-bootstrap diff --git a/spring-cloud-kubernetes-config/src/main/java/org/springframework/cloud/kubernetes/config/reload/ConfigReloadDefaultAutoConfiguration.java b/spring-cloud-kubernetes-config/src/main/java/org/springframework/cloud/kubernetes/config/reload/ConfigReloadDefaultAutoConfiguration.java new file mode 100644 index 00000000..d4097f97 --- /dev/null +++ b/spring-cloud-kubernetes-config/src/main/java/org/springframework/cloud/kubernetes/config/reload/ConfigReloadDefaultAutoConfiguration.java @@ -0,0 +1,125 @@ +/* + * 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.config.reload; + +import java.util.concurrent.ThreadLocalRandom; + +import io.fabric8.kubernetes.client.KubernetesClient; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.cloud.kubernetes.config.ConfigMapPropertySourceLocator; +import org.springframework.cloud.kubernetes.config.SecretsPropertySourceLocator; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.env.AbstractEnvironment; +import org.springframework.scheduling.annotation.EnableAsync; +import org.springframework.scheduling.annotation.EnableScheduling; + +/** + * @author Ryan Baxter + */ +@Configuration(proxyBeanMethods = false) +@ConditionalOnProperty(value = "spring.cloud.kubernetes.enabled", matchIfMissing = true) +@ConditionalOnMissingBean(ConfigReloadAutoConfiguration.class) +@EnableConfigurationProperties(ConfigReloadProperties.class) +public class ConfigReloadDefaultAutoConfiguration { + + /** + * Configuration reload must be enabled explicitly. + */ + @ConditionalOnProperty("spring.cloud.kubernetes.reload.enabled") + @EnableScheduling + @EnableAsync + protected static class ConfigReloadAutoConfigurationBeans { + + @Autowired + private AbstractEnvironment environment; + + @Autowired + private KubernetesClient kubernetesClient; + + @Autowired + private ConfigMapPropertySourceLocator configMapPropertySourceLocator; + + @Autowired + private SecretsPropertySourceLocator secretsPropertySourceLocator; + + /** + * @param properties config reload properties + * @param strategy configuration update strategy + * @return a bean that listen to configuration changes and fire a reload. + */ + @Bean + @ConditionalOnMissingBean + public ConfigurationChangeDetector propertyChangeWatcher( + ConfigReloadProperties properties, ConfigurationUpdateStrategy strategy) { + switch (properties.getMode()) { + case POLLING: + return new PollingConfigurationChangeDetector(this.environment, + properties, this.kubernetesClient, strategy, + this.configMapPropertySourceLocator, + this.secretsPropertySourceLocator); + case EVENT: + return new EventBasedConfigurationChangeDetector(this.environment, + properties, this.kubernetesClient, strategy, + this.configMapPropertySourceLocator, + this.secretsPropertySourceLocator); + } + throw new IllegalStateException( + "Unsupported configuration reload mode: " + properties.getMode()); + } + + /** + * @param properties config reload properties + * @param ctx application context + * @param restarter restart endpoint + * @param refresher context refresher + * @return provides the action to execute when the configuration changes. + */ + @Bean + @ConditionalOnMissingBean + public ConfigurationUpdateStrategy configurationUpdateStrategy( + ConfigReloadProperties properties, ConfigurableApplicationContext ctx) { + switch (properties.getStrategy()) { + case SHUTDOWN: + return new ConfigurationUpdateStrategy(properties.getStrategy().name(), + () -> { + wait(properties); + ctx.close(); + }); + } + throw new IllegalStateException("Unsupported configuration update strategy: " + + properties.getStrategy()); + } + + private static void wait(ConfigReloadProperties properties) { + final long waitMillis = ThreadLocalRandom.current() + .nextLong(properties.getMaxWaitForRestart().toMillis()); + try { + Thread.sleep(waitMillis); + } + catch (InterruptedException ignored) { + } + } + + } + +} diff --git a/spring-cloud-kubernetes-config/src/main/java/org/springframework/cloud/kubernetes/config/reload/ConfigurationChangeDetector.java b/spring-cloud-kubernetes-config/src/main/java/org/springframework/cloud/kubernetes/config/reload/ConfigurationChangeDetector.java index 2b6bace6..12034a49 100644 --- a/spring-cloud-kubernetes-config/src/main/java/org/springframework/cloud/kubernetes/config/reload/ConfigurationChangeDetector.java +++ b/spring-cloud-kubernetes-config/src/main/java/org/springframework/cloud/kubernetes/config/reload/ConfigurationChangeDetector.java @@ -99,8 +99,9 @@ public abstract class ConfigurationChangeDetector { List l2) { if (l1.size() != l2.size()) { - this.log.warn("The current number of Confimap PropertySources does not match " - + "the ones loaded from the Kubernetes - No reload will take place"); + this.log.warn( + "The current number of ConfigMap PropertySources does not match " + + "the ones loaded from the Kubernetes - No reload will take place"); return false; } diff --git a/spring-cloud-kubernetes-config/src/main/java/org/springframework/cloud/kubernetes/config/reload/EventBasedConfigurationChangeDetector.java b/spring-cloud-kubernetes-config/src/main/java/org/springframework/cloud/kubernetes/config/reload/EventBasedConfigurationChangeDetector.java index 32a9d463..43b675f6 100644 --- a/spring-cloud-kubernetes-config/src/main/java/org/springframework/cloud/kubernetes/config/reload/EventBasedConfigurationChangeDetector.java +++ b/spring-cloud-kubernetes-config/src/main/java/org/springframework/cloud/kubernetes/config/reload/EventBasedConfigurationChangeDetector.java @@ -74,6 +74,10 @@ public class EventBasedConfigurationChangeDetector extends ConfigurationChangeDe @Override public void eventReceived(Action action, ConfigMap configMap) { + if (log.isDebugEnabled()) { + log.debug(name + " received event for ConfigMap " + + configMap.getMetadata().getName()); + } onEvent(configMap); } @@ -99,6 +103,10 @@ public class EventBasedConfigurationChangeDetector extends ConfigurationChangeDe this.kubernetesClient.secrets().watch(new Watcher() { @Override public void eventReceived(Action action, Secret secret) { + if (log.isDebugEnabled()) { + log.debug(name + " received and event for Secret " + + secret.getMetadata().getName()); + } onEvent(secret); } @@ -138,7 +146,7 @@ public class EventBasedConfigurationChangeDetector extends ConfigurationChangeDe } } - private void onEvent(ConfigMap configMap) { + protected void onEvent(ConfigMap configMap) { boolean changed = changed( locateMapPropertySources(this.configMapPropertySourceLocator, this.environment), @@ -149,7 +157,7 @@ public class EventBasedConfigurationChangeDetector extends ConfigurationChangeDe } } - private void onEvent(Secret secret) { + protected void onEvent(Secret secret) { boolean changed = changed( locateMapPropertySources(this.secretsPropertySourceLocator, this.environment), diff --git a/spring-cloud-kubernetes-controllers/pom.xml b/spring-cloud-kubernetes-controllers/pom.xml new file mode 100644 index 00000000..5be61521 --- /dev/null +++ b/spring-cloud-kubernetes-controllers/pom.xml @@ -0,0 +1,19 @@ + + + + spring-cloud-kubernetes + org.springframework.cloud + 2.0.0-SNAPSHOT + + 4.0.0 + pom + + spring-cloud-kubernetes-controllers + + + spring-cloud-kubernetes-configuration-watcher + + + diff --git a/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configuration-watcher/k8s/configmap.yaml b/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configuration-watcher/k8s/configmap.yaml new file mode 100644 index 00000000..548aabd8 --- /dev/null +++ b/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configuration-watcher/k8s/configmap.yaml @@ -0,0 +1,21 @@ +apiVersion: v1 +data: + application.yaml: |- + spring: + rabbitmq: + username: rbaxter + password: password + logging: + level: + org: + springframework: + cloud: + kubernetes: TRACE + management: + endpoints: + web: + exposure: + include: "*" +kind: ConfigMap +metadata: + name: spring-cloud-kubernetes-configuration-watcher diff --git a/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configuration-watcher/k8s/deployment.yaml b/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configuration-watcher/k8s/deployment.yaml new file mode 100644 index 00000000..9404f7b7 --- /dev/null +++ b/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configuration-watcher/k8s/deployment.yaml @@ -0,0 +1,74 @@ +--- +apiVersion: v1 +kind: List +items: + - apiVersion: v1 + kind: Service + metadata: + labels: + app: spring-cloud-kubernetes-configuration-watcher + name: spring-cloud-kubernetes-configuration-watcher + spec: + ports: + - name: http + port: 8888 + targetPort: 8888 + selector: + app: spring-cloud-kubernetes-configuration-watcher + type: ClusterIP + - apiVersion: v1 + kind: ServiceAccount + metadata: + labels: + app: spring-cloud-kubernetes-configuration-watcher + name: spring-cloud-kubernetes-configuration-watcher + - apiVersion: rbac.authorization.k8s.io/v1 + kind: RoleBinding + metadata: + labels: + app: spring-cloud-kubernetes-configuration-watcher + name: spring-cloud-kubernetes-configuration-watcher:view + roleRef: + kind: Role + apiGroup: rbac.authorization.k8s.io + name: namespace-reader + subjects: + - kind: ServiceAccount + name: spring-cloud-kubernetes-configuration-watcher + - 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-configuration-watcher-deployment + spec: + selector: + matchLabels: + app: spring-cloud-kubernetes-configuration-watcher + template: + metadata: + labels: + app: spring-cloud-kubernetes-configuration-watcher + spec: + serviceAccount: spring-cloud-kubernetes-configuration-watcher + containers: + - name: spring-cloud-kubernetes-configuration-watcher + image: springcloud/spring-cloud-kubernetes-configuration-watcher:2.0.0-SNAPSHOT + imagePullPolicy: IfNotPresent + 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-configuration-watcher/pom.xml b/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configuration-watcher/pom.xml new file mode 100644 index 00000000..89ede593 --- /dev/null +++ b/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configuration-watcher/pom.xml @@ -0,0 +1,173 @@ + + + + spring-cloud-kubernetes-controllers + org.springframework.cloud + 2.0.0-SNAPSHOT + + 4.0.0 + + spring-cloud-kubernetes-configuration-watcher + + + 1.8.0 + openjdk:8u222-slim + springcloud + 4.1.0 + + + + + org.springframework.cloud + spring-cloud-starter-kubernetes-config + + + org.springframework.cloud + spring-cloud-starter-kubernetes + + + org.springframework.cloud + spring-cloud-starter-kubernetes-all + + + org.springframework.cloud + spring-cloud-starter-bus-amqp + + + org.springframework.boot + spring-boot-starter-actuator + + + org.springframework.boot + spring-boot-starter-webflux + + + + org.springframework.boot + spring-boot-starter-test + test + + + org.springframework.cloud + spring-cloud-starter-contract-stub-runner + test + + + io.projectreactor + reactor-test + test + + + org.springframework.amqp + spring-rabbit-test + test + + + org.junit.vintage + junit-vintage-engine + 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-configuration-watcher/skaffold.yaml b/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configuration-watcher/skaffold.yaml new file mode 100644 index 00000000..4157f0d4 --- /dev/null +++ b/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configuration-watcher/skaffold.yaml @@ -0,0 +1,20 @@ +apiVersion: skaffold/v2alpha3 +kind: Config +metadata: + name: spring-cloud-kubernetes-configuration-watcher +build: + artifacts: + - image: springcloud/spring-cloud-kubernetes-configuration-watcher + 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-configuration-watcher/src/main/java/org/springframework/cloud/kubernetes/configuration/watcher/BusEventBasedConfigurationWatcherChangeDetector.java b/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configuration-watcher/src/main/java/org/springframework/cloud/kubernetes/configuration/watcher/BusEventBasedConfigurationWatcherChangeDetector.java new file mode 100644 index 00000000..9a0485d1 --- /dev/null +++ b/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configuration-watcher/src/main/java/org/springframework/cloud/kubernetes/configuration/watcher/BusEventBasedConfigurationWatcherChangeDetector.java @@ -0,0 +1,80 @@ +/* + * 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.configuration.watcher; + +import io.fabric8.kubernetes.api.model.ConfigMap; +import io.fabric8.kubernetes.api.model.Secret; +import io.fabric8.kubernetes.client.KubernetesClient; +import reactor.core.publisher.Mono; + +import org.springframework.cloud.bus.BusProperties; +import org.springframework.cloud.bus.event.RefreshRemoteApplicationEvent; +import org.springframework.cloud.kubernetes.config.ConfigMapPropertySourceLocator; +import org.springframework.cloud.kubernetes.config.SecretsPropertySourceLocator; +import org.springframework.cloud.kubernetes.config.reload.ConfigReloadProperties; +import org.springframework.cloud.kubernetes.config.reload.ConfigurationUpdateStrategy; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.context.ApplicationEventPublisherAware; +import org.springframework.core.env.AbstractEnvironment; +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; + +/** + * @author Ryan Baxter + */ +public class BusEventBasedConfigurationWatcherChangeDetector extends + ConfigurationWatcherChangeDetector implements ApplicationEventPublisherAware { + + private ApplicationEventPublisher applicationEventPublisher; + + private BusProperties busProperties; + + public BusEventBasedConfigurationWatcherChangeDetector( + AbstractEnvironment environment, ConfigReloadProperties properties, + KubernetesClient kubernetesClient, ConfigurationUpdateStrategy strategy, + ConfigMapPropertySourceLocator configMapPropertySourceLocator, + SecretsPropertySourceLocator secretsPropertySourceLocator, + BusProperties busProperties, + ConfigurationWatcherConfigurationProperties k8SConfigurationProperties, + ThreadPoolTaskExecutor threadPoolTaskExecutor) { + super(environment, properties, kubernetesClient, strategy, + configMapPropertySourceLocator, secretsPropertySourceLocator, + k8SConfigurationProperties, threadPoolTaskExecutor); + + this.busProperties = busProperties; + } + + @Override + protected Mono triggerRefresh(Secret secret) { + this.applicationEventPublisher.publishEvent(new RefreshRemoteApplicationEvent( + secret, busProperties.getId(), secret.getMetadata().getName())); + return Mono.empty(); + } + + @Override + protected Mono triggerRefresh(ConfigMap configMap) { + this.applicationEventPublisher.publishEvent(new RefreshRemoteApplicationEvent( + configMap, busProperties.getId(), configMap.getMetadata().getName())); + return Mono.empty(); + } + + @Override + public void setApplicationEventPublisher( + ApplicationEventPublisher applicationEventPublisher) { + this.applicationEventPublisher = applicationEventPublisher; + } + +} diff --git a/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configuration-watcher/src/main/java/org/springframework/cloud/kubernetes/configuration/watcher/ConfigurationWatcherApplication.java b/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configuration-watcher/src/main/java/org/springframework/cloud/kubernetes/configuration/watcher/ConfigurationWatcherApplication.java new file mode 100644 index 00000000..e58da7e5 --- /dev/null +++ b/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configuration-watcher/src/main/java/org/springframework/cloud/kubernetes/configuration/watcher/ConfigurationWatcherApplication.java @@ -0,0 +1,37 @@ +/* + * Copyright 2013-2019 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.configuration.watcher; + +import org.springframework.boot.actuate.autoconfigure.amqp.RabbitHealthContributorAutoConfiguration; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.builder.SpringApplicationBuilder; +import org.springframework.cloud.function.context.config.ContextFunctionCatalogAutoConfiguration; +import org.springframework.context.annotation.Configuration; + +/** + * @author Ryan Baxter + */ +@Configuration(proxyBeanMethods = false) +@SpringBootApplication(exclude = { ContextFunctionCatalogAutoConfiguration.class, + RabbitHealthContributorAutoConfiguration.class }) +public class ConfigurationWatcherApplication { + + public static void main(String[] args) { + new SpringApplicationBuilder(ConfigurationWatcherApplication.class).run(args); + } + +} diff --git a/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configuration-watcher/src/main/java/org/springframework/cloud/kubernetes/configuration/watcher/ConfigurationWatcherAutoConfiguration.java b/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configuration-watcher/src/main/java/org/springframework/cloud/kubernetes/configuration/watcher/ConfigurationWatcherAutoConfiguration.java new file mode 100644 index 00000000..e94429fa --- /dev/null +++ b/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configuration-watcher/src/main/java/org/springframework/cloud/kubernetes/configuration/watcher/ConfigurationWatcherAutoConfiguration.java @@ -0,0 +1,96 @@ +/* + * 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.configuration.watcher; + +import io.fabric8.kubernetes.client.KubernetesClient; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.boot.actuate.autoconfigure.amqp.RabbitHealthContributorAutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.cloud.bus.BusProperties; +import org.springframework.cloud.function.context.config.ContextFunctionCatalogAutoConfiguration; +import org.springframework.cloud.kubernetes.config.ConfigMapPropertySourceLocator; +import org.springframework.cloud.kubernetes.config.SecretsPropertySourceLocator; +import org.springframework.cloud.kubernetes.config.reload.ConfigReloadProperties; +import org.springframework.cloud.kubernetes.config.reload.ConfigurationUpdateStrategy; +import org.springframework.cloud.kubernetes.discovery.reactive.KubernetesReactiveDiscoveryClient; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.context.annotation.Profile; +import org.springframework.core.env.AbstractEnvironment; +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; +import org.springframework.web.reactive.function.client.WebClient; + +/** + * @author Ryan Baxter + */ +@Configuration(proxyBeanMethods = false) +@EnableConfigurationProperties({ ConfigurationWatcherConfigurationProperties.class }) +public class ConfigurationWatcherAutoConfiguration { + + protected Log log = LogFactory.getLog(getClass()); + + @Bean + @ConditionalOnMissingBean + public WebClient webClient(WebClient.Builder webClientBuilder) { + return webClientBuilder.build(); + } + + @Bean + @ConditionalOnMissingBean(ConfigurationWatcherChangeDetector.class) + public ConfigurationWatcherChangeDetector httpBasedConfigurationWatchChangeDetector( + AbstractEnvironment environment, KubernetesClient kubernetesClient, + ConfigMapPropertySourceLocator configMapPropertySourceLocator, + SecretsPropertySourceLocator secretsPropertySourceLocator, + ConfigReloadProperties properties, ConfigurationUpdateStrategy strategy, + ConfigurationWatcherConfigurationProperties k8SConfigurationProperties, + ThreadPoolTaskExecutor threadFactory, WebClient webClient, + KubernetesReactiveDiscoveryClient kubernetesReactiveDiscoveryClient) { + return new HttpBasedConfigurationWatchChangeDetector(environment, properties, + kubernetesClient, strategy, configMapPropertySourceLocator, + secretsPropertySourceLocator, k8SConfigurationProperties, threadFactory, + webClient, kubernetesReactiveDiscoveryClient); + } + + @Configuration + @Profile("bus") + @Import({ ContextFunctionCatalogAutoConfiguration.class, + RabbitHealthContributorAutoConfiguration.class }) + static class BusConfiguration { + + @Bean + @ConditionalOnMissingBean(ConfigurationWatcherChangeDetector.class) + public ConfigurationWatcherChangeDetector busPropertyChangeWatcher( + BusProperties busProperties, AbstractEnvironment environment, + KubernetesClient kubernetesClient, + ConfigMapPropertySourceLocator configMapPropertySourceLocator, + SecretsPropertySourceLocator secretsPropertySourceLocator, + ConfigReloadProperties properties, ConfigurationUpdateStrategy strategy, + ConfigurationWatcherConfigurationProperties k8SConfigurationProperties, + ThreadPoolTaskExecutor threadFactory) { + return new BusEventBasedConfigurationWatcherChangeDetector(environment, + properties, kubernetesClient, strategy, + configMapPropertySourceLocator, secretsPropertySourceLocator, + busProperties, k8SConfigurationProperties, threadFactory); + } + + } + +} diff --git a/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configuration-watcher/src/main/java/org/springframework/cloud/kubernetes/configuration/watcher/ConfigurationWatcherChangeDetector.java b/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configuration-watcher/src/main/java/org/springframework/cloud/kubernetes/configuration/watcher/ConfigurationWatcherChangeDetector.java new file mode 100644 index 00000000..2551848e --- /dev/null +++ b/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configuration-watcher/src/main/java/org/springframework/cloud/kubernetes/configuration/watcher/ConfigurationWatcherChangeDetector.java @@ -0,0 +1,126 @@ +/* + * 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.configuration.watcher; + +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +import io.fabric8.kubernetes.api.model.ConfigMap; +import io.fabric8.kubernetes.api.model.Secret; +import io.fabric8.kubernetes.client.KubernetesClient; +import reactor.core.publisher.Mono; + +import org.springframework.cloud.kubernetes.config.ConfigMapPropertySourceLocator; +import org.springframework.cloud.kubernetes.config.SecretsPropertySourceLocator; +import org.springframework.cloud.kubernetes.config.reload.ConfigReloadProperties; +import org.springframework.cloud.kubernetes.config.reload.ConfigurationUpdateStrategy; +import org.springframework.cloud.kubernetes.config.reload.EventBasedConfigurationChangeDetector; +import org.springframework.core.env.AbstractEnvironment; +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; + +/** + * @author Ryan Baxter + */ +public abstract class ConfigurationWatcherChangeDetector + extends EventBasedConfigurationChangeDetector { + + private ScheduledExecutorService executorService; + + protected ConfigurationWatcherConfigurationProperties k8SConfigurationProperties; + + public ConfigurationWatcherChangeDetector(AbstractEnvironment environment, + ConfigReloadProperties properties, KubernetesClient kubernetesClient, + ConfigurationUpdateStrategy strategy, + ConfigMapPropertySourceLocator configMapPropertySourceLocator, + SecretsPropertySourceLocator secretsPropertySourceLocator, + ConfigurationWatcherConfigurationProperties k8SConfigurationProperties, + ThreadPoolTaskExecutor threadPoolTaskExecutor) { + super(environment, properties, kubernetesClient, strategy, + configMapPropertySourceLocator, secretsPropertySourceLocator); + this.executorService = Executors.newScheduledThreadPool( + k8SConfigurationProperties.getThreadPoolSize(), threadPoolTaskExecutor); + this.k8SConfigurationProperties = k8SConfigurationProperties; + } + + @Override + protected void onEvent(ConfigMap configMap) { + if (isSpringCloudKubernetesConfig(configMap)) { + if (log.isDebugEnabled()) { + log.debug("Scheduling remote refresh event to be published for ConfigMap " + + configMap.getMetadata().getName() + " to be published in " + + k8SConfigurationProperties.getRefreshDelay().toMillis() + + " milliseconds"); + } + executorService.schedule(() -> triggerRefresh(configMap).subscribe(), + k8SConfigurationProperties.getRefreshDelay().toMillis(), + TimeUnit.MILLISECONDS); + } + else { + if (log.isDebugEnabled()) { + log.debug("Not publishing event. ConfigMap " + + configMap.getMetadata().getName() + + " does not contain the label " + + k8SConfigurationProperties.getConfigLabel()); + } + } + } + + protected boolean isSpringCloudKubernetesConfig(ConfigMap configMap) { + if (configMap.getMetadata() == null + || configMap.getMetadata().getLabels() == null) { + return false; + } + return Boolean.parseBoolean(configMap.getMetadata().getLabels() + .getOrDefault(k8SConfigurationProperties.getConfigLabel(), "false")); + } + + protected boolean isSpringCloudKubernetesSecret(Secret secret) { + if (secret.getMetadata() == null || secret.getMetadata().getLabels() == null) { + return false; + } + return Boolean.parseBoolean(secret.getMetadata().getLabels() + .getOrDefault(k8SConfigurationProperties.getSecretLabel(), "false")); + } + + protected abstract Mono triggerRefresh(Secret secret); + + protected abstract Mono triggerRefresh(ConfigMap configMap); + + @Override + protected void onEvent(Secret secret) { + if (isSpringCloudKubernetesSecret(secret)) { + if (log.isDebugEnabled()) { + log.debug("Scheduling remote refresh event to be published for Secret " + + secret.getMetadata().getName() + " to be published in " + + k8SConfigurationProperties.getRefreshDelay().toMillis() + + " milliseconds"); + } + executorService.schedule(() -> triggerRefresh(secret).subscribe(), + k8SConfigurationProperties.getRefreshDelay().toMillis(), + TimeUnit.MILLISECONDS); + } + else { + if (log.isDebugEnabled()) { + log.debug("Not publishing event. Secret " + secret.getMetadata().getName() + + " does not contain the label " + + k8SConfigurationProperties.getSecretLabel()); + } + } + } + +} diff --git a/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configuration-watcher/src/main/java/org/springframework/cloud/kubernetes/configuration/watcher/ConfigurationWatcherConfigurationProperties.java b/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configuration-watcher/src/main/java/org/springframework/cloud/kubernetes/configuration/watcher/ConfigurationWatcherConfigurationProperties.java new file mode 100644 index 00000000..b7cb2004 --- /dev/null +++ b/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configuration-watcher/src/main/java/org/springframework/cloud/kubernetes/configuration/watcher/ConfigurationWatcherConfigurationProperties.java @@ -0,0 +1,103 @@ +/* + * 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.configuration.watcher; + +import java.time.Duration; +import java.time.temporal.ChronoUnit; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.convert.DurationUnit; + +/** + * @author Ryan Baxter + */ +@ConfigurationProperties("spring.cloud.kubernetes.configuration.watcher") +public class ConfigurationWatcherConfigurationProperties { + + /** + * Amount of time to delay the posting of the event to allow the app volume to update + * data. + */ + @DurationUnit(ChronoUnit.MILLIS) + private Duration refreshDelay = Duration.ofMillis(120000); + + private int threadPoolSize = 1; + + private String configLabel = "spring.cloud.kubernetes.config"; + + private String secretLabel = "spring.cloud.kubernetes.secret"; + + private String actuatorPath = "/actuator"; + + private Integer actuatorPort = -1; + + public String getActuatorPath() { + return actuatorPath; + } + + public void setActuatorPath(String actuatorPath) { + String normalizedPath = actuatorPath; + if (!normalizedPath.startsWith("/")) { + normalizedPath = "/" + normalizedPath; + } + if (normalizedPath.endsWith("/")) { + normalizedPath = normalizedPath.substring(0, normalizedPath.length() - 1); + } + this.actuatorPath = normalizedPath; + } + + public Integer getActuatorPort() { + return actuatorPort; + } + + public void setActuatorPort(Integer actuatorPort) { + this.actuatorPort = actuatorPort; + } + + public String getSecretLabel() { + return secretLabel; + } + + public void setSecretLabel(String secretLabel) { + this.secretLabel = secretLabel; + } + + public String getConfigLabel() { + return configLabel; + } + + public void setConfigLabel(String configLabel) { + this.configLabel = configLabel; + } + + public Duration getRefreshDelay() { + return refreshDelay; + } + + public void setRefreshDelay(Duration refreshDelay) { + this.refreshDelay = refreshDelay; + } + + public int getThreadPoolSize() { + return threadPoolSize; + } + + public void setThreadPoolSize(int threadPoolSize) { + this.threadPoolSize = threadPoolSize; + } + +} diff --git a/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configuration-watcher/src/main/java/org/springframework/cloud/kubernetes/configuration/watcher/HttpBasedConfigurationWatchChangeDetector.java b/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configuration-watcher/src/main/java/org/springframework/cloud/kubernetes/configuration/watcher/HttpBasedConfigurationWatchChangeDetector.java new file mode 100644 index 00000000..671d36f7 --- /dev/null +++ b/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configuration-watcher/src/main/java/org/springframework/cloud/kubernetes/configuration/watcher/HttpBasedConfigurationWatchChangeDetector.java @@ -0,0 +1,153 @@ +/* + * 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.configuration.watcher; + +import java.net.URI; + +import io.fabric8.kubernetes.api.model.ConfigMap; +import io.fabric8.kubernetes.api.model.ObjectMeta; +import io.fabric8.kubernetes.api.model.Secret; +import io.fabric8.kubernetes.client.KubernetesClient; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import org.springframework.cloud.client.ServiceInstance; +import org.springframework.cloud.kubernetes.config.ConfigMapPropertySourceLocator; +import org.springframework.cloud.kubernetes.config.SecretsPropertySourceLocator; +import org.springframework.cloud.kubernetes.config.reload.ConfigReloadProperties; +import org.springframework.cloud.kubernetes.config.reload.ConfigurationUpdateStrategy; +import org.springframework.cloud.kubernetes.discovery.reactive.KubernetesReactiveDiscoveryClient; +import org.springframework.core.env.AbstractEnvironment; +import org.springframework.http.ResponseEntity; +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; +import org.springframework.util.StringUtils; +import org.springframework.web.reactive.function.client.WebClient; +import org.springframework.web.util.UriComponentsBuilder; + +/** + * @author Ryan Baxter + */ +public class HttpBasedConfigurationWatchChangeDetector + extends ConfigurationWatcherChangeDetector { + + /** + * Annotation key for actuator port and path. + */ + public static String ANNOTATION_KEY = "boot.spring.io/actuator"; + + private WebClient webClient; + + private KubernetesReactiveDiscoveryClient kubernetesReactiveDiscoveryClient; + + public HttpBasedConfigurationWatchChangeDetector(AbstractEnvironment environment, + ConfigReloadProperties properties, KubernetesClient kubernetesClient, + ConfigurationUpdateStrategy strategy, + ConfigMapPropertySourceLocator configMapPropertySourceLocator, + SecretsPropertySourceLocator secretsPropertySourceLocator, + ConfigurationWatcherConfigurationProperties k8SConfigurationProperties, + ThreadPoolTaskExecutor threadPoolTaskExecutor, WebClient webClient, + KubernetesReactiveDiscoveryClient k8sReactiveDiscoveryClient) { + super(environment, properties, kubernetesClient, strategy, + configMapPropertySourceLocator, secretsPropertySourceLocator, + k8SConfigurationProperties, threadPoolTaskExecutor); + this.webClient = webClient; + this.kubernetesReactiveDiscoveryClient = k8sReactiveDiscoveryClient; + } + + @Override + protected Mono triggerRefresh(Secret secret) { + return refresh(secret.getMetadata()).then(); + } + + private void setActuatorUriFromAnnotation(UriComponentsBuilder actuatorUriBuilder, + String metadataUri) { + URI annotationUri = URI.create(metadataUri); + actuatorUriBuilder.path(annotationUri.getPath() + "/refresh"); + + // The URI may not contain a host so if that is the case the port in the URI will + // be -1 + // The authority of the URI will be : for example :9090, we just need the + // 9090 in this case + if (annotationUri.getPort() < 0) { + if (annotationUri.getAuthority() != null) { + actuatorUriBuilder + .port(annotationUri.getAuthority().replaceFirst(":", "")); + } + } + else { + actuatorUriBuilder.port(annotationUri.getPort()); + } + } + + private URI getActuatorUri(ServiceInstance si) { + String metadataUri = si.getMetadata().getOrDefault(ANNOTATION_KEY, ""); + if (log.isDebugEnabled()) { + log.debug("Metadata actuator uri is: " + metadataUri); + } + + UriComponentsBuilder actuatorUriBuilder = UriComponentsBuilder.newInstance() + .scheme(si.getScheme()).host(si.getHost()); + + if (!StringUtils.isEmpty(metadataUri)) { + if (log.isDebugEnabled()) { + log.debug("Found actuator URI in service instance metadata"); + } + setActuatorUriFromAnnotation(actuatorUriBuilder, metadataUri); + } + else { + Integer port = k8SConfigurationProperties.getActuatorPort() < 0 ? si.getPort() + : k8SConfigurationProperties.getActuatorPort(); + actuatorUriBuilder = actuatorUriBuilder + .path(k8SConfigurationProperties.getActuatorPath() + "/refresh") + .port(port); + } + + return actuatorUriBuilder.build().toUri(); + } + + protected Flux> refresh(ObjectMeta objectMeta) { + + return kubernetesReactiveDiscoveryClient.getInstances(objectMeta.getName()) + .flatMap(si -> { + URI actuatorUri = getActuatorUri(si); + if (log.isDebugEnabled()) { + log.debug("Sending refresh request for " + objectMeta.getName() + + " to URI " + actuatorUri.toString()); + } + Mono> response = webClient.post() + .uri(actuatorUri).retrieve().toBodilessEntity() + .doOnSuccess(re -> { + if (log.isDebugEnabled()) { + log.debug("Refresh sent to " + objectMeta.getName() + + " at URI address " + actuatorUri + + " returned a " + + re.getStatusCode().toString()); + } + }).doOnError(t -> { + log.warn("Refresh sent to " + objectMeta.getName() + + " failed", t); + }); + return response; + }); + } + + @Override + protected Mono triggerRefresh(ConfigMap configMap) { + return refresh(configMap.getMetadata()).then(); + } + +} diff --git a/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configuration-watcher/src/main/resources/META-INF/spring.factories b/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configuration-watcher/src/main/resources/META-INF/spring.factories new file mode 100644 index 00000000..c2377568 --- /dev/null +++ b/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configuration-watcher/src/main/resources/META-INF/spring.factories @@ -0,0 +1,2 @@ +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ +org.springframework.cloud.kubernetes.configuration.watcher.ConfigurationWatcherAutoConfiguration diff --git a/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configuration-watcher/src/main/resources/application.yml b/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configuration-watcher/src/main/resources/application.yml new file mode 100644 index 00000000..3df5355d --- /dev/null +++ b/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configuration-watcher/src/main/resources/application.yml @@ -0,0 +1,19 @@ +spring: + application: + name: spring-cloud-kubernetes-configuration-watcher + cloud: + kubernetes: + reload: + enabled: true + monitoring-secrets: true + strategy: shutdown + bus: + enabled: false +server: + port: 8888 +--- +spring: + profiles: bus + cloud: + bus: + enabled: true diff --git a/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configuration-watcher/src/test/java/org/springframework/cloud/kubernetes/configuration/watcher/BusEventBasedConfigurationWatcherChangeDetectorTests.java b/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configuration-watcher/src/test/java/org/springframework/cloud/kubernetes/configuration/watcher/BusEventBasedConfigurationWatcherChangeDetectorTests.java new file mode 100644 index 00000000..be8c3c46 --- /dev/null +++ b/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configuration-watcher/src/test/java/org/springframework/cloud/kubernetes/configuration/watcher/BusEventBasedConfigurationWatcherChangeDetectorTests.java @@ -0,0 +1,119 @@ +/* + * 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.configuration.watcher; + +import io.fabric8.kubernetes.api.model.ConfigMap; +import io.fabric8.kubernetes.api.model.ObjectMeta; +import io.fabric8.kubernetes.api.model.Secret; +import io.fabric8.kubernetes.client.KubernetesClient; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +import org.springframework.cloud.bus.BusProperties; +import org.springframework.cloud.bus.event.RefreshRemoteApplicationEvent; +import org.springframework.cloud.kubernetes.config.ConfigMapPropertySourceLocator; +import org.springframework.cloud.kubernetes.config.SecretsPropertySourceLocator; +import org.springframework.cloud.kubernetes.config.reload.ConfigReloadProperties; +import org.springframework.cloud.kubernetes.config.reload.ConfigurationUpdateStrategy; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.mock.env.MockEnvironment; +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.verify; + +/** + * @author Ryan Baxter + */ +@RunWith(MockitoJUnitRunner.class) +public class BusEventBasedConfigurationWatcherChangeDetectorTests { + + @Mock + private KubernetesClient client; + + @Mock + private ConfigurationUpdateStrategy updateStrategy; + + @Mock + private ConfigMapPropertySourceLocator configMapPropertySourceLocator; + + @Mock + private SecretsPropertySourceLocator secretsPropertySourceLocator; + + @Mock + private ThreadPoolTaskExecutor threadPoolTaskExecutor; + + @Mock + private ApplicationEventPublisher applicationEventPublisher; + + private BusEventBasedConfigurationWatcherChangeDetector changeDetector; + + private ConfigurationWatcherConfigurationProperties configurationWatcherConfigurationProperties; + + private BusProperties busProperties; + + @Before + public void setup() { + MockEnvironment mockEnvironment = new MockEnvironment(); + ConfigReloadProperties configReloadProperties = new ConfigReloadProperties(); + configurationWatcherConfigurationProperties = new ConfigurationWatcherConfigurationProperties(); + busProperties = new BusProperties(); + changeDetector = new BusEventBasedConfigurationWatcherChangeDetector( + mockEnvironment, configReloadProperties, client, updateStrategy, + configMapPropertySourceLocator, secretsPropertySourceLocator, + busProperties, configurationWatcherConfigurationProperties, + threadPoolTaskExecutor); + changeDetector.setApplicationEventPublisher(applicationEventPublisher); + } + + @Test + public void triggerRefreshWithConfigMap() { + ObjectMeta objectMeta = new ObjectMeta(); + objectMeta.setName("foo"); + ConfigMap configMap = new ConfigMap(); + configMap.setMetadata(objectMeta); + changeDetector.triggerRefresh(configMap); + ArgumentCaptor argumentCaptor = ArgumentCaptor + .forClass(RefreshRemoteApplicationEvent.class); + verify(applicationEventPublisher).publishEvent(argumentCaptor.capture()); + assertThat(argumentCaptor.getValue().getSource()).isEqualTo(configMap); + assertThat(argumentCaptor.getValue().getOriginService()) + .isEqualTo(busProperties.getId()); + assertThat(argumentCaptor.getValue().getDestinationService()).isEqualTo("foo:**"); + } + + @Test + public void triggerRefreshWithSecret() { + ObjectMeta objectMeta = new ObjectMeta(); + objectMeta.setName("foo"); + Secret secret = new Secret(); + secret.setMetadata(objectMeta); + changeDetector.triggerRefresh(secret); + ArgumentCaptor argumentCaptor = ArgumentCaptor + .forClass(RefreshRemoteApplicationEvent.class); + verify(applicationEventPublisher).publishEvent(argumentCaptor.capture()); + assertThat(argumentCaptor.getValue().getSource()).isEqualTo(secret); + assertThat(argumentCaptor.getValue().getOriginService()) + .isEqualTo(busProperties.getId()); + assertThat(argumentCaptor.getValue().getDestinationService()).isEqualTo("foo:**"); + } + +} diff --git a/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configuration-watcher/src/test/java/org/springframework/cloud/kubernetes/configuration/watcher/ConfigurationWatcherConfigurationPropertiesTests.java b/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configuration-watcher/src/test/java/org/springframework/cloud/kubernetes/configuration/watcher/ConfigurationWatcherConfigurationPropertiesTests.java new file mode 100644 index 00000000..9c08860c --- /dev/null +++ b/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configuration-watcher/src/test/java/org/springframework/cloud/kubernetes/configuration/watcher/ConfigurationWatcherConfigurationPropertiesTests.java @@ -0,0 +1,37 @@ +/* + * 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.configuration.watcher; + +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Ryan Baxter + */ +public class ConfigurationWatcherConfigurationPropertiesTests { + + @Test + public void setActuatorPath() { + ConfigurationWatcherConfigurationProperties properties = new ConfigurationWatcherConfigurationProperties(); + properties.setActuatorPath("foo"); + assertThat(properties.getActuatorPath()).isEqualTo("/foo"); + properties.setActuatorPath("/foo/bar/"); + assertThat(properties.getActuatorPath()).isEqualTo("/foo/bar"); + } + +} diff --git a/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configuration-watcher/src/test/java/org/springframework/cloud/kubernetes/configuration/watcher/HttpBasedConfigurationWatchChangeDetectorTests.java b/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configuration-watcher/src/test/java/org/springframework/cloud/kubernetes/configuration/watcher/HttpBasedConfigurationWatchChangeDetectorTests.java new file mode 100644 index 00000000..3636bbca --- /dev/null +++ b/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configuration-watcher/src/test/java/org/springframework/cloud/kubernetes/configuration/watcher/HttpBasedConfigurationWatchChangeDetectorTests.java @@ -0,0 +1,228 @@ +/* + * 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.configuration.watcher; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import com.github.tomakehurst.wiremock.client.WireMock; +import com.github.tomakehurst.wiremock.junit.WireMockRule; +import io.fabric8.kubernetes.api.model.ConfigMap; +import io.fabric8.kubernetes.api.model.EndpointAddress; +import io.fabric8.kubernetes.api.model.EndpointPort; +import io.fabric8.kubernetes.api.model.ObjectMeta; +import io.fabric8.kubernetes.api.model.Secret; +import io.fabric8.kubernetes.client.KubernetesClient; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; +import reactor.core.publisher.Flux; +import reactor.test.StepVerifier; + +import org.springframework.cloud.client.ServiceInstance; +import org.springframework.cloud.kubernetes.config.ConfigMapPropertySourceLocator; +import org.springframework.cloud.kubernetes.config.SecretsPropertySourceLocator; +import org.springframework.cloud.kubernetes.config.reload.ConfigReloadProperties; +import org.springframework.cloud.kubernetes.config.reload.ConfigurationUpdateStrategy; +import org.springframework.cloud.kubernetes.discovery.KubernetesServiceInstance; +import org.springframework.cloud.kubernetes.discovery.reactive.KubernetesReactiveDiscoveryClient; +import org.springframework.mock.env.MockEnvironment; +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; +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.post; +import static com.github.tomakehurst.wiremock.client.WireMock.postRequestedFor; +import static com.github.tomakehurst.wiremock.client.WireMock.stubFor; +import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo; +import static com.github.tomakehurst.wiremock.client.WireMock.verify; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.when; +import static org.springframework.cloud.kubernetes.configuration.watcher.HttpBasedConfigurationWatchChangeDetector.ANNOTATION_KEY; + +/** + * @author Ryan Baxter + */ +@RunWith(MockitoJUnitRunner.class) +public class HttpBasedConfigurationWatchChangeDetectorTests { + + @Rule + public WireMockRule wireMockRule = new WireMockRule(0); + + @Mock + private KubernetesClient client; + + @Mock + private ConfigurationUpdateStrategy updateStrategy; + + @Mock + private ConfigMapPropertySourceLocator configMapPropertySourceLocator; + + @Mock + private SecretsPropertySourceLocator secretsPropertySourceLocator; + + @Mock + private ThreadPoolTaskExecutor threadPoolTaskExecutor; + + @Mock + private KubernetesReactiveDiscoveryClient reactiveDiscoveryClient; + + private HttpBasedConfigurationWatchChangeDetector changeDetector; + + private ConfigurationWatcherConfigurationProperties configurationWatcherConfigurationProperties; + + @Before + public void setup() { + EndpointAddress fooEndpointAddress = new EndpointAddress(); + fooEndpointAddress.setIp("127.0.0.1"); + fooEndpointAddress.setHostname("localhost"); + EndpointPort fooEndpointPort = new EndpointPort(); + fooEndpointPort.setPort(wireMockRule.port()); + List instances = new ArrayList<>(); + KubernetesServiceInstance fooServiceInstance = new KubernetesServiceInstance( + "foo", "foo", fooEndpointAddress, fooEndpointPort, new HashMap<>(), + false); + instances.add(fooServiceInstance); + when(reactiveDiscoveryClient.getInstances(eq("foo"))) + .thenReturn(Flux.fromIterable(instances)); + MockEnvironment mockEnvironment = new MockEnvironment(); + ConfigReloadProperties configReloadProperties = new ConfigReloadProperties(); + configurationWatcherConfigurationProperties = new ConfigurationWatcherConfigurationProperties(); + WebClient webClient = WebClient.builder().build(); + changeDetector = new HttpBasedConfigurationWatchChangeDetector(mockEnvironment, + configReloadProperties, client, updateStrategy, + configMapPropertySourceLocator, secretsPropertySourceLocator, + configurationWatcherConfigurationProperties, threadPoolTaskExecutor, + webClient, reactiveDiscoveryClient); + } + + @Test + public void triggerConfigMapRefresh() { + ConfigMap configMap = new ConfigMap(); + ObjectMeta objectMeta = new ObjectMeta(); + objectMeta.setName("foo"); + configMap.setMetadata(objectMeta); + WireMock.configureFor("localhost", wireMockRule.port()); + stubFor(post(WireMock.urlEqualTo("/actuator/refresh")) + .willReturn(aResponse().withStatus(200))); + StepVerifier.create(changeDetector.triggerRefresh(configMap)).verifyComplete(); + verify(postRequestedFor(urlEqualTo("/actuator/refresh"))); + } + + @Test + public void triggerSecretRefresh() throws InterruptedException { + Secret secret = new Secret(); + ObjectMeta objectMeta = new ObjectMeta(); + objectMeta.setName("foo"); + secret.setMetadata(objectMeta); + WireMock.configureFor("localhost", wireMockRule.port()); + stubFor(post(WireMock.urlEqualTo("/actuator/refresh")) + .willReturn(aResponse().withStatus(200))); + StepVerifier.create(changeDetector.triggerRefresh(secret)).verifyComplete(); + verify(postRequestedFor(urlEqualTo("/actuator/refresh"))); + } + + @Test + public void triggerConfigMapRefreshWithPropertiesBasedActuatorPath() + throws InterruptedException { + configurationWatcherConfigurationProperties + .setActuatorPath("/my/custom/actuator"); + ConfigMap configMap = new ConfigMap(); + ObjectMeta objectMeta = new ObjectMeta(); + objectMeta.setName("foo"); + configMap.setMetadata(objectMeta); + WireMock.configureFor("localhost", wireMockRule.port()); + stubFor(post(WireMock.urlEqualTo("/my/custom/actuator/refresh")) + .willReturn(aResponse().withStatus(200))); + StepVerifier.create(changeDetector.triggerRefresh(configMap)).verifyComplete(); + verify(postRequestedFor(urlEqualTo("/my/custom/actuator/refresh"))); + } + + @Test + public void triggerSecretRefreshWithPropertiesBasedActuatorPath() + throws InterruptedException { + configurationWatcherConfigurationProperties + .setActuatorPath("/my/custom/actuator"); + Secret secret = new Secret(); + ObjectMeta objectMeta = new ObjectMeta(); + objectMeta.setName("foo"); + secret.setMetadata(objectMeta); + WireMock.configureFor("localhost", wireMockRule.port()); + stubFor(post(WireMock.urlEqualTo("/my/custom/actuator/refresh")) + .willReturn(aResponse().withStatus(200))); + StepVerifier.create(changeDetector.triggerRefresh(secret)).verifyComplete(); + verify(postRequestedFor(urlEqualTo("/my/custom/actuator/refresh"))); + } + + @Test + public void triggerConfigMapRefreshWithAnnotationActuatorPath() { + Map metadata = new HashMap<>(); + metadata.put(ANNOTATION_KEY, + "http://:" + wireMockRule.port() + "/my/custom/actuator"); + EndpointAddress fooEndpointAddress = new EndpointAddress(); + fooEndpointAddress.setIp("127.0.0.1"); + fooEndpointAddress.setHostname("localhost"); + EndpointPort fooEndpointPort = new EndpointPort(); + fooEndpointPort.setPort(wireMockRule.port()); + List instances = new ArrayList<>(); + KubernetesServiceInstance fooServiceInstance = new KubernetesServiceInstance( + "foo", "foo", fooEndpointAddress, fooEndpointPort, metadata, false); + instances.add(fooServiceInstance); + when(reactiveDiscoveryClient.getInstances(eq("foo"))) + .thenReturn(Flux.fromIterable(instances)); + ConfigMap configMap = new ConfigMap(); + ObjectMeta objectMeta = new ObjectMeta(); + objectMeta.setName("foo"); + configMap.setMetadata(objectMeta); + stubFor(post(WireMock.urlEqualTo("/my/custom/actuator/refresh")) + .willReturn(aResponse().withStatus(200))); + StepVerifier.create(changeDetector.triggerRefresh(configMap)).verifyComplete(); + verify(postRequestedFor(urlEqualTo("/my/custom/actuator/refresh"))); + } + + @Test + public void triggerSecretRefreshWithAnnotationActuatorPath() { + Map metadata = new HashMap<>(); + metadata.put(ANNOTATION_KEY, + "http://:" + wireMockRule.port() + "/my/custom/actuator"); + EndpointAddress fooEndpointAddress = new EndpointAddress(); + fooEndpointAddress.setIp("127.0.0.1"); + fooEndpointAddress.setHostname("localhost"); + EndpointPort fooEndpointPort = new EndpointPort(); + fooEndpointPort.setPort(wireMockRule.port()); + List instances = new ArrayList<>(); + KubernetesServiceInstance fooServiceInstance = new KubernetesServiceInstance( + "foo", "foo", fooEndpointAddress, fooEndpointPort, metadata, false); + instances.add(fooServiceInstance); + when(reactiveDiscoveryClient.getInstances(eq("foo"))) + .thenReturn(Flux.fromIterable(instances)); + Secret secret = new Secret(); + ObjectMeta objectMeta = new ObjectMeta(); + objectMeta.setName("foo"); + secret.setMetadata(objectMeta); + stubFor(post(WireMock.urlEqualTo("/my/custom/actuator/refresh")) + .willReturn(aResponse().withStatus(200))); + StepVerifier.create(changeDetector.triggerRefresh(secret)).verifyComplete(); + verify(postRequestedFor(urlEqualTo("/my/custom/actuator/refresh"))); + } + +} diff --git a/spring-cloud-kubernetes-dependencies/pom.xml b/spring-cloud-kubernetes-dependencies/pom.xml index e2c50435..333188b1 100644 --- a/spring-cloud-kubernetes-dependencies/pom.xml +++ b/spring-cloud-kubernetes-dependencies/pom.xml @@ -34,10 +34,10 @@ 1.4.0.Final 1.15.2 - 4.9.0 + 4.10.3 1.1.1 0.1.2 - 3.12.0 + 3.14.4 diff --git a/spring-cloud-kubernetes-discovery/src/main/java/org/springframework/cloud/kubernetes/discovery/KubernetesCatalogWatch.java b/spring-cloud-kubernetes-discovery/src/main/java/org/springframework/cloud/kubernetes/discovery/KubernetesCatalogWatch.java index a0bee9f6..416c5371 100644 --- a/spring-cloud-kubernetes-discovery/src/main/java/org/springframework/cloud/kubernetes/discovery/KubernetesCatalogWatch.java +++ b/spring-cloud-kubernetes-discovery/src/main/java/org/springframework/cloud/kubernetes/discovery/KubernetesCatalogWatch.java @@ -71,8 +71,10 @@ public class KubernetesCatalogWatch implements ApplicationEventPublisherAware { // not all pods participate in the service discovery. only those that have // endpoints. List endpoints = this.properties.isAllNamespaces() - ? this.kubernetesClient.endpoints().inAnyNamespace().list().getItems() - : this.kubernetesClient.endpoints().list().getItems(); + ? this.kubernetesClient.endpoints().inAnyNamespace() + .withLabels(properties.getServiceLabels()).list().getItems() + : this.kubernetesClient.endpoints() + .withLabels(properties.getServiceLabels()).list().getItems(); List endpointsPodNames = endpoints.stream().map(Endpoints::getSubsets) .filter(Objects::nonNull).flatMap(Collection::stream) .map(EndpointSubset::getAddresses).filter(Objects::nonNull) 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 index 7d0f6792..c1747f55 100644 --- 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 @@ -17,7 +17,6 @@ package org.springframework.cloud.kubernetes.discovery; import java.util.ArrayList; -import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -102,13 +101,7 @@ public class KubernetesDiscoveryClient implements DiscoveryClient { Assert.notNull(serviceId, "[Assertion failed] - the object argument must not be null"); - List endpointsList = this.properties.isAllNamespaces() - ? this.client.endpoints().inAnyNamespace() - .withField("metadata.name", serviceId).list().getItems() - : Collections - .singletonList(this.client.endpoints().withName(serviceId).get()); - - List subsetsNS = endpointsList.stream() + List subsetsNS = this.getEndPointsList(serviceId).stream() .map(endpoints -> getSubsetsFromEndpoints(endpoints)) .collect(Collectors.toList()); @@ -122,6 +115,15 @@ public class KubernetesDiscoveryClient implements DiscoveryClient { return instances; } + public List getEndPointsList(String serviceId) { + return this.properties.isAllNamespaces() + ? this.client.endpoints().inAnyNamespace() + .withField("metadata.name", serviceId) + .withLabels(properties.getServiceLabels()).list().getItems() + : this.client.endpoints().withField("metadata.name", serviceId) + .withLabels(properties.getServiceLabels()).list().getItems(); + } + private List getNamespaceServiceInstances(EndpointSubsetNS es, String serviceId) { String namespace = es.getNamespace(); diff --git a/spring-cloud-kubernetes-discovery/src/test/java/org/springframework/cloud/kubernetes/discovery/KubernetesCatalogWatchTest.java b/spring-cloud-kubernetes-discovery/src/test/java/org/springframework/cloud/kubernetes/discovery/KubernetesCatalogWatchTest.java index 419cb595..19075075 100644 --- a/spring-cloud-kubernetes-discovery/src/test/java/org/springframework/cloud/kubernetes/discovery/KubernetesCatalogWatchTest.java +++ b/spring-cloud-kubernetes-discovery/src/test/java/org/springframework/cloud/kubernetes/discovery/KubernetesCatalogWatchTest.java @@ -44,6 +44,7 @@ import org.springframework.context.ApplicationEventPublisher; import static java.util.Arrays.stream; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.anyMap; import static org.mockito.Mockito.any; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -54,6 +55,9 @@ import static org.mockito.Mockito.when; @RunWith(MockitoJUnitRunner.class) public class KubernetesCatalogWatchTest { + @Mock + private KubernetesDiscoveryProperties properties; + @Mock private KubernetesClient kubernetesClient; @@ -85,6 +89,28 @@ public class KubernetesCatalogWatchTest { .thenReturn(createSingleEndpointEndpointListByPodName("other-pod", "api-pod")); when(this.kubernetesClient.endpoints()).thenReturn(this.endpointsOperation); + when(this.kubernetesClient.endpoints().withLabels(anyMap())) + .thenReturn(this.endpointsOperation); + + this.underTest.catalogServicesWatch(); + // second execution on shuffleServices + this.underTest.catalogServicesWatch(); + + verify(this.applicationEventPublisher).publishEvent(any(HeartbeatEvent.class)); + } + + @Test + public void testRandomOrderChangePodsAllNamespaces() throws Exception { + when(this.endpointsOperation.list()) + .thenReturn( + createSingleEndpointEndpointListByPodName("api-pod", "other-pod")) + .thenReturn(createSingleEndpointEndpointListByPodName("other-pod", + "api-pod")); + when(this.kubernetesClient.endpoints()).thenReturn(this.endpointsOperation); + when(this.kubernetesClient.endpoints().inAnyNamespace()) + .thenReturn(this.endpointsOperation); + when(this.kubernetesClient.endpoints().inAnyNamespace().withLabels(anyMap())) + .thenReturn(this.endpointsOperation); this.underTest.catalogServicesWatch(); // second execution on shuffleServices @@ -101,6 +127,28 @@ public class KubernetesCatalogWatchTest { .thenReturn( createEndpointsListByServiceName("other-service", "api-service")); when(this.kubernetesClient.endpoints()).thenReturn(this.endpointsOperation); + when(this.kubernetesClient.endpoints().withLabels(anyMap())) + .thenReturn(this.endpointsOperation); + + this.underTest.catalogServicesWatch(); + // second execution on shuffleServices + this.underTest.catalogServicesWatch(); + + verify(this.applicationEventPublisher).publishEvent(any(HeartbeatEvent.class)); + } + + @Test + public void testRandomOrderChangeServicesAllNamespaces() throws Exception { + when(this.endpointsOperation.list()) + .thenReturn( + createEndpointsListByServiceName("api-service", "other-service")) + .thenReturn( + createEndpointsListByServiceName("other-service", "api-service")); + when(this.kubernetesClient.endpoints()).thenReturn(this.endpointsOperation); + when(this.kubernetesClient.endpoints().inAnyNamespace()) + .thenReturn(this.endpointsOperation); + when(this.kubernetesClient.endpoints().inAnyNamespace().withLabels(anyMap())) + .thenReturn(this.endpointsOperation); this.underTest.catalogServicesWatch(); // second execution on shuffleServices @@ -114,6 +162,30 @@ public class KubernetesCatalogWatchTest { when(this.endpointsOperation.list()).thenReturn( createSingleEndpointEndpointListByPodName("api-pod", "other-pod")); when(this.kubernetesClient.endpoints()).thenReturn(this.endpointsOperation); + when(this.kubernetesClient.endpoints().withLabels(anyMap())) + .thenReturn(this.endpointsOperation); + + this.underTest.catalogServicesWatch(); + + verify(this.applicationEventPublisher) + .publishEvent(this.heartbeatEventArgumentCaptor.capture()); + + HeartbeatEvent event = this.heartbeatEventArgumentCaptor.getValue(); + assertThat(event.getValue()).isInstanceOf(List.class); + + List expectedPodsList = Arrays.asList("api-pod", "other-pod"); + assertThat(event.getValue()).isEqualTo(expectedPodsList); + } + + @Test + public void testEventBodyAllNamespaces() throws Exception { + when(this.endpointsOperation.list()).thenReturn( + createSingleEndpointEndpointListByPodName("api-pod", "other-pod")); + when(this.kubernetesClient.endpoints()).thenReturn(this.endpointsOperation); + when(this.kubernetesClient.endpoints().inAnyNamespace()) + .thenReturn(this.endpointsOperation); + when(this.kubernetesClient.endpoints().inAnyNamespace().withLabels(anyMap())) + .thenReturn(this.endpointsOperation); this.underTest.catalogServicesWatch(); @@ -134,6 +206,27 @@ public class KubernetesCatalogWatchTest { when(this.endpointsOperation.list()).thenReturn(endpoints); when(this.kubernetesClient.endpoints()).thenReturn(this.endpointsOperation); + when(this.kubernetesClient.endpoints().withLabels(anyMap())) + .thenReturn(this.endpointsOperation); + + this.underTest.catalogServicesWatch(); + // second execution on shuffleServices + this.underTest.catalogServicesWatch(); + + verify(this.applicationEventPublisher).publishEvent(any(HeartbeatEvent.class)); + } + + @Test + public void testEndpointsWithoutSubsetsAllNamespaces() { + + EndpointsList endpoints = createSingleEndpointEndpointListWithoutSubsets(); + + when(this.endpointsOperation.list()).thenReturn(endpoints); + when(this.kubernetesClient.endpoints()).thenReturn(this.endpointsOperation); + when(this.kubernetesClient.endpoints().inAnyNamespace()) + .thenReturn(this.endpointsOperation); + when(this.kubernetesClient.endpoints().inAnyNamespace().withLabels(anyMap())) + .thenReturn(this.endpointsOperation); this.underTest.catalogServicesWatch(); // second execution on shuffleServices @@ -150,6 +243,28 @@ public class KubernetesCatalogWatchTest { when(this.endpointsOperation.list()).thenReturn(endpoints); when(this.kubernetesClient.endpoints()).thenReturn(this.endpointsOperation); + when(this.kubernetesClient.endpoints().withLabels(anyMap())) + .thenReturn(this.endpointsOperation); + + this.underTest.catalogServicesWatch(); + // second execution on shuffleServices + this.underTest.catalogServicesWatch(); + + verify(this.applicationEventPublisher).publishEvent(any(HeartbeatEvent.class)); + } + + @Test + public void testEndpointsWithoutAddressesAllNamespaces() { + + EndpointsList endpoints = createSingleEndpointEndpointListByPodName("api-pod"); + endpoints.getItems().get(0).getSubsets().get(0).setAddresses(null); + + when(this.endpointsOperation.list()).thenReturn(endpoints); + when(this.kubernetesClient.endpoints()).thenReturn(this.endpointsOperation); + when(this.kubernetesClient.endpoints().inAnyNamespace()) + .thenReturn(this.endpointsOperation); + when(this.kubernetesClient.endpoints().inAnyNamespace().withLabels(anyMap())) + .thenReturn(this.endpointsOperation); this.underTest.catalogServicesWatch(); // second execution on shuffleServices @@ -167,6 +282,29 @@ public class KubernetesCatalogWatchTest { when(this.endpointsOperation.list()).thenReturn(endpoints); when(this.kubernetesClient.endpoints()).thenReturn(this.endpointsOperation); + when(this.kubernetesClient.endpoints().withLabels(anyMap())) + .thenReturn(this.endpointsOperation); + + this.underTest.catalogServicesWatch(); + // second execution on shuffleServices + this.underTest.catalogServicesWatch(); + + verify(this.applicationEventPublisher).publishEvent(any(HeartbeatEvent.class)); + } + + @Test + public void testEndpointsWithoutTargetRefsAllNamespaces() { + + EndpointsList endpoints = createSingleEndpointEndpointListByPodName("api-pod"); + endpoints.getItems().get(0).getSubsets().get(0).getAddresses().get(0) + .setTargetRef(null); + + when(this.endpointsOperation.list()).thenReturn(endpoints); + when(this.kubernetesClient.endpoints()).thenReturn(this.endpointsOperation); + when(this.kubernetesClient.endpoints().inAnyNamespace()) + .thenReturn(this.endpointsOperation); + when(this.kubernetesClient.endpoints().inAnyNamespace().withLabels(anyMap())) + .thenReturn(this.endpointsOperation); this.underTest.catalogServicesWatch(); // second execution on shuffleServices diff --git a/spring-cloud-kubernetes-discovery/src/test/java/org/springframework/cloud/kubernetes/discovery/KubernetesDiscoveryClientFilterMetadataTest.java b/spring-cloud-kubernetes-discovery/src/test/java/org/springframework/cloud/kubernetes/discovery/KubernetesDiscoveryClientFilterMetadataTest.java index ae22dda2..c001699b 100644 --- a/spring-cloud-kubernetes-discovery/src/test/java/org/springframework/cloud/kubernetes/discovery/KubernetesDiscoveryClientFilterMetadataTest.java +++ b/spring-cloud-kubernetes-discovery/src/test/java/org/springframework/cloud/kubernetes/discovery/KubernetesDiscoveryClientFilterMetadataTest.java @@ -16,6 +16,7 @@ package org.springframework.cloud.kubernetes.discovery; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -34,6 +35,9 @@ import io.fabric8.kubernetes.api.model.ServiceList; import io.fabric8.kubernetes.api.model.ServicePort; import io.fabric8.kubernetes.api.model.ServicePortBuilder; import io.fabric8.kubernetes.client.KubernetesClient; +import io.fabric8.kubernetes.client.Watch; +import io.fabric8.kubernetes.client.Watcher; +import io.fabric8.kubernetes.client.dsl.FilterWatchListDeletable; import io.fabric8.kubernetes.client.dsl.MixedOperation; import io.fabric8.kubernetes.client.dsl.Resource; import io.fabric8.kubernetes.client.dsl.ServiceResource; @@ -49,7 +53,9 @@ import org.springframework.cloud.client.ServiceInstance; import static java.util.stream.Collectors.toList; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.entry; +import static org.mockito.ArgumentMatchers.anyMap; import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.when; @RunWith(MockitoJUnitRunner.class) @@ -79,6 +85,9 @@ public class KubernetesDiscoveryClientFilterMetadataTest { @Mock private Resource endpointsResource; + @Mock + FilterWatchListDeletable> filter; + @InjectMocks private KubernetesDiscoveryClient underTest; @@ -360,10 +369,16 @@ public class KubernetesDiscoveryClientFilterMetadataTest { .addNewSubset().addAllToPorts(getEndpointPorts(ports)).addNewAddress() .endAddress().endSubset().build(); - when(this.endpointsResource.get()).thenReturn(endpoints); - when(this.endpointsOperation.withName(serviceId)) - .thenReturn(this.endpointsResource); when(this.kubernetesClient.endpoints()).thenReturn(this.endpointsOperation); + + EndpointsList endpointsList = new EndpointsList(null, + Collections.singletonList(endpoints), null, null); + when(filter.list()).thenReturn(endpointsList); + when(filter.withLabels(anyMap())).thenReturn(filter); + + when(this.kubernetesClient.endpoints().withField(eq("metadata.name"), + eq(serviceId))).thenReturn(filter); + } private List getServicePorts(Map ports) { diff --git a/spring-cloud-kubernetes-discovery/src/test/java/org/springframework/cloud/kubernetes/discovery/KubernetesDiscoveryClientTest.java b/spring-cloud-kubernetes-discovery/src/test/java/org/springframework/cloud/kubernetes/discovery/KubernetesDiscoveryClientTest.java index 8eac4a77..85a77231 100644 --- a/spring-cloud-kubernetes-discovery/src/test/java/org/springframework/cloud/kubernetes/discovery/KubernetesDiscoveryClientTest.java +++ b/spring-cloud-kubernetes-discovery/src/test/java/org/springframework/cloud/kubernetes/discovery/KubernetesDiscoveryClientTest.java @@ -19,6 +19,7 @@ package org.springframework.cloud.kubernetes.discovery; import java.util.ArrayList; import java.util.HashMap; import java.util.List; +import java.util.Map; import io.fabric8.kubernetes.api.model.Endpoints; import io.fabric8.kubernetes.api.model.EndpointsBuilder; @@ -61,106 +62,44 @@ public class KubernetesDiscoveryClientTest { } @Test - public void getInstancesShouldBeAbleToHandleEndpointsFromMultipleNamespaces() { - Endpoints endPoints1 = new EndpointsBuilder().withNewMetadata() - .withName("endpoint").withNamespace("test").endMetadata().addNewSubset() - .addNewAddress().withIp("ip1").withNewTargetRef().withUid("uid1") - .endTargetRef().endAddress().addNewPort("http", 80, "TCP").endSubset() - .build(); + public void getInstancesShouldBeAbleToHandleEndpointsSingleAddress() { + Map labels = new HashMap(); + labels.put("l", "v"); - Endpoints endpoints2 = new EndpointsBuilder().withNewMetadata() - .withName("endpoint").withNamespace("test2").endMetadata().addNewSubset() - .addNewAddress().withIp("ip2").withNewTargetRef().withUid("uid2") - .endTargetRef().endAddress().addNewPort("http", 80, "TCP").endSubset() - .build(); + Endpoints endPoint = new EndpointsBuilder().withNewMetadata().withName("endpoint") + .withNamespace("test").withLabels(labels).endMetadata().addNewSubset() + .addNewAddress().withIp("ip1").withNewTargetRef().withUid("10") + .endTargetRef().endAddress().addNewPort("http", "http_tcp", 80, "TCP") + .endSubset().build(); List endpointsList = new ArrayList<>(); - endpointsList.add(endPoints1); - endpointsList.add(endpoints2); + endpointsList.add(endPoint); EndpointsList endpoints = new EndpointsList(); endpoints.setItems(endpointsList); + mockServer.expect().get().withPath( + "/api/v1/namespaces/test/endpoints?labelSelector=l%3Dv&fieldSelector=metadata.name%3Dendpoint") + .andReturn(200, endpoints).once(); + mockServer.expect().get() .withPath("/api/v1/endpoints?fieldSelector=metadata.name%3Dendpoint") .andReturn(200, endpoints).once(); - mockServer.expect().get().withPath("/api/v1/namespaces/test/endpoints/endpoint") - .andReturn(200, endPoints1).once(); + mockServer.expect().get().withPath( + "/api/v1/namespaces/test/endpoints?fieldSelector=metadata.name%3Dendpoint") + .andReturn(200, endpoints).once(); - mockServer.expect().get().withPath("/api/v1/namespaces/test2/endpoints/endpoint") - .andReturn(200, endpoints2).once(); - - Service service1 = new ServiceBuilder().withNewMetadata().withName("endpoint") - .withNamespace("test").withLabels(new HashMap() { - { - put("l", "v"); - } - }).endMetadata().build(); - - Service service2 = new ServiceBuilder().withNewMetadata().withName("endpoint") - .withNamespace("test2").withLabels(new HashMap() { - { - put("l", "v"); - } - }).endMetadata().build(); - - List servicesList = new ArrayList<>(); - servicesList.add(service1); - servicesList.add(service2); - - ServiceList services = new ServiceList(); - services.setItems(servicesList); - - mockServer.expect().get() - .withPath("/api/v1/services?fieldSelector=metadata.name%3Dendpoint") - .andReturn(200, services).once(); + Service service = new ServiceBuilder().withNewMetadata().withName("endpoint") + .withNamespace("test").withLabels(labels).endMetadata().build(); mockServer.expect().get().withPath("/api/v1/namespaces/test/services/endpoint") - .andReturn(200, service1).always(); - - mockServer.expect().get().withPath("/api/v1/namespaces/test2/services/endpoint") - .andReturn(200, service2).always(); - - final KubernetesDiscoveryProperties properties = new KubernetesDiscoveryProperties(); - properties.setAllNamespaces(true); - - final DiscoveryClient discoveryClient = new KubernetesDiscoveryClient(mockClient, - properties, KubernetesClient::services, - new DefaultIsServicePortSecureResolver(properties)); - - final List instances = discoveryClient.getInstances("endpoint"); - - assertThat(instances).hasSize(2); - assertThat(instances).filteredOn(s -> s.getHost().equals("ip1") && !s.isSecure()) - .hasSize(1); - assertThat(instances).filteredOn(s -> s.getHost().equals("ip2") && !s.isSecure()) - .hasSize(1); - assertThat(instances).filteredOn(s -> s.getInstanceId().equals("uid1")) - .hasSize(1); - assertThat(instances).filteredOn(s -> s.getInstanceId().equals("uid2")) - .hasSize(1); - } - - @Test - public void getInstancesShouldBeAbleToHandleEndpointsSingleAddress() { - mockServer.expect().get().withPath("/api/v1/namespaces/test/endpoints/endpoint") - .andReturn(200, new EndpointsBuilder().withNewMetadata() - .withName("endpoint").endMetadata().addNewSubset().addNewAddress() - .withIp("ip1").withNewTargetRef().withUid("uid1").endTargetRef() - .endAddress().addNewPort("http", 80, "TCP").endSubset().build()) - .once(); - - mockServer.expect().get().withPath("/api/v1/services/endpoint") - .andReturn(200, new ServiceBuilder().withNewMetadata() - .withName("endpoint").withLabels(new HashMap() { - { - put("l", "v"); - } - }).endMetadata().build()) - .always(); + .andReturn(200, service).always(); final KubernetesDiscoveryProperties properties = new KubernetesDiscoveryProperties(); + properties.setServiceLabels(labels); + properties.getMetadata().setAddLabels(false); + properties.getMetadata().setAddAnnotations(false); final DiscoveryClient discoveryClient = new KubernetesDiscoveryClient(mockClient, properties, KubernetesClient::services, @@ -170,30 +109,45 @@ public class KubernetesDiscoveryClientTest { assertThat(instances).hasSize(1) .filteredOn(s -> s.getHost().equals("ip1") && !s.isSecure()).hasSize(1) - .filteredOn(s -> s.getInstanceId().equals("uid1")).hasSize(1); + .filteredOn(s -> s.getInstanceId().equals("10")).hasSize(1); } @Test public void getInstancesShouldBeAbleToHandleEndpointsSingleAddressAndMultiplePorts() { - mockServer.expect().get().withPath("/api/v1/namespaces/test/endpoints/endpoint") - .andReturn(200, new EndpointsBuilder().withNewMetadata() - .withName("endpoint").endMetadata().addNewSubset().addNewAddress() - .withIp("ip1").withNewTargetRef().withUid("uid").endTargetRef() - .endAddress().addNewPort("mgmt", 9000, "TCP") - .addNewPort("http", 80, "TCP").endSubset().build()) - .once(); + Map labels = new HashMap(); + labels.put("l2", "v2"); + + Endpoints endPoint1 = new EndpointsBuilder().withNewMetadata() + .withName("endpoint").withNamespace("test").withLabels(labels) + .endMetadata().addNewSubset().addNewAddress().withIp("ip1") + .withNewTargetRef().withUid("20").endTargetRef().endAddress() + .addNewPort("mgmt", "mgmt_tcp", 900, "TCP") + .addNewPort("http", "http_tcp", 80, "TCP").endSubset().build(); + + List endpointsList = new ArrayList<>(); + endpointsList.add(endPoint1); + + EndpointsList endpoints = new EndpointsList(); + endpoints.setItems(endpointsList); + + mockServer.expect().get().withPath( + "/api/v1/namespaces/test/endpoints?labelSelector=l2%3Dv2&fieldSelector=metadata.name%3Dendpoint") + .andReturn(200, endpoints).once(); + + mockServer.expect().get().withPath( + "/api/v1/namespaces/test/endpoints?fieldSelector=metadata.name%3Dendpoint") + .andReturn(200, endpoints).once(); + + Service service = new ServiceBuilder().withNewMetadata().withName("endpoint") + .withNamespace("test").withLabels(labels).withAnnotations(labels) + .endMetadata().build(); mockServer.expect().get().withPath("/api/v1/namespaces/test/services/endpoint") - .andReturn(200, new ServiceBuilder().withNewMetadata() - .withName("endpoint").withLabels(new HashMap() { - { - put("l", "v"); - } - }).endMetadata().build()) - .always(); + .andReturn(200, service).always(); final KubernetesDiscoveryProperties properties = new KubernetesDiscoveryProperties(); - properties.setPrimaryPortName("http"); + properties.setPrimaryPortName("http_tcp"); + final DiscoveryClient discoveryClient = new KubernetesDiscoveryClient(mockClient, properties, KubernetesClient::services, new DefaultIsServicePortSecureResolver(properties)); @@ -202,29 +156,81 @@ public class KubernetesDiscoveryClientTest { assertThat(instances).hasSize(1) .filteredOn(s -> s.getHost().equals("ip1") && !s.isSecure()).hasSize(1) - .filteredOn(s -> s.getInstanceId().equals("uid")).hasSize(1) + .filteredOn(s -> s.getInstanceId().equals("20")).hasSize(1) .filteredOn(s -> 80 == s.getPort()).hasSize(1); } @Test - public void getInstancesShouldBeAbleToHandleEndpointsMultipleAddresses() { - mockServer.expect().get().withPath("/api/v1/namespaces/test/endpoints/endpoint") - .andReturn(200, new EndpointsBuilder().withNewMetadata() - .withName("endpoint").endMetadata().addNewSubset().addNewAddress() - .withIp("ip1").endAddress().addNewAddress().withIp("ip2") - .endAddress().addNewPort("https", 443, "TCP").endSubset().build()) - .once(); + public void getEndPointsListTest() { + Map labels = new HashMap(); + labels.put("l", "v"); - mockServer.expect().get().withPath("/api/v1/namespaces/test/services/endpoint") - .andReturn(200, new ServiceBuilder().withNewMetadata() - .withName("endpoint").withLabels(new HashMap() { - { - put("l", "v"); - } - }).endMetadata().build()) - .always(); + Endpoints endPoint = new EndpointsBuilder().withNewMetadata().withName("endpoint") + .withNamespace("test").withLabels(labels).endMetadata().addNewSubset() + .addNewAddress().withIp("ip1").withNewTargetRef().withUid("30") + .endTargetRef().endAddress().addNewPort("http", "http_tcp", 80, "TCP") + .endSubset().build(); + + List endpointsList = new ArrayList<>(); + endpointsList.add(endPoint); + + EndpointsList endpoints = new EndpointsList(); + endpoints.setItems(endpointsList); + + mockServer.expect().get().withPath( + "/api/v1/namespaces/test/endpoints?labelSelector=l%3Dv&fieldSelector=metadata.name%3Dendpoint") + .andReturn(200, endpoints).once(); final KubernetesDiscoveryProperties properties = new KubernetesDiscoveryProperties(); + properties.setServiceLabels(labels); + + final KubernetesDiscoveryClient discoveryClient = new KubernetesDiscoveryClient( + mockClient, properties, KubernetesClient::services, + new DefaultIsServicePortSecureResolver(properties)); + + final List result_endpoints = discoveryClient + .getEndPointsList("endpoint"); + + assertThat(result_endpoints).hasSize(1); + } + + @Test + public void getInstancesShouldBeAbleToHandleEndpointsMultipleAddresses() { + Map labels = new HashMap(); + labels.put("l1", "v1"); + + Endpoints endPoint = new EndpointsBuilder().withNewMetadata().withName("endpoint") + .withNamespace("test").withLabels(labels).endMetadata().addNewSubset() + .addNewAddress().withIp("ip1").withNewTargetRef().withUid("40") + .endTargetRef().endAddress().addNewAddress().withIp("ip2") + .withNewTargetRef().withUid("50").endTargetRef().endAddress() + .addNewPort("https", "https_tcp", 443, "TCP").endSubset().build(); + + List endpointsList = new ArrayList<>(); + endpointsList.add(endPoint); + + EndpointsList endpoints = new EndpointsList(); + endpoints.setItems(endpointsList); + + mockServer.expect().get().withPath( + "/api/v1/namespaces/test/endpoints?labelSelector=l1%3Dv1&fieldSelector=metadata.name%3Dendpoint") + .andReturn(200, endpoints).once(); + + mockServer.expect().get().withPath( + "/api/v1/namespaces/test/endpoints?fieldSelector=metadata.name%3Dendpoint") + .andReturn(200, endpoints).once(); + + Service service = new ServiceBuilder().withNewMetadata().withName("endpoint") + .withNamespace("test").withLabels(labels).endMetadata().build(); + + mockServer.expect().get().withPath("/api/v1/namespaces/test/services/endpoint") + .andReturn(200, service).always(); + + final KubernetesDiscoveryProperties properties = new KubernetesDiscoveryProperties(); + properties.setServiceLabels(labels); + properties.getMetadata().setAddAnnotations(false); + properties.getMetadata().setAddLabels(false); + final DiscoveryClient discoveryClient = new KubernetesDiscoveryClient(mockClient, properties, KubernetesClient::services, new DefaultIsServicePortSecureResolver(properties)); @@ -295,4 +301,84 @@ public class KubernetesDiscoveryClientTest { assertThat(services).containsOnly("s1", "s2"); } + @Test + public void getInstancesShouldBeAbleToHandleEndpointsFromMultipleNamespaces() { + Endpoints endPoints1 = new EndpointsBuilder().withNewMetadata() + .withName("endpoint").withNamespace("test").endMetadata().addNewSubset() + .addNewAddress().withIp("ip1").withNewTargetRef().withUid("60") + .endTargetRef().endAddress().addNewPort("http", "http_tcp", 80, "TCP") + .endSubset().build(); + + Endpoints endpoints2 = new EndpointsBuilder().withNewMetadata() + .withName("endpoint").withNamespace("test2").endMetadata().addNewSubset() + .addNewAddress().withIp("ip2").withNewTargetRef().withUid("70") + .endTargetRef().endAddress().addNewPort("http", "http_tcp", 80, "TCP") + .endSubset().build(); + + List endpointsList = new ArrayList<>(); + endpointsList.add(endPoints1); + endpointsList.add(endpoints2); + + EndpointsList endpoints = new EndpointsList(); + endpoints.setItems(endpointsList); + + mockServer.expect().get() + .withPath("/api/v1/endpoints?fieldSelector=metadata.name%3Dendpoint") + .andReturn(200, endpoints).once(); + + mockServer.expect().get().withPath("/api/v1/namespaces/test/endpoints/endpoint") + .andReturn(200, endPoints1).once(); + + mockServer.expect().get().withPath("/api/v1/namespaces/test2/endpoints/endpoint") + .andReturn(200, endpoints2).once(); + + Service service1 = new ServiceBuilder().withNewMetadata().withName("endpoint") + .withNamespace("test").withLabels(new HashMap() { + { + put("l", "v"); + } + }).endMetadata().build(); + + Service service2 = new ServiceBuilder().withNewMetadata().withName("endpoint") + .withNamespace("test2").withLabels(new HashMap() { + { + put("l", "v"); + } + }).endMetadata().build(); + + List servicesList = new ArrayList<>(); + servicesList.add(service1); + servicesList.add(service2); + + ServiceList services = new ServiceList(); + services.setItems(servicesList); + + mockServer.expect().get() + .withPath("/api/v1/services?fieldSelector=metadata.name%3Dendpoint") + .andReturn(200, services).once(); + + mockServer.expect().get().withPath("/api/v1/namespaces/test/services/endpoint") + .andReturn(200, service1).always(); + + mockServer.expect().get().withPath("/api/v1/namespaces/test2/services/endpoint") + .andReturn(200, service2).always(); + + final KubernetesDiscoveryProperties properties = new KubernetesDiscoveryProperties(); + properties.setAllNamespaces(true); + + final DiscoveryClient discoveryClient = new KubernetesDiscoveryClient(mockClient, + properties, KubernetesClient::services, + new DefaultIsServicePortSecureResolver(properties)); + + final List instances = discoveryClient.getInstances("endpoint"); + + assertThat(instances).hasSize(2); + assertThat(instances).filteredOn(s -> s.getHost().equals("ip1") && !s.isSecure()) + .hasSize(1); + assertThat(instances).filteredOn(s -> s.getHost().equals("ip2") && !s.isSecure()) + .hasSize(1); + assertThat(instances).filteredOn(s -> s.getInstanceId().equals("60")).hasSize(1); + assertThat(instances).filteredOn(s -> s.getInstanceId().equals("70")).hasSize(1); + } + } diff --git a/spring-cloud-kubernetes-discovery/src/test/java/org/springframework/cloud/kubernetes/discovery/reactive/KubernetesReactiveDiscoveryClientTests.java b/spring-cloud-kubernetes-discovery/src/test/java/org/springframework/cloud/kubernetes/discovery/reactive/KubernetesReactiveDiscoveryClientTests.java index fa35fa23..6c89ba81 100644 --- a/spring-cloud-kubernetes-discovery/src/test/java/org/springframework/cloud/kubernetes/discovery/reactive/KubernetesReactiveDiscoveryClientTests.java +++ b/spring-cloud-kubernetes-discovery/src/test/java/org/springframework/cloud/kubernetes/discovery/reactive/KubernetesReactiveDiscoveryClientTests.java @@ -27,6 +27,7 @@ import io.fabric8.kubernetes.client.Config; import io.fabric8.kubernetes.client.KubernetesClient; import io.fabric8.kubernetes.client.server.mock.KubernetesServer; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import reactor.core.publisher.Flux; @@ -112,6 +113,7 @@ class KubernetesReactiveDiscoveryClientTests { } @Test + @Disabled // see gh-603 public void shouldReturnEmptyFluxForNonExistingService( @Client KubernetesClient kubernetesClient) { KubernetesDiscoveryProperties properties = new KubernetesDiscoveryProperties(); @@ -122,6 +124,7 @@ class KubernetesReactiveDiscoveryClientTests { } @Test + @Disabled // see gh-603 public void shouldReturnEmptyFluxWhenServiceHasNoSubsets( @Client KubernetesClient kubernetesClient, @Server KubernetesServer kubernetesServer) { @@ -144,6 +147,7 @@ class KubernetesReactiveDiscoveryClientTests { } @Test + @Disabled // see gh-603 public void shouldReturnFlux(@Client KubernetesClient kubernetesClient, @Server KubernetesServer kubernetesServer) { kubernetesServer.expect().get().withPath("/api/v1/namespaces/test/services") @@ -160,8 +164,8 @@ class KubernetesReactiveDiscoveryClientTests { Endpoints endPoints = new EndpointsBuilder().withNewMetadata() .withName("endpoint").withNamespace("test").endMetadata().addNewSubset() .addNewAddress().withIp("ip1").withNewTargetRef().withUid("uid1") - .endTargetRef().endAddress().addNewPort("http", 80, "TCP").endSubset() - .build(); + .endTargetRef().endAddress().addNewPort("http", "http_tcp", 80, "TCP") + .endSubset().build(); kubernetesServer.expect().get() .withPath("/api/v1/namespaces/test/endpoints/existing-service") @@ -187,6 +191,7 @@ class KubernetesReactiveDiscoveryClientTests { } @Test + @Disabled // see gh-603 public void shouldReturnFluxWithPrefixedMetadata( @Client KubernetesClient kubernetesClient, @Server KubernetesServer kubernetesServer) { @@ -204,8 +209,8 @@ class KubernetesReactiveDiscoveryClientTests { Endpoints endPoints = new EndpointsBuilder().withNewMetadata() .withName("endpoint").withNamespace("test").endMetadata().addNewSubset() .addNewAddress().withIp("ip1").withNewTargetRef().withUid("uid1") - .endTargetRef().endAddress().addNewPort("http", 80, "TCP").endSubset() - .build(); + .endTargetRef().endAddress().addNewPort("http", "http_tcp", 80, "TCP") + .endSubset().build(); kubernetesServer.expect().get() .withPath("/api/v1/namespaces/test/endpoints/existing-service") @@ -234,6 +239,7 @@ class KubernetesReactiveDiscoveryClientTests { } @Test + @Disabled // see gh-603 public void shouldReturnFluxWhenServiceHasMultiplePortsAndPrimaryPortNameIsSet( @Client KubernetesClient kubernetesClient, @Server KubernetesServer kubernetesServer) { @@ -251,8 +257,8 @@ class KubernetesReactiveDiscoveryClientTests { Endpoints endPoints = new EndpointsBuilder().withNewMetadata() .withName("endpoint").withNamespace("test").endMetadata().addNewSubset() .addNewAddress().withIp("ip1").withNewTargetRef().withUid("uid1") - .endTargetRef().endAddress().addNewPort("http", 80, "TCP") - .addNewPort("https", 443, "TCP").endSubset().build(); + .endTargetRef().endAddress().addNewPort("http", "http_tcp", 80, "TCP") + .addNewPort("https", "https_tcp", 443, "TCP").endSubset().build(); kubernetesServer.expect().get() .withPath("/api/v1/namespaces/test/endpoints/existing-service") @@ -296,8 +302,8 @@ class KubernetesReactiveDiscoveryClientTests { Endpoints endpoints = new EndpointsBuilder().withNewMetadata() .withName("endpoint").withNamespace("test").endMetadata().addNewSubset() .addNewAddress().withIp("ip1").withNewTargetRef().withUid("uid1") - .endTargetRef().endAddress().addNewPort("http", 80, "TCP") - .addNewPort("https", 443, "TCP").endSubset().build(); + .endTargetRef().endAddress().addNewPort("http", "http_tcp", 80, "TCP") + .addNewPort("https", "https_tcp", 443, "TCP").endSubset().build(); EndpointsList endpointsList = new EndpointsList(); endpointsList.setItems(singletonList(endpoints)); diff --git a/spring-cloud-kubernetes-examples/kubernetes-leader-election-example/pom.xml b/spring-cloud-kubernetes-examples/kubernetes-leader-election-example/pom.xml index 4dc16630..ae237902 100644 --- a/spring-cloud-kubernetes-examples/kubernetes-leader-election-example/pom.xml +++ b/spring-cloud-kubernetes-examples/kubernetes-leader-election-example/pom.xml @@ -83,6 +83,14 @@ true + + maven-surefire-plugin + 2.22.2 + + + maven-failsafe-plugin + 2.22.2 + diff --git a/spring-cloud-kubernetes-examples/kubernetes-leader-election-example/src/test/java/org/springframework/cloud/kubernetes/examples/LeaderControllerTest.java b/spring-cloud-kubernetes-examples/kubernetes-leader-election-example/src/test/java/org/springframework/cloud/kubernetes/examples/LeaderControllerTest.java index fa8c4266..686cbcf2 100644 --- a/spring-cloud-kubernetes-examples/kubernetes-leader-election-example/src/test/java/org/springframework/cloud/kubernetes/examples/LeaderControllerTest.java +++ b/spring-cloud-kubernetes-examples/kubernetes-leader-election-example/src/test/java/org/springframework/cloud/kubernetes/examples/LeaderControllerTest.java @@ -19,11 +19,11 @@ package org.springframework.cloud.kubernetes.examples; import java.net.InetAddress; import java.net.UnknownHostException; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; -import org.mockito.junit.MockitoJUnitRunner; +import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; @@ -36,7 +36,7 @@ import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; -@RunWith(MockitoJUnitRunner.class) +@ExtendWith(MockitoExtension.class) public class LeaderControllerTest { @Mock @@ -52,7 +52,7 @@ public class LeaderControllerTest { private LeaderController leaderController; - @Before + @BeforeEach public void before() throws UnknownHostException { this.host = InetAddress.getLocalHost().getHostName(); this.leaderController = new LeaderController(); diff --git a/spring-cloud-kubernetes-integration-tests/pom.xml b/spring-cloud-kubernetes-integration-tests/pom.xml index fcbd148b..b68d1430 100644 --- a/spring-cloud-kubernetes-integration-tests/pom.xml +++ b/spring-cloud-kubernetes-integration-tests/pom.xml @@ -134,6 +134,8 @@ discovery load-balancer + + diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-configuration-watcher-it/deployment-it.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-configuration-watcher-it/deployment-it.yaml new file mode 100644 index 00000000..7d513fab --- /dev/null +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-configuration-watcher-it/deployment-it.yaml @@ -0,0 +1,27 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + creationTimestamp: null + labels: + app: k8s-demo-app + name: k8s-demo-app +spec: + replicas: 1 + selector: + matchLabels: + app: k8s-demo-app + strategy: {} + template: + metadata: + creationTimestamp: null + labels: + app: k8s-demo-app + spec: + containers: + - image: docker.io/springcloud/spring-cloud-kubernetes-configuration-watcher-it:2.0.0-SNAPSHOT + name: spring-cloud-kubernetes-configuration-watcher-it + resources: {} + env: + - name: SPRING_RABBITMQ_HOST + value: rabbitmq-service +status: {} diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-configuration-watcher-it/kind-config.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-configuration-watcher-it/kind-config.yaml new file mode 100644 index 00000000..90afa077 --- /dev/null +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-configuration-watcher-it/kind-config.yaml @@ -0,0 +1,28 @@ +# this config file contains all config fields with comments +# NOTE: this is not a particularly useful config file +kind: Cluster +apiVersion: kind.x-k8s.io/v1alpha4 +containerdConfigPatches: +- |- + [plugins."io.containerd.grpc.v1.cri".registry.mirrors."localhost:5000"] + endpoint = ["http://kind-registry:5000"] + +# 1 control plane node and 3 workers +nodes: + - role: control-plane + image: kindest/node:v1.14.10@sha256:6cd43ff41ae9f02bb46c8f455d5323819aec858b99534a290517ebc181b443c6 + kubeadmConfigPatches: + - | + kind: InitConfiguration + nodeRegistration: + kubeletExtraArgs: + node-labels: "ingress-ready=true" + extraPortMappings: + - containerPort: 80 + hostPort: 80 + protocol: TCP + - containerPort: 443 + hostPort: 443 + protocol: TCP + - role: worker + image: kindest/node:v1.14.10@sha256:6cd43ff41ae9f02bb46c8f455d5323819aec858b99534a290517ebc181b443c6 diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-configuration-watcher-it/permissions.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-configuration-watcher-it/permissions.yaml new file mode 100644 index 00000000..f5183e04 --- /dev/null +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-configuration-watcher-it/permissions.yaml @@ -0,0 +1,32 @@ +--- +apiVersion: v1 +kind: List +items: + - apiVersion: v1 + kind: ServiceAccount + metadata: + labels: + app: spring-cloud-kubernetes-configuration-watcher + name: spring-cloud-kubernetes-serviceaccount + - apiVersion: rbac.authorization.k8s.io/v1 + kind: RoleBinding + metadata: + labels: + app: spring-cloud-kubernetes-configuration-watcher + name: spring-cloud-kubernetes-serviceaccount:view + roleRef: + kind: Role + apiGroup: rbac.authorization.k8s.io + name: namespace-reader + subjects: + - kind: ServiceAccount + name: spring-cloud-kubernetes-serviceaccount + - 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"] diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-configuration-watcher-it/pom.xml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-configuration-watcher-it/pom.xml new file mode 100644 index 00000000..c01b65a2 --- /dev/null +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-configuration-watcher-it/pom.xml @@ -0,0 +1,106 @@ + + + + org.springframework.cloud + spring-cloud-kubernetes-integration-tests + 2.0.0-SNAPSHOT + + 4.0.0 + + spring-cloud-kubernetes-configuration-watcher-it + jar + + + 2.26.3 + 6.0.1 + 3.2.2 + 4.0.3 + + + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.cloud + spring-cloud-starter-bus-amqp + + + org.springframework.boot + spring-boot-starter-actuator + + + org.springframework.boot + spring-boot-starter-test + test + + + org.junit.vintage + junit-vintage-engine + test + + + com.github.tomakehurst + wiremock-jre8 + ${wiremock.version} + test + + + io.kubernetes + client-java + ${kubernetes-client.version} + test + + + io.kubernetes + client-java-extended + ${kubernetes-client.version} + test + + + com.github.docker-java + docker-java-core + ${docker-java.version} + test + + + com.github.docker-java + docker-java-transport-httpclient5 + ${docker-java.version} + test + + + org.awaitility + awaitility + ${awaitility.version} + test + + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + springcloud/${project.artifactId}:${project.version} + + build-image + + + + package + + build-image + + + + + + + diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-configuration-watcher-it/run.sh b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-configuration-watcher-it/run.sh new file mode 100755 index 00000000..3f266f2b --- /dev/null +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-configuration-watcher-it/run.sh @@ -0,0 +1,143 @@ +#!/bin/bash + +# standard bash error handling +set -o errexit; +set -o pipefail; +set -o nounset; +# debug commands +set -x; + +# working dir to install binaries etc, cleaned up on exit +BIN_DIR="$(mktemp -d)" +# kind binary will be here +KIND="${BIN_DIR}/kind" + +ISTIOCTL="${BIN_DIR}/istio-1.6.2/bin/istioctl" + +CURRENT_DIR="$(pwd)" + +# cleanup on exit (useful for running locally) +cleanup() { + "${KIND}" delete cluster || true + rm -rf "${BIN_DIR}" + + docker kill kind-registry + docker rm kind-registry + docker network rm kind +} +trap cleanup EXIT + +# util to install the latest kind version into ${BIN_DIR} +install_latest_kind() { + # clone kind into a tempdir within BIN_DIR + local tmp_dir + tmp_dir="$(TMPDIR="${BIN_DIR}" mktemp -d "${BIN_DIR}/kind-source.XXXXX")" + cd "${tmp_dir}" || exit + git clone https://github.com/kubernetes-sigs/kind && cd ./kind + make install INSTALL_DIR="${BIN_DIR}" +} + +# util to install a released kind version into ${BIN_DIR} +install_kind_release() { + VERSION="v0.5.1" + KIND_BINARY_URL="https://github.com/kubernetes-sigs/kind/releases/download/${VERSION}/kind-linux-amd64" + if [[ "$OSTYPE" == "darwin"* ]]; then + KIND_BINARY_URL="https://github.com/kubernetes-sigs/kind/releases/download/${VERSION}/kind-darwin-amd64" + elif [[ "$OSTYPE" == "cygwin" ]]; then + KIND_BINARY_URL="https://github.com/kubernetes-sigs/kind/releases/download/${VERSION}/kind-windows-amd64" + elif [[ "$OSTYPE" == "msys" ]]; then + KIND_BINARY_URL="https://github.com/kubernetes-sigs/kind/releases/download/${VERSION}/kind-windows-amd64" + elif [[ "$OSTYPE" == "win32" ]]; then + KIND_BINARY_URL="https://github.com/kubernetes-sigs/kind/releases/download/${VERSION}/kind-windows-amd64" + else + echo "Uknown OS, using linux binary" + fi + wget -O "${KIND}" "${KIND_BINARY_URL}" + chmod +x "${KIND}" +} + +setup_registry() { +set -o errexit + +# create registry container unless it already exists +reg_name='kind-registry' +reg_port='5000' +running="$(docker inspect -f '{{.State.Running}}' "${reg_name}" 2>/dev/null || true)" +if [ "${running}" != 'true' ]; then + docker run \ + -d --restart=always -p "${reg_port}:5000" --name "${reg_name}" \ + registry:2 +fi + +# create a cluster with the local registry enabled in containerd +cat </dev/null || true)" + if [ "${running}" != 'true' ]; then + docker run \ + -d --restart=always -p "${reg_port}:5000" --name "${reg_name}" \ + registry:2 + fi + + #TODO what happens if cluster is already there???? + "${KIND}" create cluster --config kind-config.yaml --loglevel=debug + + # connect the registry to the cluster network + docker network connect "kind" "${reg_name}" + + # tell https://tilt.dev to use the registry + # https://docs.tilt.dev/choosing_clusters.html#discovering-the-registry + for node in $("${KIND}" get nodes); do + kubectl annotate node "${node}" "kind.x-k8s.io/registry=localhost:${reg_port}"; + done + + # set KUBECONFIG to point to the cluster + kubectl cluster-info --context kind-kind + + #setup nginx ingress + kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/master/deploy/static/provider/kind/deploy.yaml + kubectl wait --namespace ingress-nginx --for=condition=ready pod --selector=app.kubernetes.io/component=controller --timeout=360s + + # This creates the service account, role, and role binding necessary for Spring Cloud k8s apps + kubectl apply -f ./permissions.yaml + +# cd ${BIN_DIR} +# curl -L https://istio.io/downloadIstio | sh - + #"${ISTIOCTL}" install --set profile=demo + + cd $CURRENT_DIR + + # TODO: invoke your tests here + ../../mvnw clean install -P it + # teardown will happen automatically on exit +} + +main diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-configuration-watcher-it/src/main/java/org/springframework/cloud/kubernetes/configuration/watcher/ConfigWatcherTestApplication.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-configuration-watcher-it/src/main/java/org/springframework/cloud/kubernetes/configuration/watcher/ConfigWatcherTestApplication.java new file mode 100644 index 00000000..9f8ce737 --- /dev/null +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-configuration-watcher-it/src/main/java/org/springframework/cloud/kubernetes/configuration/watcher/ConfigWatcherTestApplication.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.configuration.watcher; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.cloud.bus.event.RefreshRemoteApplicationEvent; +import org.springframework.context.ApplicationListener; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +@SpringBootApplication +@RestController +public class ConfigWatcherTestApplication + implements ApplicationListener { + + protected Log log = LogFactory.getLog(getClass()); + + private boolean value = false; + + public static void main(String[] args) { + SpringApplication.run(ConfigWatcherTestApplication.class, args); + } + + @GetMapping("/") + public boolean index() { + log.warn("Current value: " + value); + return value; + } + + @Override + public void onApplicationEvent( + RefreshRemoteApplicationEvent refreshRemoteApplicationEvent) { + log.warn("Received remote refresh event"); + this.value = true; + } + +} diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-configuration-watcher-it/src/main/resources/application.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-configuration-watcher-it/src/main/resources/application.yaml new file mode 100644 index 00000000..2a10259c --- /dev/null +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-configuration-watcher-it/src/main/resources/application.yaml @@ -0,0 +1,7 @@ +spring: + application: + name: spring-cloud-kubernetes-configuration-watcher-it + cloud: + bus: + refresh: + enabled: false #disable this because we are going to provide our own refresh listener for testing purposes diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-configuration-watcher-it/src/test/java/org/springframework/cloud/kubernetes/configuration/watcher/ActuatorRefreshIT.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-configuration-watcher-it/src/test/java/org/springframework/cloud/kubernetes/configuration/watcher/ActuatorRefreshIT.java new file mode 100644 index 00000000..c8d67adc --- /dev/null +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-configuration-watcher-it/src/test/java/org/springframework/cloud/kubernetes/configuration/watcher/ActuatorRefreshIT.java @@ -0,0 +1,235 @@ +/* + * 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.configuration.watcher; + +import java.time.Duration; + +import com.github.dockerjava.api.DockerClient; +import com.github.dockerjava.core.DefaultDockerClientConfig; +import com.github.dockerjava.core.DockerClientConfig; +import com.github.dockerjava.core.DockerClientImpl; +import com.github.dockerjava.httpclient5.ApacheDockerHttpClient; +import com.github.dockerjava.transport.DockerHttpClient; +import com.github.tomakehurst.wiremock.client.WireMock; +import io.kubernetes.client.ApiClient; +import io.kubernetes.client.Configuration; +import io.kubernetes.client.apis.AppsV1Api; +import io.kubernetes.client.apis.CoreV1Api; +import io.kubernetes.client.apis.NetworkingV1beta1Api; +import io.kubernetes.client.models.NetworkingV1beta1Ingress; +import io.kubernetes.client.models.V1ConfigMap; +import io.kubernetes.client.models.V1ConfigMapBuilder; +import io.kubernetes.client.models.V1Deployment; +import io.kubernetes.client.models.V1Service; +import io.kubernetes.client.util.Config; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.junit.MockitoJUnitRunner; + +import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; +import static com.github.tomakehurst.wiremock.client.WireMock.findAll; +import static com.github.tomakehurst.wiremock.client.WireMock.post; +import static com.github.tomakehurst.wiremock.client.WireMock.postRequestedFor; +import static com.github.tomakehurst.wiremock.client.WireMock.stubFor; +import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo; +import static com.github.tomakehurst.wiremock.client.WireMock.verify; +import static org.awaitility.Awaitility.await; + +/** + * @author Ryan Baxter + */ +@RunWith(MockitoJUnitRunner.class) +public class ActuatorRefreshIT { + + private static final String KIND_REPO_HOST_PORT = "localhost:5000"; + + private static final String KIND_REPO_URL = "http://" + KIND_REPO_HOST_PORT; + + private static final String IMAGE = "spring-cloud-kubernetes-configuration-watcher"; + + private static final String IMAGE_TAG = "2.0.0-SNAPSHOT"; + + private static final String LOCAL_REPO = "docker.io/springcloud"; + + private static final String LOCAL_IMAGE = LOCAL_REPO + "/" + IMAGE + ":" + IMAGE_TAG; + + private static final String KIND_IMAGE = KIND_REPO_HOST_PORT + "/" + IMAGE; + + private static final String KIND_IMAGE_WITH_TAG = KIND_IMAGE + ":" + IMAGE_TAG; + + private static final String CONFIG_WATCHER_WIREMOCK_DEPLOYMENT_NAME = "config-watcher-wiremock-deployment"; + + private static final String CONFIG_WATCHER_WIREMOCK_APP_NAME = "config-watcher-wiremock"; + + private static final String SPRING_CLOUD_K8S_CONFIG_WATCHER_DEPLOYMENT_NAME = "spring-cloud-kubernetes-configuration-watcher-deployment"; + + private static final String SPRING_CLOUD_K8S_CONFIG_WATCHER_APP_NAME = "spring-cloud-kubernetes-configuration-watcher"; + + private static final String WIREMOCK_HOST = "localhost"; + + private static final String WIREMOCK_PATH = "/wiremock"; + + private static final int WIREMOCK_PORT = 80; + + private static final String NAMESPACE = "default"; + + private ApiClient client; + + private CoreV1Api api; + + private AppsV1Api appsApi; + + private NetworkingV1beta1Api networkingApi; + + private K8SUtils k8SUtils; + + @Before + public void setup() throws Exception { + this.client = Config.defaultClient(); + // client.setDebugging(true); + Configuration.setDefaultApiClient(client); + this.api = new CoreV1Api(); + this.appsApi = new AppsV1Api(); + this.networkingApi = new NetworkingV1beta1Api(); + this.k8SUtils = new K8SUtils(api, appsApi); + + DockerClientConfig config = DefaultDockerClientConfig.createDefaultConfigBuilder() + .withRegistryUrl(KIND_REPO_URL).build(); + DockerHttpClient httpClient = new ApacheDockerHttpClient.Builder() + .dockerHost(config.getDockerHost()).sslConfig(config.getSSLConfig()) + .build(); + + DockerClient dockerClient = DockerClientImpl.getInstance(config, httpClient); + dockerClient.tagImageCmd(LOCAL_IMAGE, KIND_IMAGE, IMAGE_TAG).exec(); + dockerClient.pushImageCmd(KIND_IMAGE_WITH_TAG).start(); + + deployWiremock(); + + // Check to make sure the wiremock deployment is ready + k8SUtils.waitForDeployment(CONFIG_WATCHER_WIREMOCK_DEPLOYMENT_NAME, NAMESPACE); + + // Check to see if endpoint is ready + k8SUtils.waitForEndpointReady(CONFIG_WATCHER_WIREMOCK_APP_NAME, NAMESPACE); + + deployConfigWatcher(); + + // Check to make sure the controller deployment is ready + k8SUtils.waitForDeployment(SPRING_CLOUD_K8S_CONFIG_WATCHER_DEPLOYMENT_NAME, + NAMESPACE); + } + + @Test + public void testActuatorRefresh() throws Exception { + // Configure wiremock to point at the server + WireMock.configureFor(WIREMOCK_HOST, WIREMOCK_PORT, WIREMOCK_PATH); + + // Setup stubs for actuator refresh + stubFor(post(urlEqualTo("/actuator/refresh")) + .willReturn(aResponse().withStatus(200))); + + // Create new configmap to trigger controller to signal app to refresh + V1ConfigMap configMap = new V1ConfigMapBuilder().editOrNewMetadata() + .withName(CONFIG_WATCHER_WIREMOCK_APP_NAME) + .addToLabels("spring.cloud.kubernetes.config", "true").endMetadata() + .addToData("foo", "bar").build(); + api.createNamespacedConfigMap(NAMESPACE, configMap, null, null, null); + + // Wait a bit before we verify + await().atMost(Duration.ofMillis(3400)) + .until(() -> !findAll(postRequestedFor(urlEqualTo("/actuator/refresh"))) + .isEmpty()); + + verify(postRequestedFor(urlEqualTo("/actuator/refresh"))); + } + + @After + public void after() throws Exception { + + appsApi.deleteCollectionNamespacedDeployment(NAMESPACE, null, null, + "metadata.name=" + SPRING_CLOUD_K8S_CONFIG_WATCHER_DEPLOYMENT_NAME, null, + null, null, null, null); + appsApi.deleteCollectionNamespacedDeployment(NAMESPACE, null, null, + "metadata.name=" + CONFIG_WATCHER_WIREMOCK_DEPLOYMENT_NAME, null, null, + null, null, null); + api.deleteNamespacedService(SPRING_CLOUD_K8S_CONFIG_WATCHER_APP_NAME, NAMESPACE, + null, null, null, null, null, null); + api.deleteNamespacedService(CONFIG_WATCHER_WIREMOCK_APP_NAME, NAMESPACE, null, + null, null, null, null, null); + networkingApi.deleteNamespacedIngress("nginx-ingress", NAMESPACE, null, null, + null, null, null, null); + api.deleteNamespacedConfigMap(SPRING_CLOUD_K8S_CONFIG_WATCHER_APP_NAME, NAMESPACE, + null, null, null, null, null, null); + api.deleteNamespacedConfigMap(CONFIG_WATCHER_WIREMOCK_APP_NAME, NAMESPACE, null, + null, null, null, null, null); + } + + private void deployConfigWatcher() throws Exception { + api.createNamespacedConfigMap(NAMESPACE, getConfigWatcherConfigMap(), null, null, + null); + appsApi.createNamespacedDeployment(NAMESPACE, getConfigWatcherDeployment(), null, + null, null); + api.createNamespacedService(NAMESPACE, getConfigWatcherService(), null, null, + null); + } + + private V1Service getConfigWatcherService() throws Exception { + V1Service service = (V1Service) k8SUtils.readYamlFromClasspath( + "spring-cloud-kubernetes-configuration-watcher-service.yaml"); + return service; + } + + private V1ConfigMap getConfigWatcherConfigMap() throws Exception { + V1ConfigMap configMap = (V1ConfigMap) k8SUtils.readYamlFromClasspath( + "spring-cloud-kubernetes-configuration-watcher-configmap.yaml"); + return configMap; + } + + private V1Deployment getConfigWatcherDeployment() throws Exception { + V1Deployment deployment = (V1Deployment) k8SUtils.readYamlFromClasspath( + "spring-cloud-kubernetes-configuration-watcher-http-deployment.yaml"); + return deployment; + } + + private void deployWiremock() throws Exception { + appsApi.createNamespacedDeployment(NAMESPACE, getWireockDeployment(), null, null, + null); + api.createNamespacedService(NAMESPACE, getWiremockAppService(), null, null, null); + networkingApi.createNamespacedIngress(NAMESPACE, getWiremockIngress(), null, null, + null); + } + + private NetworkingV1beta1Ingress getWiremockIngress() throws Exception { + NetworkingV1beta1Ingress ingress = (NetworkingV1beta1Ingress) k8SUtils + .readYamlFromClasspath("wiremock-ingress.yaml"); + return ingress; + } + + private V1Service getWiremockAppService() throws Exception { + V1Service service = (V1Service) k8SUtils + .readYamlFromClasspath("wiremock-service.yaml"); + return service; + } + + private V1Deployment getWireockDeployment() throws Exception { + V1Deployment deployment = (V1Deployment) k8SUtils + .readYamlFromClasspath("wiremock-deployment.yaml"); + return deployment; + } + +} diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-configuration-watcher-it/src/test/java/org/springframework/cloud/kubernetes/configuration/watcher/ActuatorRefreshRabbitMQIT.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-configuration-watcher-it/src/test/java/org/springframework/cloud/kubernetes/configuration/watcher/ActuatorRefreshRabbitMQIT.java new file mode 100644 index 00000000..3ce5e2da --- /dev/null +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-configuration-watcher-it/src/test/java/org/springframework/cloud/kubernetes/configuration/watcher/ActuatorRefreshRabbitMQIT.java @@ -0,0 +1,285 @@ +/* + * 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.configuration.watcher; + +import java.time.Duration; + +import com.github.dockerjava.api.DockerClient; +import com.github.dockerjava.core.DefaultDockerClientConfig; +import com.github.dockerjava.core.DockerClientConfig; +import com.github.dockerjava.core.DockerClientImpl; +import com.github.dockerjava.httpclient5.ApacheDockerHttpClient; +import com.github.dockerjava.transport.DockerHttpClient; +import io.kubernetes.client.ApiClient; +import io.kubernetes.client.Configuration; +import io.kubernetes.client.apis.AppsV1Api; +import io.kubernetes.client.apis.CoreV1Api; +import io.kubernetes.client.apis.NetworkingV1beta1Api; +import io.kubernetes.client.models.NetworkingV1beta1Ingress; +import io.kubernetes.client.models.V1ConfigMap; +import io.kubernetes.client.models.V1ConfigMapBuilder; +import io.kubernetes.client.models.V1Deployment; +import io.kubernetes.client.models.V1ReplicationController; +import io.kubernetes.client.models.V1Service; +import io.kubernetes.client.util.Config; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.junit.MockitoJUnitRunner; + +import org.springframework.boot.web.client.RestTemplateBuilder; +import org.springframework.web.client.RestTemplate; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; + +/** + * @author Ryan Baxter + */ +@RunWith(MockitoJUnitRunner.class) +public class ActuatorRefreshRabbitMQIT { + + private Log log = LogFactory.getLog(getClass()); + + private static final String KIND_REPO_HOST_PORT = "localhost:5000"; + + private static final String KIND_REPO_URL = "http://" + KIND_REPO_HOST_PORT; + + private static final String CONFIG_WATCHER_IMAGE = "spring-cloud-kubernetes-configuration-watcher"; + + private static final String CONFIG_WATCHER_IT_IMAGE = "spring-cloud-kubernetes-configuration-watcher-it"; + + private static final String IMAGE_TAG = "2.0.0-SNAPSHOT"; + + private static final String LOCAL_REPO = "docker.io/springcloud"; + + private static final String CONFIG_WATCHER_LOCAL_IMAGE = LOCAL_REPO + "/" + + CONFIG_WATCHER_IMAGE + ":" + IMAGE_TAG; + + private static final String CONFIG_WATCHER_IT_LOCAL_IMAGE = LOCAL_REPO + "/" + + CONFIG_WATCHER_IT_IMAGE + ":" + IMAGE_TAG; + + private static final String CONFIG_WATCHER_KIND_IMAGE = KIND_REPO_HOST_PORT + "/" + + CONFIG_WATCHER_IMAGE; + + private static final String CONFIG_WATCHER_IT_KIND_IMAGE = KIND_REPO_HOST_PORT + "/" + + CONFIG_WATCHER_IT_IMAGE; + + private static final String CONFIG_WATCHER_KIND_IMAGE_WITH_TAG = CONFIG_WATCHER_KIND_IMAGE + + ":" + IMAGE_TAG; + + private static final String CONFIG_WATCHER_IT_KIND_IMAGE_WITH_TAG = CONFIG_WATCHER_IT_KIND_IMAGE + + ":" + IMAGE_TAG; + + private static final String SPRING_CLOUD_K8S_CONFIG_WATCHER_DEPLOYMENT_NAME = "spring-cloud-kubernetes-configuration-watcher-deployment"; + + private static final String SPRING_CLOUD_K8S_CONFIG_WATCHER_IT_DEPLOYMENT_NAME = "spring-cloud-kubernetes-configuration-watcher-it-deployment"; + + private static final String SPRING_CLOUD_K8S_CONFIG_WATCHER_APP_NAME = "spring-cloud-kubernetes-configuration-watcher"; + + private static final String NAMESPACE = "default"; + + private static final String RABBIT_MQ_CONTROLLER_NAME = "rabbitmq-controller"; + + private ApiClient client; + + private CoreV1Api api; + + private AppsV1Api appsApi; + + private NetworkingV1beta1Api networkingApi; + + private K8SUtils k8SUtils; + + @Before + public void setup() throws Exception { + this.client = Config.defaultClient(); + // client.setDebugging(true); + Configuration.setDefaultApiClient(client); + this.api = new CoreV1Api(); + this.appsApi = new AppsV1Api(); + this.networkingApi = new NetworkingV1beta1Api(); + this.k8SUtils = new K8SUtils(api, appsApi); + + DockerClientConfig config = DefaultDockerClientConfig.createDefaultConfigBuilder() + .withRegistryUrl(KIND_REPO_URL).build(); + DockerHttpClient httpClient = new ApacheDockerHttpClient.Builder() + .dockerHost(config.getDockerHost()).sslConfig(config.getSSLConfig()) + .build(); + + DockerClient dockerClient = DockerClientImpl.getInstance(config, httpClient); + dockerClient.tagImageCmd(CONFIG_WATCHER_LOCAL_IMAGE, CONFIG_WATCHER_KIND_IMAGE, + IMAGE_TAG).exec(); + dockerClient.pushImageCmd(CONFIG_WATCHER_KIND_IMAGE_WITH_TAG).start(); + + dockerClient.tagImageCmd(CONFIG_WATCHER_IT_LOCAL_IMAGE, + CONFIG_WATCHER_IT_KIND_IMAGE, IMAGE_TAG).exec(); + dockerClient.pushImageCmd(CONFIG_WATCHER_IT_KIND_IMAGE_WITH_TAG).start(); + + deployRabbitMQ(); + + k8SUtils.waitForReplicationController(RABBIT_MQ_CONTROLLER_NAME, NAMESPACE); + + deployTestApp(); + + k8SUtils.waitForDeployment(SPRING_CLOUD_K8S_CONFIG_WATCHER_IT_DEPLOYMENT_NAME, + NAMESPACE); + + deployConfigWatcher(); + + // Check to make sure the controller deployment is ready + k8SUtils.waitForDeployment(SPRING_CLOUD_K8S_CONFIG_WATCHER_DEPLOYMENT_NAME, + NAMESPACE); + } + + @Test + public void testRefresh() throws Exception { + // Create new configmap to trigger controller to signal app to refresh + V1ConfigMap configMap = new V1ConfigMapBuilder().editOrNewMetadata() + .withName(CONFIG_WATCHER_IT_IMAGE) + .addToLabels("spring.cloud.kubernetes.config", "true").endMetadata() + .addToData("foo", "hello world").build(); + api.createNamespacedConfigMap(NAMESPACE, configMap, null, null, null); + RestTemplate rest = new RestTemplateBuilder().build(); + // Wait a bit before we verify + await().pollInterval(Duration.ofSeconds(1)).atMost(Duration.ofSeconds(90)) + .until(() -> { + Boolean value = rest.getForObject("http://localhost:80/it", + Boolean.class); + log.info("Returned " + value + " from http://localhost:80/it"); + return value; + }); + + assertThat(rest.getForObject("http://localhost:80/it", Boolean.class)).isTrue(); + } + + @After + public void after() throws Exception { + api.deleteNamespacedService("rabbitmq-service", NAMESPACE, null, null, null, null, + null, null); + api.deleteNamespacedService(CONFIG_WATCHER_IT_IMAGE, NAMESPACE, null, null, null, + null, null, null); + api.deleteNamespacedService(SPRING_CLOUD_K8S_CONFIG_WATCHER_APP_NAME, NAMESPACE, + null, null, null, null, null, null); + + appsApi.deleteNamespacedDeployment( + SPRING_CLOUD_K8S_CONFIG_WATCHER_DEPLOYMENT_NAME, NAMESPACE, null, null, + null, null, null, null); + appsApi.deleteNamespacedDeployment( + SPRING_CLOUD_K8S_CONFIG_WATCHER_IT_DEPLOYMENT_NAME, NAMESPACE, null, null, + null, null, null, null); + + try { + api.deleteNamespacedReplicationController(RABBIT_MQ_CONTROLLER_NAME, + NAMESPACE, null, null, null, null, null, null); + } + catch (Exception e) { + // swallowing this exception, the delete does actually happen, its a problem + // downstream from the k8s + // client + // see + // https://github.com/kubernetes-client/java/issues/86#issuecomment-411234259 + } + + networkingApi.deleteNamespacedIngress("it-ingress", NAMESPACE, null, null, null, + null, null, null); + + api.deleteNamespacedConfigMap(SPRING_CLOUD_K8S_CONFIG_WATCHER_APP_NAME, NAMESPACE, + null, null, null, null, null, null); + api.deleteNamespacedConfigMap(CONFIG_WATCHER_IT_IMAGE, NAMESPACE, null, null, + null, null, null, null); + + } + + private void deployTestApp() throws Exception { + appsApi.createNamespacedDeployment(NAMESPACE, getItDeployment(), null, null, + null); + api.createNamespacedService(NAMESPACE, getItAppService(), null, null, null); + networkingApi.createNamespacedIngress(NAMESPACE, getItIngress(), null, null, + null); + } + + private void deployConfigWatcher() throws Exception { + api.createNamespacedConfigMap(NAMESPACE, getConfigWatcherConfigMap(), null, null, + null); + appsApi.createNamespacedDeployment(NAMESPACE, getConfigWatcherDeployment(), null, + null, null); + api.createNamespacedService(NAMESPACE, getConfigWatcherService(), null, null, + null); + } + + private void deployRabbitMQ() throws Exception { + api.createNamespacedService(NAMESPACE, getRabbitMQService(), null, null, null); + api.createNamespacedReplicationController(NAMESPACE, + getRabbitMQRepplicationController(), null, null, null); + } + + private V1Service getConfigWatcherService() throws Exception { + V1Service service = (V1Service) k8SUtils.readYamlFromClasspath( + "spring-cloud-kubernetes-configuration-watcher-service.yaml"); + return service; + } + + private V1ConfigMap getConfigWatcherConfigMap() throws Exception { + V1ConfigMap configMap = (V1ConfigMap) k8SUtils.readYamlFromClasspath( + "spring-cloud-kubernetes-configuration-watcher-configmap.yaml"); + return configMap; + } + + private V1Deployment getConfigWatcherDeployment() throws Exception { + V1Deployment deployment = (V1Deployment) k8SUtils.readYamlFromClasspath( + "spring-cloud-kubernetes-configuration-watcher-bus-deployment.yaml"); + return deployment; + } + + private V1Service getItAppService() throws Exception { + String urlString = "spring-cloud-kubernetes-configuration-watcher-it-service.yaml"; + V1Service service = (V1Service) k8SUtils.readYamlFromClasspath(urlString); + return service; + } + + private V1Deployment getItDeployment() throws Exception { + String urlString = "spring-cloud-kubernetes-configuration-watcher-it-deployment.yaml"; + V1Deployment deployment = (V1Deployment) k8SUtils + .readYamlFromClasspath(urlString); + return deployment; + } + + private NetworkingV1beta1Ingress getItIngress() throws Exception { + String urlString = "spring-cloud-kubernetes-configuration-watcher-it-ingress.yaml"; + NetworkingV1beta1Ingress ingress = (NetworkingV1beta1Ingress) k8SUtils + .readYamlFromClasspath(urlString); + return ingress; + } + + private V1ReplicationController getRabbitMQRepplicationController() throws Exception { + String urlString = "rabbitmq-controller.yaml"; + V1ReplicationController replicationController = (V1ReplicationController) k8SUtils + .readYamlFromClasspath(urlString); + return replicationController; + } + + private V1Service getRabbitMQService() throws Exception { + String urlString = "rabbitmq-service.yaml"; + V1Service service = (V1Service) k8SUtils.readYamlFromClasspath(urlString); + return service; + } + +} diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-configuration-watcher-it/src/test/java/org/springframework/cloud/kubernetes/configuration/watcher/K8SUtils.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-configuration-watcher-it/src/test/java/org/springframework/cloud/kubernetes/configuration/watcher/K8SUtils.java new file mode 100644 index 00000000..e953f956 --- /dev/null +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-configuration-watcher-it/src/test/java/org/springframework/cloud/kubernetes/configuration/watcher/K8SUtils.java @@ -0,0 +1,200 @@ +/* + * 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.configuration.watcher; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileReader; +import java.io.InputStreamReader; +import java.net.URL; +import java.time.Duration; +import java.util.Collection; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +import io.kubernetes.client.ApiException; +import io.kubernetes.client.apis.AppsV1Api; +import io.kubernetes.client.apis.CoreV1Api; +import io.kubernetes.client.models.V1Deployment; +import io.kubernetes.client.models.V1DeploymentBuilder; +import io.kubernetes.client.models.V1DeploymentList; +import io.kubernetes.client.models.V1Endpoints; +import io.kubernetes.client.models.V1EndpointsList; +import io.kubernetes.client.models.V1EnvVar; +import io.kubernetes.client.models.V1ReplicationController; +import io.kubernetes.client.models.V1ReplicationControllerList; +import io.kubernetes.client.models.V1Service; +import io.kubernetes.client.models.V1ServiceBuilder; +import io.kubernetes.client.util.Yaml; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import static org.awaitility.Awaitility.await; +import static org.junit.Assert.fail; + +/** + * @author Ryan Baxter + */ +public class K8SUtils { + + private Log log = LogFactory.getLog(getClass()); + + private CoreV1Api api; + + private AppsV1Api appsApi; + + public K8SUtils(CoreV1Api api, AppsV1Api appsApi) { + this.api = api; + this.appsApi = appsApi; + } + + public Object readYaml(String urlString) throws Exception { + // create the url + URL url = new URL(urlString); + BufferedReader reader = null; + Object yamlObj = null; + try { + // open the url stream, wrap it an a few "readers" + reader = new BufferedReader(new InputStreamReader(url.openStream())); + yamlObj = Yaml.load(reader); + } + catch (Exception e) { + throw e; + } + finally { + if (reader != null) { + reader.close(); + } + } + return yamlObj; + } + + public Object readYamlFromClasspath(String fileName) throws Exception { + ClassLoader classLoader = getClass().getClassLoader(); + File file = new File(classLoader.getResource(fileName).getFile()); + BufferedReader reader = null; + Object yamlObj = null; + try { + // open the url stream, wrap it an a few "readers" + reader = new BufferedReader(new FileReader(file)); + yamlObj = Yaml.load(reader); + } + catch (Exception e) { + throw e; + } + finally { + if (reader != null) { + reader.close(); + } + } + return yamlObj; + } + + public V1Service createService(String name, Map labels, + Map specSelectors, String type, String portName, int port, + int targetPort, String namespace) throws ApiException { + V1Service wiremockService = new V1ServiceBuilder().editOrNewMetadata() + .withName(name).addToLabels(labels).endMetadata().editOrNewSpec() + .addToSelector(specSelectors).withNewType(type).addNewPort() + .withName(portName).withPort(port).withNewTargetPort(targetPort).endPort() + .endSpec().build(); + return api.createNamespacedService(namespace, wiremockService, null, null, null); + } + + public V1Deployment createDeployment(String name, + Map selectorMatchLabels, + Map templateMetadataLabels, String containerName, + String image, String pullPolicy, int containerPort, int readinessProbePort, + String readinessProbePath, int livenessProbePort, String livenessProbePath, + String serviceAccountName, Collection envVars, String namespace) + throws ApiException { + + V1Deployment wiremockDeployment = new V1DeploymentBuilder().editOrNewMetadata() + .withName(name).endMetadata().editOrNewSpec().withNewSelector() + .addToMatchLabels(selectorMatchLabels).endSelector().editOrNewTemplate() + .editOrNewMetadata().addToLabels(templateMetadataLabels).endMetadata() + .editOrNewSpec().withServiceAccountName(serviceAccountName) + .addNewContainer().withName(containerName).withImage(image) + .withImagePullPolicy(pullPolicy).addNewPort() + .withContainerPort(containerPort).endPort().editOrNewReadinessProbe() + .editOrNewHttpGet().withNewPort(readinessProbePort) + .withNewPath(readinessProbePath).endHttpGet().endReadinessProbe() + .editOrNewLivenessProbe().editOrNewHttpGet() + .withNewPort(livenessProbePort).withNewPath(livenessProbePath) + .endHttpGet().endLivenessProbe().addAllToEnv(envVars).endContainer() + .endSpec().endTemplate().endSpec().build(); + return appsApi.createNamespacedDeployment(namespace, wiremockDeployment, null, + null, null); + + } + + public void waitForEndpointReady(String name, String namespace) throws Exception { + await().pollInterval(Duration.ofSeconds(1)).atMost(90, TimeUnit.SECONDS) + .until(() -> isEndpointReady(name, namespace)); + } + + public boolean isEndpointReady(String name, String namespace) throws ApiException { + V1EndpointsList endpoints = api.listNamespacedEndpoints(namespace, null, null, + "metadata.name=" + name, null, null, null, null, null); + if (endpoints.getItems().isEmpty()) { + fail("no endpoints for " + name); + } + V1Endpoints endpoint = endpoints.getItems().get(0); + return endpoint.getSubsets().get(0).getAddresses().size() >= 1; + } + + public void waitForReplicationController(String name, String namespace) { + await().pollInterval(Duration.ofSeconds(1)).atMost(90, TimeUnit.SECONDS) + .until(() -> isReplicationControllerReady(name, namespace)); + } + + public boolean isReplicationControllerReady(String name, String namespace) + throws ApiException { + V1ReplicationControllerList controllerList = api + .listNamespacedReplicationController(namespace, null, null, + "metadata.name=" + name, null, null, null, null, null); + if (controllerList.getItems().size() < 1) { + fail("Replication controller with name " + name + "could not be found"); + } + + V1ReplicationController replicationController = controllerList.getItems().get(0); + Integer availableReplicas = replicationController.getStatus() + .getAvailableReplicas(); + log.info("Available replicas for " + name + ": " + availableReplicas); + return availableReplicas != null && availableReplicas >= 1; + + } + + public void waitForDeployment(String deploymentName, String namespace) { + await().pollInterval(Duration.ofSeconds(1)).atMost(90, TimeUnit.SECONDS) + .until(() -> isDeployentReady(deploymentName, namespace)); + } + + public boolean isDeployentReady(String deploymentName, String namespace) + throws ApiException { + V1DeploymentList deployments = appsApi.listNamespacedDeployment(namespace, null, + null, "metadata.name=" + deploymentName, null, null, null, null, null); + if (deployments.getItems().size() < 1) { + fail("No deployments with the name " + deploymentName); + } + V1Deployment deployment = deployments.getItems().get(0); + Integer availableReplicas = deployment.getStatus().getAvailableReplicas(); + log.info("Available replicas for " + deploymentName + ": " + availableReplicas); + return availableReplicas != null && availableReplicas >= 1; + } + +} diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-configuration-watcher-it/src/test/resources/rabbitmq-controller.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-configuration-watcher-it/src/test/resources/rabbitmq-controller.yaml new file mode 100644 index 00000000..81dd5aec --- /dev/null +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-configuration-watcher-it/src/test/resources/rabbitmq-controller.yaml @@ -0,0 +1,36 @@ +apiVersion: v1 +kind: ReplicationController +metadata: + labels: + component: rabbitmq + name: rabbitmq-controller +spec: + replicas: 1 + template: + metadata: + labels: + app: taskQueue + component: rabbitmq + spec: + containers: + - image: rabbitmq:3-management + name: rabbitmq + ports: + - name: amqp + containerPort: 5672 + - name: http-stats + containerPort: 15672 + readinessProbe: + httpGet: + port: 15672 + path: /api/healthchecks/node + httpHeaders: + - name: Authorization + value: Basic Z3Vlc3Q6Z3Vlc3Q= +# livenessProbe: +# httpGet: +# port: 15672 +# path: /api/healthchecks/node +# httpHeaders: +# - name: Authorization +# value: Basic Z3Vlc3Q6Z3Vlc3Q= diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-configuration-watcher-it/src/test/resources/rabbitmq-service.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-configuration-watcher-it/src/test/resources/rabbitmq-service.yaml new file mode 100644 index 00000000..e960e5f4 --- /dev/null +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-configuration-watcher-it/src/test/resources/rabbitmq-service.yaml @@ -0,0 +1,17 @@ +apiVersion: v1 +kind: Service +metadata: + labels: + component: rabbitmq + name: rabbitmq-service +spec: + ports: + - port: 5672 + name: amqp + targetPort: 5672 + - port: 15672 + name: http-stats + targetPort: 15672 + selector: + app: taskQueue + component: rabbitmq diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-configuration-watcher-it/src/test/resources/spring-cloud-kubernetes-configuration-watcher-bus-deployment.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-configuration-watcher-it/src/test/resources/spring-cloud-kubernetes-configuration-watcher-bus-deployment.yaml new file mode 100644 index 00000000..2a6bd3e5 --- /dev/null +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-configuration-watcher-it/src/test/resources/spring-cloud-kubernetes-configuration-watcher-bus-deployment.yaml @@ -0,0 +1,33 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: spring-cloud-kubernetes-configuration-watcher-deployment +spec: + selector: + matchLabels: + app: spring-cloud-kubernetes-configuration-watcher + template: + metadata: + labels: + app: spring-cloud-kubernetes-configuration-watcher + spec: + serviceAccountName: spring-cloud-kubernetes-serviceaccount + containers: + - name: spring-cloud-kubernetes-configuration-watcher + image: localhost:5000/spring-cloud-kubernetes-configuration-watcher:2.0.0-SNAPSHOT + imagePullPolicy: IfNotPresent + env: + - name: SPRING_PROFILES_ACTIVE + value: bus + - name: SPRING_RABBITMQ_HOST + value: rabbitmq-service + 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-integration-tests/spring-cloud-kubernetes-configuration-watcher-it/src/test/resources/spring-cloud-kubernetes-configuration-watcher-configmap.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-configuration-watcher-it/src/test/resources/spring-cloud-kubernetes-configuration-watcher-configmap.yaml new file mode 100644 index 00000000..9c2ea62e --- /dev/null +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-configuration-watcher-it/src/test/resources/spring-cloud-kubernetes-configuration-watcher-configmap.yaml @@ -0,0 +1,9 @@ +apiVersion: v1 +data: + application.properties: |- + # Set the refresh interval to 0 so the refresh event gets sent immediately + spring.cloud.kubernetes.configuration.watcher.refreshDelay=0 + logging.level.org.springframework.cloud.kubernetes=TRACE +kind: ConfigMap +metadata: + name: spring-cloud-kubernetes-configuration-watcher diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-configuration-watcher-it/src/test/resources/spring-cloud-kubernetes-configuration-watcher-http-deployment.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-configuration-watcher-it/src/test/resources/spring-cloud-kubernetes-configuration-watcher-http-deployment.yaml new file mode 100644 index 00000000..68089e13 --- /dev/null +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-configuration-watcher-it/src/test/resources/spring-cloud-kubernetes-configuration-watcher-http-deployment.yaml @@ -0,0 +1,28 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: spring-cloud-kubernetes-configuration-watcher-deployment +spec: + selector: + matchLabels: + app: spring-cloud-kubernetes-configuration-watcher + template: + metadata: + labels: + app: spring-cloud-kubernetes-configuration-watcher + spec: + serviceAccountName: spring-cloud-kubernetes-serviceaccount + containers: + - name: spring-cloud-kubernetes-configuration-watcher + image: localhost:5000/spring-cloud-kubernetes-configuration-watcher:2.0.0-SNAPSHOT + imagePullPolicy: IfNotPresent + 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-integration-tests/spring-cloud-kubernetes-configuration-watcher-it/src/test/resources/spring-cloud-kubernetes-configuration-watcher-it-deployment.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-configuration-watcher-it/src/test/resources/spring-cloud-kubernetes-configuration-watcher-it-deployment.yaml new file mode 100644 index 00000000..949adc28 --- /dev/null +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-configuration-watcher-it/src/test/resources/spring-cloud-kubernetes-configuration-watcher-it-deployment.yaml @@ -0,0 +1,30 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: spring-cloud-kubernetes-configuration-watcher-it-deployment +spec: + selector: + matchLabels: + app: spring-cloud-kubernetes-configuration-watcher-it + template: + metadata: + labels: + app: spring-cloud-kubernetes-configuration-watcher-it + spec: + containers: + - name: spring-cloud-kubernetes-configuration-watcher-it + image: localhost:5000/spring-cloud-kubernetes-configuration-watcher-it:2.0.0-SNAPSHOT + imagePullPolicy: IfNotPresent + env: + - name: SPRING_RABBITMQ_HOST + value: rabbitmq-service + 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-configuration-watcher-it/src/test/resources/spring-cloud-kubernetes-configuration-watcher-it-ingress.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-configuration-watcher-it/src/test/resources/spring-cloud-kubernetes-configuration-watcher-it-ingress.yaml new file mode 100644 index 00000000..8f8d3473 --- /dev/null +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-configuration-watcher-it/src/test/resources/spring-cloud-kubernetes-configuration-watcher-it-ingress.yaml @@ -0,0 +1,14 @@ +apiVersion: networking.k8s.io/v1beta1 +kind: Ingress +metadata: + name: it-ingress + annotations: + nginx.ingress.kubernetes.io/rewrite-target: /$2 +spec: + rules: + - http: + paths: + - path: /it(/|$)(.*) + backend: + serviceName: spring-cloud-kubernetes-configuration-watcher-it + servicePort: 8080 diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-configuration-watcher-it/src/test/resources/spring-cloud-kubernetes-configuration-watcher-it-service.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-configuration-watcher-it/src/test/resources/spring-cloud-kubernetes-configuration-watcher-it-service.yaml new file mode 100644 index 00000000..7b8a1c21 --- /dev/null +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-configuration-watcher-it/src/test/resources/spring-cloud-kubernetes-configuration-watcher-it-service.yaml @@ -0,0 +1,14 @@ +apiVersion: v1 +kind: Service +metadata: + labels: + app: spring-cloud-kubernetes-configuration-watcher-it + name: spring-cloud-kubernetes-configuration-watcher-it +spec: + ports: + - name: http + port: 8080 + targetPort: 8080 + selector: + app: spring-cloud-kubernetes-configuration-watcher-it + type: ClusterIP diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-configuration-watcher-it/src/test/resources/spring-cloud-kubernetes-configuration-watcher-service.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-configuration-watcher-it/src/test/resources/spring-cloud-kubernetes-configuration-watcher-service.yaml new file mode 100644 index 00000000..c8496317 --- /dev/null +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-configuration-watcher-it/src/test/resources/spring-cloud-kubernetes-configuration-watcher-service.yaml @@ -0,0 +1,14 @@ +apiVersion: v1 +kind: Service +metadata: + labels: + app: spring-cloud-kubernetes-configuration-watcher + name: spring-cloud-kubernetes-configuration-watcher +spec: + ports: + - name: http + port: 8888 + targetPort: 8888 + selector: + app: spring-cloud-kubernetes-configuration-watcher + type: ClusterIP diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-configuration-watcher-it/src/test/resources/wiremock-deployment.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-configuration-watcher-it/src/test/resources/wiremock-deployment.yaml new file mode 100644 index 00000000..2ee9b179 --- /dev/null +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-configuration-watcher-it/src/test/resources/wiremock-deployment.yaml @@ -0,0 +1,27 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: config-watcher-wiremock-deployment +spec: + selector: + matchLabels: + app: config-watcher-wiremock + template: + metadata: + labels: + app: config-watcher-wiremock + spec: + containers: + - name: config-watcher-wiremock + image: rodolpheche/wiremock + imagePullPolicy: IfNotPresent + readinessProbe: + httpGet: + port: 8080 + path: /__admin/mappings + livenessProbe: + httpGet: + port: 8080 + path: /__admin/mappings + ports: + - containerPort: 8080 diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-configuration-watcher-it/src/test/resources/wiremock-ingress.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-configuration-watcher-it/src/test/resources/wiremock-ingress.yaml new file mode 100644 index 00000000..bf6835a0 --- /dev/null +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-configuration-watcher-it/src/test/resources/wiremock-ingress.yaml @@ -0,0 +1,14 @@ +apiVersion: networking.k8s.io/v1beta1 +kind: Ingress +metadata: + name: nginx-ingress + annotations: + nginx.ingress.kubernetes.io/rewrite-target: /$2 +spec: + rules: + - http: + paths: + - path: /wiremock(/|$)(.*) + backend: + serviceName: config-watcher-wiremock + servicePort: 8080 diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-configuration-watcher-it/src/test/resources/wiremock-service.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-configuration-watcher-it/src/test/resources/wiremock-service.yaml new file mode 100644 index 00000000..559c7645 --- /dev/null +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-configuration-watcher-it/src/test/resources/wiremock-service.yaml @@ -0,0 +1,14 @@ +apiVersion: v1 +kind: Service +metadata: + labels: + app: config-watcher-wiremock + name: config-watcher-wiremock +spec: + ports: + - name: http + port: 8080 + targetPort: 8080 + selector: + app: config-watcher-wiremock + type: ClusterIP diff --git a/spring-cloud-kubernetes-leader/pom.xml b/spring-cloud-kubernetes-leader/pom.xml index 4f9c39ef..64932d90 100644 --- a/spring-cloud-kubernetes-leader/pom.xml +++ b/spring-cloud-kubernetes-leader/pom.xml @@ -61,15 +61,21 @@ org.springframework.boot spring-boot-starter-test test - - - org.junit.vintage - junit-vintage-engine - test + + + org.junit.vintage + junit-vintage-engine + + + + org.apache.maven.plugins + maven-surefire-plugin + ${maven-surefire-plugin.version} + org.apache.maven.plugins maven-failsafe-plugin diff --git a/spring-cloud-kubernetes-leader/src/test/java/org/springframework/cloud/kubernetes/leader/LeaderAutoConfigurationTests.java b/spring-cloud-kubernetes-leader/src/test/java/org/springframework/cloud/kubernetes/leader/LeaderAutoConfigurationTests.java index 0072a811..fee60341 100644 --- a/spring-cloud-kubernetes-leader/src/test/java/org/springframework/cloud/kubernetes/leader/LeaderAutoConfigurationTests.java +++ b/spring-cloud-kubernetes-leader/src/test/java/org/springframework/cloud/kubernetes/leader/LeaderAutoConfigurationTests.java @@ -16,8 +16,9 @@ package org.springframework.cloud.kubernetes.leader; -import org.junit.Test; -import org.junit.runner.RunWith; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; @@ -25,12 +26,11 @@ import org.springframework.boot.SpringBootConfiguration; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.http.MediaType; -import org.springframework.test.context.junit4.SpringRunner; import org.springframework.test.web.reactive.server.WebTestClient; import static org.hamcrest.Matchers.containsString; -@RunWith(SpringRunner.class) +@ExtendWith(MockitoExtension.class) @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, properties = { // Make sure test passes without Kubernetes cluster "spring.cloud.kubernetes.leader.autoStartup=false" }) diff --git a/spring-cloud-kubernetes-leader/src/test/java/org/springframework/cloud/kubernetes/leader/LeaderContextTest.java b/spring-cloud-kubernetes-leader/src/test/java/org/springframework/cloud/kubernetes/leader/LeaderContextTest.java index 7681d91b..cb291fed 100644 --- a/spring-cloud-kubernetes-leader/src/test/java/org/springframework/cloud/kubernetes/leader/LeaderContextTest.java +++ b/spring-cloud-kubernetes-leader/src/test/java/org/springframework/cloud/kubernetes/leader/LeaderContextTest.java @@ -18,11 +18,11 @@ package org.springframework.cloud.kubernetes.leader; import java.util.Optional; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; -import org.mockito.junit.MockitoJUnitRunner; +import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.integration.leader.Candidate; @@ -33,7 +33,7 @@ import static org.mockito.Mockito.verify; /** * @author Gytis Trikleris */ -@RunWith(MockitoJUnitRunner.class) +@ExtendWith(MockitoExtension.class) public class LeaderContextTest { @Mock @@ -47,7 +47,7 @@ public class LeaderContextTest { private LeaderContext leaderContext; - @Before + @BeforeEach public void before() { this.leaderContext = new LeaderContext(this.mockCandidate, this.mockLeadershipController); diff --git a/spring-cloud-kubernetes-leader/src/test/java/org/springframework/cloud/kubernetes/leader/LeaderInfoContributorTest.java b/spring-cloud-kubernetes-leader/src/test/java/org/springframework/cloud/kubernetes/leader/LeaderInfoContributorTest.java index abb04c3e..97de1bb3 100644 --- a/spring-cloud-kubernetes-leader/src/test/java/org/springframework/cloud/kubernetes/leader/LeaderInfoContributorTest.java +++ b/spring-cloud-kubernetes-leader/src/test/java/org/springframework/cloud/kubernetes/leader/LeaderInfoContributorTest.java @@ -19,11 +19,11 @@ package org.springframework.cloud.kubernetes.leader; import java.util.Map; import java.util.Optional; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; -import org.mockito.junit.MockitoJUnitRunner; +import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.boot.actuate.info.Info; import org.springframework.integration.leader.Candidate; @@ -31,7 +31,7 @@ import org.springframework.integration.leader.Candidate; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.BDDMockito.given; -@RunWith(MockitoJUnitRunner.class) +@ExtendWith(MockitoExtension.class) public class LeaderInfoContributorTest { @Mock @@ -45,7 +45,7 @@ public class LeaderInfoContributorTest { private LeaderInfoContributor leaderInfoContributor; - @Before + @BeforeEach public void before() { this.leaderInfoContributor = new LeaderInfoContributor( this.mockLeadershipController, this.mockCandidate); diff --git a/spring-cloud-kubernetes-leader/src/test/java/org/springframework/cloud/kubernetes/leader/LeaderInitiatorTest.java b/spring-cloud-kubernetes-leader/src/test/java/org/springframework/cloud/kubernetes/leader/LeaderInitiatorTest.java index 92e05f9b..844e66b2 100644 --- a/spring-cloud-kubernetes-leader/src/test/java/org/springframework/cloud/kubernetes/leader/LeaderInitiatorTest.java +++ b/spring-cloud-kubernetes-leader/src/test/java/org/springframework/cloud/kubernetes/leader/LeaderInitiatorTest.java @@ -18,12 +18,12 @@ package org.springframework.cloud.kubernetes.leader; import java.time.Duration; -import org.junit.After; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; -import org.mockito.junit.MockitoJUnitRunner; +import org.mockito.junit.jupiter.MockitoExtension; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.BDDMockito.given; @@ -33,7 +33,7 @@ import static org.mockito.internal.verification.VerificationModeFactory.atLeastO /** * @author Gytis Trikleris */ -@RunWith(MockitoJUnitRunner.class) +@ExtendWith(MockitoExtension.class) public class LeaderInitiatorTest { @Mock @@ -53,14 +53,14 @@ public class LeaderInitiatorTest { private LeaderInitiator leaderInitiator; - @Before + @BeforeEach public void before() { this.leaderInitiator = new LeaderInitiator(this.mockLeaderProperties, this.mockLeadershipController, this.mockLeaderRecordWatcher, this.mockPodReadinessWatcher); } - @After + @AfterEach public void after() { this.leaderInitiator.stop(); } diff --git a/spring-cloud-kubernetes-leader/src/test/java/org/springframework/cloud/kubernetes/leader/LeaderRecordWatcherTest.java b/spring-cloud-kubernetes-leader/src/test/java/org/springframework/cloud/kubernetes/leader/LeaderRecordWatcherTest.java index 59cfead8..a609ee43 100644 --- a/spring-cloud-kubernetes-leader/src/test/java/org/springframework/cloud/kubernetes/leader/LeaderRecordWatcherTest.java +++ b/spring-cloud-kubernetes-leader/src/test/java/org/springframework/cloud/kubernetes/leader/LeaderRecordWatcherTest.java @@ -26,11 +26,11 @@ import io.fabric8.kubernetes.client.Watcher; import io.fabric8.kubernetes.client.dsl.MixedOperation; import io.fabric8.kubernetes.client.dsl.NonNamespaceOperation; import io.fabric8.kubernetes.client.dsl.Resource; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; -import org.mockito.junit.MockitoJUnitRunner; +import org.mockito.junit.jupiter.MockitoExtension; import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.times; @@ -39,7 +39,7 @@ import static org.mockito.Mockito.verify; /** * @author Gytis Trikleris */ -@RunWith(MockitoJUnitRunner.class) +@ExtendWith(MockitoExtension.class) public class LeaderRecordWatcherTest { @Mock @@ -71,22 +71,15 @@ public class LeaderRecordWatcherTest { private LeaderRecordWatcher watcher; - @Before + @BeforeEach public void before() { this.watcher = new LeaderRecordWatcher(this.mockLeaderProperties, this.mockLeadershipController, this.mockKubernetesClient); - - given(this.mockKubernetesClient.configMaps()) - .willReturn(this.mockConfigMapsOperation); - given(this.mockConfigMapsOperation.inNamespace(null)) - .willReturn(this.mockInNamespaceOperation); - given(this.mockInNamespaceOperation.withName(null)) - .willReturn(this.mockWithNameResource); - given(this.mockWithNameResource.watch(this.watcher)).willReturn(this.mockWatch); } @Test public void shouldStartOnce() { + initStubs(); this.watcher.start(); this.watcher.start(); @@ -95,6 +88,7 @@ public class LeaderRecordWatcherTest { @Test public void shouldStopOnce() { + initStubs(); this.watcher.start(); this.watcher.stop(); this.watcher.stop(); @@ -120,6 +114,7 @@ public class LeaderRecordWatcherTest { @Test public void shouldHandleClose() { + initStubs(); this.watcher.onClose(this.mockKubernetesClientException); verify(this.mockWithNameResource).watch(this.watcher); @@ -132,4 +127,14 @@ public class LeaderRecordWatcherTest { verify(this.mockWithNameResource, times(0)).watch(this.watcher); } + private void initStubs() { + given(this.mockKubernetesClient.configMaps()) + .willReturn(this.mockConfigMapsOperation); + given(this.mockConfigMapsOperation.inNamespace(null)) + .willReturn(this.mockInNamespaceOperation); + given(this.mockInNamespaceOperation.withName(null)) + .willReturn(this.mockWithNameResource); + given(this.mockWithNameResource.watch(this.watcher)).willReturn(this.mockWatch); + } + } diff --git a/spring-cloud-kubernetes-leader/src/test/java/org/springframework/cloud/kubernetes/leader/LeaderTest.java b/spring-cloud-kubernetes-leader/src/test/java/org/springframework/cloud/kubernetes/leader/LeaderTest.java index 9be158ba..93e8753f 100644 --- a/spring-cloud-kubernetes-leader/src/test/java/org/springframework/cloud/kubernetes/leader/LeaderTest.java +++ b/spring-cloud-kubernetes-leader/src/test/java/org/springframework/cloud/kubernetes/leader/LeaderTest.java @@ -16,11 +16,11 @@ package org.springframework.cloud.kubernetes.leader; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; -import org.mockito.junit.MockitoJUnitRunner; +import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.integration.leader.Candidate; @@ -30,7 +30,7 @@ import static org.mockito.BDDMockito.given; /** * @author Gytis Trikleris */ -@RunWith(MockitoJUnitRunner.class) +@ExtendWith(MockitoExtension.class) public class LeaderTest { private static final String ROLE = "test-role"; @@ -42,7 +42,7 @@ public class LeaderTest { private Leader leader; - @Before + @BeforeEach public void before() { this.leader = new Leader(ROLE, ID); } diff --git a/spring-cloud-kubernetes-leader/src/test/java/org/springframework/cloud/kubernetes/leader/LeadershipControllerTest.java b/spring-cloud-kubernetes-leader/src/test/java/org/springframework/cloud/kubernetes/leader/LeadershipControllerTest.java index 6c94e3f6..68eb61c8 100644 --- a/spring-cloud-kubernetes-leader/src/test/java/org/springframework/cloud/kubernetes/leader/LeadershipControllerTest.java +++ b/spring-cloud-kubernetes-leader/src/test/java/org/springframework/cloud/kubernetes/leader/LeadershipControllerTest.java @@ -17,11 +17,11 @@ package org.springframework.cloud.kubernetes.leader; import io.fabric8.kubernetes.client.KubernetesClient; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; -import org.mockito.junit.MockitoJUnitRunner; +import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.integration.leader.Candidate; import org.springframework.integration.leader.event.LeaderEventPublisher; @@ -31,7 +31,7 @@ import static org.assertj.core.api.Assertions.assertThat; /** * @author Gytis Trikleris */ -@RunWith(MockitoJUnitRunner.class) +@ExtendWith(MockitoExtension.class) public class LeadershipControllerTest { @Mock @@ -48,7 +48,7 @@ public class LeadershipControllerTest { private LeadershipController leadershipController; - @Before + @BeforeEach public void before() { this.leadershipController = new LeadershipController(this.mockCandidate, this.mockLeaderProperties, this.mockLeaderEventPublisher, diff --git a/spring-cloud-kubernetes-leader/src/test/java/org/springframework/cloud/kubernetes/leader/PodReadinessWatcherTest.java b/spring-cloud-kubernetes-leader/src/test/java/org/springframework/cloud/kubernetes/leader/PodReadinessWatcherTest.java index 3b2d26c7..4e8b308d 100644 --- a/spring-cloud-kubernetes-leader/src/test/java/org/springframework/cloud/kubernetes/leader/PodReadinessWatcherTest.java +++ b/spring-cloud-kubernetes-leader/src/test/java/org/springframework/cloud/kubernetes/leader/PodReadinessWatcherTest.java @@ -26,11 +26,11 @@ import io.fabric8.kubernetes.client.Watch; import io.fabric8.kubernetes.client.Watcher; import io.fabric8.kubernetes.client.dsl.MixedOperation; import io.fabric8.kubernetes.client.dsl.PodResource; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; -import org.mockito.junit.MockitoJUnitRunner; +import org.mockito.junit.jupiter.MockitoExtension; import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.times; @@ -39,7 +39,7 @@ import static org.mockito.Mockito.verify; /** * @author Gytis Trikleris */ -@RunWith(MockitoJUnitRunner.class) +@ExtendWith(MockitoExtension.class) public class PodReadinessWatcherTest { private static final String POD_NAME = "test-pod"; @@ -70,18 +70,15 @@ public class PodReadinessWatcherTest { private PodReadinessWatcher watcher; - @Before + @BeforeEach public void before() { this.watcher = new PodReadinessWatcher(POD_NAME, this.mockKubernetesClient, this.mockLeadershipController); - - given(this.mockKubernetesClient.pods()).willReturn(this.mockPodsOperation); - given(this.mockPodsOperation.withName(POD_NAME)).willReturn(this.mockPodResource); - given(this.mockPodResource.watch(this.watcher)).willReturn(this.mockWatch); } @Test public void shouldStartOnce() { + initStubs(); this.watcher.start(); this.watcher.start(); @@ -90,6 +87,7 @@ public class PodReadinessWatcherTest { @Test public void shouldStopOnce() { + initStubs(); this.watcher.start(); this.watcher.stop(); this.watcher.stop(); @@ -99,6 +97,7 @@ public class PodReadinessWatcherTest { @Test public void shouldHandleEventWithStateChange() { + initStubs(); given(this.mockPodResource.isReady()).willReturn(true); given(this.mockPod.getStatus()).willReturn(this.mockPodStatus); @@ -110,6 +109,7 @@ public class PodReadinessWatcherTest { @Test public void shouldIgnoreEventIfStateDoesNotChange() { + initStubs(); given(this.mockPod.getStatus()).willReturn(this.mockPodStatus); this.watcher.start(); @@ -120,6 +120,7 @@ public class PodReadinessWatcherTest { @Test public void shouldHandleClose() { + initStubs(); this.watcher.onClose(this.mockKubernetesClientException); verify(this.mockPodResource).watch(this.watcher); @@ -132,4 +133,10 @@ public class PodReadinessWatcherTest { verify(this.mockPodResource, times(0)).watch(this.watcher); } + private void initStubs() { + given(this.mockKubernetesClient.pods()).willReturn(this.mockPodsOperation); + given(this.mockPodsOperation.withName(POD_NAME)).willReturn(this.mockPodResource); + given(this.mockPodResource.watch(this.watcher)).willReturn(this.mockWatch); + } + } diff --git a/src/checkstyle/checkstyle-suppressions.xml b/src/checkstyle/checkstyle-suppressions.xml index c4698b11..b7714c41 100644 --- a/src/checkstyle/checkstyle-suppressions.xml +++ b/src/checkstyle/checkstyle-suppressions.xml @@ -12,4 +12,5 @@ +