Merge remote-tracking branch 'upstream/master' into load-balancer

This commit is contained in:
piomin
2020-08-24 13:34:54 +02:00
68 changed files with 3710 additions and 223 deletions

View File

@@ -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: |

View File

@@ -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 <<spring-cloud-kubernetes-configuration-watcher>> 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

View File

@@ -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 <<spring-cloud-kubernetes-configuration-watcher>> 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.

View File

@@ -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
----
====

View File

@@ -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[]

23
pom.xml
View File

@@ -65,12 +65,14 @@
<!-- Dependency Versions -->
<spring-cloud-commons.version>3.0.0-SNAPSHOT</spring-cloud-commons.version>
<spring-cloud-config.version>3.0.0-SNAPSHOT</spring-cloud-config.version>
<spring-cloud-bus.version>3.0.0-SNAPSHOT</spring-cloud-bus.version>
<spring-cloud-contract.version>3.0.0-SNAPSHOT</spring-cloud-contract.version>
<!-- Maven Plugin Versions -->
<maven-compiler-plugin.version>3.5</maven-compiler-plugin.version>
<maven-deploy-plugin.version>2.8.2</maven-deploy-plugin.version>
<maven-failsafe-plugin.version>2.18.1</maven-failsafe-plugin.version>
<maven-surefire-plugin.version>2.21.0</maven-surefire-plugin.version>
<maven-failsafe-plugin.version>2.22.2</maven-failsafe-plugin.version>
<maven-surefire-plugin.version>2.22.2</maven-surefire-plugin.version>
<fabric8.maven.plugin.version>4.4.0</fabric8.maven.plugin.version>
<groovy.version>2.4.12</groovy.version>
<restassured.version>3.0.2</restassured.version>
@@ -94,6 +96,7 @@
<module>spring-cloud-kubernetes-examples</module>
<module>spring-cloud-kubernetes-leader</module>
<module>spring-cloud-kubernetes-istio</module>
<module>spring-cloud-kubernetes-controllers</module>
<module>spring-cloud-kubernetes-integration-tests</module>
<module>docs</module>
<module>spring-cloud-kubernetes-loadbalancer</module>
@@ -127,6 +130,22 @@
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-bus-dependencies</artifactId>
<version>${spring-cloud-bus.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-contract-dependencies</artifactId>
<version>${spring-cloud-contract.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.codehaus.groovy</groupId>
<artifactId>groovy-all</artifactId>

View File

@@ -54,7 +54,12 @@
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-context</artifactId>
<artifactId>spring-cloud-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bootstrap</artifactId>
</dependency>
<dependency>

View File

@@ -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) {
}
}
}
}

View File

@@ -99,8 +99,9 @@ public abstract class ConfigurationChangeDetector {
List<? extends MapPropertySource> 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;
}

View File

@@ -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<Secret>() {
@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),

View File

@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>spring-cloud-kubernetes</artifactId>
<groupId>org.springframework.cloud</groupId>
<version>2.0.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<packaging>pom</packaging>
<artifactId>spring-cloud-kubernetes-controllers</artifactId>
<modules>
<module>spring-cloud-kubernetes-configuration-watcher</module>
</modules>
</project>

View File

@@ -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

View File

@@ -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

View File

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

View File

@@ -0,0 +1,20 @@
apiVersion: skaffold/v2alpha3
kind: Config
metadata:
name: spring-cloud-kubernetes-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

View File

@@ -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<Void> triggerRefresh(Secret secret) {
this.applicationEventPublisher.publishEvent(new RefreshRemoteApplicationEvent(
secret, busProperties.getId(), secret.getMetadata().getName()));
return Mono.empty();
}
@Override
protected Mono<Void> 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;
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}
}

View File

@@ -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<Void> triggerRefresh(Secret secret);
protected abstract Mono<Void> 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());
}
}
}
}

View File

@@ -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;
}
}

View File

@@ -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<Void> 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 :<port> 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<ResponseEntity<Void>> 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<ResponseEntity<Void>> 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<Void> triggerRefresh(ConfigMap configMap) {
return refresh(configMap.getMetadata()).then();
}
}

View File

@@ -0,0 +1,2 @@
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.cloud.kubernetes.configuration.watcher.ConfigurationWatcherAutoConfiguration

View File

@@ -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

View File

@@ -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<RefreshRemoteApplicationEvent> 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<RefreshRemoteApplicationEvent> 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:**");
}
}

View File

@@ -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");
}
}

View File

@@ -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<ServiceInstance> 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<String, String> 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<ServiceInstance> 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<String, String> 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<ServiceInstance> 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")));
}
}

View File

@@ -34,10 +34,10 @@
<properties>
<arquillian.version>1.4.0.Final</arquillian.version>
<arquillian-cube.version>1.15.2</arquillian-cube.version>
<kubernetes-client.version>4.9.0</kubernetes-client.version>
<kubernetes-client.version>4.10.3</kubernetes-client.version>
<istio-client.version>1.1.1</istio-client.version>
<mockwebserver.version>0.1.2</mockwebserver.version>
<okhttp.version>3.12.0</okhttp.version>
<okhttp.version>3.14.4</okhttp.version>
</properties>
<dependencyManagement>
<dependencies>

View File

@@ -71,8 +71,10 @@ public class KubernetesCatalogWatch implements ApplicationEventPublisherAware {
// not all pods participate in the service discovery. only those that have
// endpoints.
List<Endpoints> 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<String> endpointsPodNames = endpoints.stream().map(Endpoints::getSubsets)
.filter(Objects::nonNull).flatMap(Collection::stream)
.map(EndpointSubset::getAddresses).filter(Objects::nonNull)

View File

@@ -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<Endpoints> endpointsList = this.properties.isAllNamespaces()
? this.client.endpoints().inAnyNamespace()
.withField("metadata.name", serviceId).list().getItems()
: Collections
.singletonList(this.client.endpoints().withName(serviceId).get());
List<EndpointSubsetNS> subsetsNS = endpointsList.stream()
List<EndpointSubsetNS> 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<Endpoints> 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<ServiceInstance> getNamespaceServiceInstances(EndpointSubsetNS es,
String serviceId) {
String namespace = es.getNamespace();

View File

@@ -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<String> 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

View File

@@ -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<Endpoints, DoneableEndpoints> endpointsResource;
@Mock
FilterWatchListDeletable<Endpoints, EndpointsList, Boolean, Watch, Watcher<Endpoints>> 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<ServicePort> getServicePorts(Map<Integer, String> ports) {

View File

@@ -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<String, String> 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<Endpoints> 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<String, String>() {
{
put("l", "v");
}
}).endMetadata().build();
Service service2 = new ServiceBuilder().withNewMetadata().withName("endpoint")
.withNamespace("test2").withLabels(new HashMap<String, String>() {
{
put("l", "v");
}
}).endMetadata().build();
List<Service> 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<ServiceInstance> 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<String, String>() {
{
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<String, String> 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<Endpoints> 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<String, String>() {
{
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<String, String> 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<String, String>() {
{
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<Endpoints> 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<Endpoints> result_endpoints = discoveryClient
.getEndPointsList("endpoint");
assertThat(result_endpoints).hasSize(1);
}
@Test
public void getInstancesShouldBeAbleToHandleEndpointsMultipleAddresses() {
Map<String, String> 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<Endpoints> 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<Endpoints> 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<String, String>() {
{
put("l", "v");
}
}).endMetadata().build();
Service service2 = new ServiceBuilder().withNewMetadata().withName("endpoint")
.withNamespace("test2").withLabels(new HashMap<String, String>() {
{
put("l", "v");
}
}).endMetadata().build();
List<Service> 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<ServiceInstance> 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);
}
}

View File

@@ -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));

View File

@@ -83,6 +83,14 @@
<skip>true</skip>
</configuration>
</plugin>
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.22.2</version>
</plugin>
<plugin>
<artifactId>maven-failsafe-plugin</artifactId>
<version>2.22.2</version>
</plugin>
</plugins>
</build>

View File

@@ -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();

View File

@@ -134,6 +134,8 @@
<!-- <module>istio</module>-->
<module>discovery</module>
<module>load-balancer</module>
<!-- Add this module back once we convert everything over to kind -->
<!-- <module>spring-cloud-kubernetes-configuration-watcher-it</module>-->
</modules>

View File

@@ -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: {}

View File

@@ -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

View File

@@ -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"]

View File

@@ -0,0 +1,106 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-kubernetes-integration-tests</artifactId>
<version>2.0.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>spring-cloud-kubernetes-configuration-watcher-it</artifactId>
<packaging>jar</packaging>
<properties>
<wiremock.version>2.26.3</wiremock.version>
<kubernetes-client.version>6.0.1</kubernetes-client.version>
<docker-java.version>3.2.2</docker-java.version>
<awaitility.version>4.0.3</awaitility.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bus-amqp</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.github.tomakehurst</groupId>
<artifactId>wiremock-jre8</artifactId>
<version>${wiremock.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.kubernetes</groupId>
<artifactId>client-java</artifactId>
<version>${kubernetes-client.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.kubernetes</groupId>
<artifactId>client-java-extended</artifactId>
<version>${kubernetes-client.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.github.docker-java</groupId>
<artifactId>docker-java-core</artifactId>
<version>${docker-java.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.github.docker-java</groupId>
<artifactId>docker-java-transport-httpclient5</artifactId>
<version>${docker-java.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.awaitility</groupId>
<artifactId>awaitility</artifactId>
<version>${awaitility.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<image>
<name>springcloud/${project.artifactId}:${project.version}</name>
</image>
<goal>build-image</goal>
</configuration>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>build-image</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

View File

@@ -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 <<EOF | "${KIND}" create cluster --config=-
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
containerdConfigPatches:
- |-
[plugins."io.containerd.grpc.v1.cri".registry.mirrors."localhost:${reg_port}"]
endpoint = ["http://${reg_name}:${reg_port}"]
EOF
# 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
}
main() {
# get kind
install_latest_kind
# create a cluster
cd $CURRENT_DIR
# 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
#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

View File

@@ -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<RefreshRemoteApplicationEvent> {
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;
}
}

View File

@@ -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

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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<String, String> labels,
Map<String, String> 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<String, String> selectorMatchLabels,
Map<String, String> templateMetadataLabels, String containerName,
String image, String pullPolicy, int containerPort, int readinessProbePort,
String readinessProbePath, int livenessProbePort, String livenessProbePath,
String serviceAccountName, Collection<V1EnvVar> 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;
}
}

View File

@@ -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=

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -61,15 +61,21 @@
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>${maven-surefire-plugin.version}</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-failsafe-plugin</artifactId>

View File

@@ -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" })

View File

@@ -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);

View File

@@ -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);

View File

@@ -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();
}

View File

@@ -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);
}
}

View File

@@ -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);
}

View File

@@ -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,

View File

@@ -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);
}
}

View File

@@ -12,4 +12,5 @@
<suppress files=".*KubernetesDiscoveryClientFilterTest.*" checks="LineLength*"/>
<suppress files=".*PollingConfigurationChangeDetector.*" checks="LineLength*"/>
<suppress files=".*LeaderRecordWatcherTest.*" checks="LineLength*"/>
<suppress files=".*ConfigurationWatcherApplication\.java" checks="HideUtilityClassConstructor"/>
</suppressions>