#233 Initial Istio Awareness implementation (#286)

* initial profile and check

* initial istio aware profile impl

* adding initial docs

* fixing parent

* using BootstrapConfiguration for istio profile check

* Improve log messages

* Add Istio integration tests

* Remove unused IstioClient

* Remove redundant parent

* Revert "Remove redundant parent"

This reverts commit c41271fa

* Fix startup error

* Add name to istio module

* Move IstioClient out of bootstrap configuration

This is needed because the Kubernetes Config is not a bean in the
bootstrap context.
Furthermore the IstioClient is not used anywhere in the bootstrap
context

* Polish

* Ensure this IstioClient is actually injected

* Fix integration test setup

* more refinements

* aligning property name

* adding headers and small refinements
This commit is contained in:
salaboy
2018-12-20 17:16:59 +00:00
committed by Ryan Baxter
parent b3de20b215
commit ef51d11f33
20 changed files with 504 additions and 5 deletions

View File

@@ -72,7 +72,7 @@ jobs:
# Allow intra-pod communication
sudo iptables -P FORWARD ACCEPT
/snap/bin/microk8s.enable dns registry
echo n |/snap/bin/microk8s.enable dns registry istio
# wait until the registry is up and running
sleep 10
@@ -85,6 +85,21 @@ jobs:
done
echo "Kubernetes Container Registry enabled"
# wait until istio is up and running
sleep 10
n=0
until [ $n -ge 10 ]
do
(/snap/bin/microk8s.kubectl get pod -l istio=sidecar-injector --namespace=istio-system | grep -z "Running") && break
n=$[$n+1]
sleep 10
done
echo "Istio enabled"
# create the namespace where the istio integration test will run
/snap/bin/microk8s.kubectl create -f .circleci/istio-test-namespace.yml
- run:
name: Run integration tests
command: |

View File

@@ -0,0 +1,6 @@
apiVersion: v1
kind: Namespace
metadata:
name: istio-test
labels:
istio-injection: enabled

View File

@@ -1,11 +1,11 @@
== Kubernetes Awareness
== Kubernetes Ecosystem Awareness
All of the features described above will work equally well regardless of whether your application is running inside
Kubernetes or not. This is really helpful for development and troubleshooting.
From a development point of view, this is really helpful as you can start your Spring Boot application and debug one
of the modules part of this project. It is not required to deploy it in Kubernetes
as the code of the project relies on the
[Fabric8 Kubernetes Java client](https://github.com/fabric8io/kubernetes-client) which is a fluent DSL able to
https://github.com/fabric8io/kubernetes-client[Fabric8 Kubernetes Java client] which is a fluent DSL able to
communicate using `http` protocol to the REST API of Kubernetes Server.
=== Kubernetes Profile Autoconfiguration
@@ -13,3 +13,12 @@ communicate using `http` protocol to the REST API of Kubernetes Server.
When the application runs as a pod inside Kubernetes a Spring profile named `kubernetes` will automatically get activated.
This allows the developer to customize the configuration, to define beans that will be applied when the Spring Boot application is deployed
within the Kubernetes platform *(e.g. different dev and prod configuration)*.
=== Istio Awareness
When including the **spring-cloud-kubernetes-istio** module into the application classpath a new profile will be added to the application,
if the application is running inside a Kubernetes Cluster with http://istio.io[Istio] installed. Then you can use
spring **@Profile("istio")** annotations into your Beans and **@Configuration**'s.
The Istio awareness module uses the **me.snowdrop:istio-client** to interact with Istio APIs enabling us to discover traffic rules, circuit breakers, etc.
Making it easy for our Spring Boot applications to consume this data to dynamically configure themselves according the environment.

View File

@@ -88,13 +88,14 @@
<module>spring-cloud-starter-kubernetes-all</module>
<module>spring-cloud-kubernetes-examples</module>
<module>spring-cloud-kubernetes-leader</module>
<module>spring-cloud-kubernetes-istio</module>
<module>spring-cloud-kubernetes-integration-tests</module>
<module>docs</module>
</modules>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-kubernetes-dependencies</artifactId>

View File

@@ -34,6 +34,7 @@
<arquillian.version>1.4.0.Final</arquillian.version>
<arquillian-cube.version>1.15.2</arquillian-cube.version>
<kubernetes-client.version>4.1.0</kubernetes-client.version>
<istio-client.version>1.0.0</istio-client.version>
<mockwebserver.version>0.1.0</mockwebserver.version>
</properties>
<dependencyManagement>
@@ -46,6 +47,12 @@
<scope>import</scope>
</dependency>
<dependency>
<groupId>me.snowdrop</groupId>
<artifactId>istio-client</artifactId>
<version>${istio-client.version}</version>
</dependency>
<!-- Own dependencies -->
<dependency>
<groupId>org.springframework.cloud</groupId>
@@ -65,6 +72,11 @@
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-kubernetes-istio</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>

View File

@@ -27,7 +27,7 @@ tests against that microk8s cluster
```bash
sudo snap install microk8s --classic --channel=1.11/stable
sleep 10
microk8s.enable dns registry
echo n | microk8s.enable dns registry istio
```
Ensure everything is running by inspecting the output of:
@@ -66,6 +66,13 @@ Export the kube config file that will be used to access this cluster
microk8s.kubectl config view --raw > /tmp/kubeconfig
```
For the istio tests to work, we need a namespace that is properly configured for Istio injection.
Such a namespace called `istio-test` can easily be created using:
```bash
microk8s.kubectl create -f .circleci/istio-test-namespace.yml
```
## Launch tests

View File

@@ -0,0 +1,64 @@
<?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">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-kubernetes-integration-tests</artifactId>
<version>1.0.0.BUILD-SNAPSHOT</version>
</parent>
<name>Spring Cloud Kubernetes :: Integration Tests :: Istio</name>
<artifactId>istio</artifactId>
<packaging>jar</packaging>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-actuator-autoconfigure</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-kubernetes-istio</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.arquillian.cube</groupId>
<artifactId>arquillian-cube-kubernetes</artifactId>
<version>${arquillian-cube.version}</version>
<scope>test</scope>
<exclusions>
<exclusion>
<artifactId>undertow-core</artifactId>
<groupId>io.undertow</groupId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.jboss.arquillian.junit</groupId>
<artifactId>arquillian-junit-standalone</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.rest-assured</groupId>
<artifactId>rest-assured</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,10 @@
# We need this fragment in order for the kubernetes client to talk to
# the Kubernetes API without caring about proper certificates
spec:
template:
spec:
containers:
- env:
- name: KUBERNETES_TRUST_CERTIFICATES
value: true

View File

@@ -0,0 +1,15 @@
# we are using an FMP fragment to ensure that NodePort is used correctly
kind: Service
apiVersion: v1
metadata:
name: ${project.artifactId}
labels:
app: ${project.artifactId}
spec:
selector:
app: ${project.artifactId}
ports:
- protocol: TCP
port: 8080
nodePort: ${nodeport.value}
type: NodePort

View File

@@ -0,0 +1,34 @@
package org.springframework.cloud.kubernetes.it;
import me.snowdrop.istio.client.IstioClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.core.env.Environment;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Arrays;
import java.util.List;
@SpringBootApplication
@RestController
public class IstioApplication {
@Autowired
private Environment environment;
// used just to ensure that the IstioClient is properly injected into the context
@Autowired
private IstioClient istioClient;
@GetMapping("/profiles")
public List<String> profiles() {
return Arrays.asList(environment.getActiveProfiles());
}
public static void main(String[] args) {
SpringApplication.run(IstioApplication.class, args);
}
}

View File

@@ -0,0 +1,5 @@
spring:
cloud:
istio:
client:
envoyPort: 15000

View File

@@ -0,0 +1,28 @@
package org.springframework.cloud.kubernetes.it;
import static io.restassured.RestAssured.given;
import org.arquillian.cube.kubernetes.impl.requirement.RequiresKubernetes;
import org.hamcrest.core.StringContains;
import org.jboss.arquillian.junit.Arquillian;
import org.junit.Test;
import org.junit.runner.RunWith;
@RequiresKubernetes
@RunWith(Arquillian.class)
public class ProfilesIT {
private static final String HOST = System.getProperty("service.host");
private static final Integer PORT = Integer.valueOf(System.getProperty("service.port"));
@Test
public void testProfileEndpoint() {
given()
.baseUri(String.format("http://%s:%d", HOST, PORT))
.get("profiles")
.then()
.statusCode(200)
.body(new StringContains("istio"));
}
}

View File

@@ -0,0 +1,9 @@
<arquillian xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://jboss.org/schema/arquillian"
xsi:schemaLocation="http://jboss.org/schema/arquillian http://jboss.org/schema/arquillian/arquillian_1_0.xsd">
<extension qualifier="kubernetes">
<property name="namespace.use.existing">istio-test</property>
</extension>
</arquillian>

View File

@@ -128,6 +128,7 @@
<modules>
<module>simple-core</module>
<module>simple-configmap</module>
<module>istio</module>
<module>discovery</module>
</modules>

View File

@@ -0,0 +1,41 @@
<?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>1.0.0.BUILD-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>spring-cloud-kubernetes-istio</artifactId>
<name>Spring Cloud Kubernetes :: Istio</name>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-kubernetes-core</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
</dependency>
<dependency>
<groupId>me.snowdrop</groupId>
<artifactId>istio-client</artifactId>
</dependency>
<!-- Testing Dependencies -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,36 @@
/*
* Copyright 2013-2018 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
*
* http://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.istio;
import io.fabric8.kubernetes.client.Config;
import me.snowdrop.istio.client.DefaultIstioClient;
import me.snowdrop.istio.client.IstioClient;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
@ConditionalOnProperty(value = "spring.cloud.istio.enabled", matchIfMissing = true)
public class IstioAutoConfiguration {
@Bean
@ConditionalOnMissingBean
public IstioClient istioClient(Config config) {
return new DefaultIstioClient(config);
}
}

View File

@@ -0,0 +1,90 @@
/*
* Copyright 2013-2018 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
*
* http://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.istio;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
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.istio.utils.MeshUtils;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.Environment;
import javax.annotation.PostConstruct;
import java.util.Arrays;
@Configuration
@ConditionalOnProperty(value = "spring.cloud.istio.enabled", matchIfMissing = true)
@EnableConfigurationProperties(IstioClientProperties.class)
public class IstioBootstrapConfiguration {
private static final Log LOG = LogFactory.getLog(IstioBootstrapConfiguration.class);
private static final String ISTIO_PROFILE = "istio";
@Bean
@ConditionalOnMissingBean
public MeshUtils istioMeshUtils(IstioClientProperties istioClientProperties) {
return new MeshUtils(istioClientProperties);
}
@EnableConfigurationProperties(IstioClientProperties.class)
protected static class IstioDetectionConfiguration {
private final MeshUtils utils;
private final ConfigurableEnvironment environment;
public IstioDetectionConfiguration(MeshUtils utils, ConfigurableEnvironment environment) {
this.utils = utils;
this.environment = environment;
}
@PostConstruct
public void detectIstio() {
addIstioProfile(environment);
}
void addIstioProfile(ConfigurableEnvironment environment) {
if (utils.isIstioEnabled()) {
if (hasIstioProfile(environment)) {
if (LOG.isDebugEnabled()) {
LOG.debug("'istio' already in list of active profiles");
}
} else {
if (LOG.isDebugEnabled()) {
LOG.debug("Adding 'istio' to list of active profiles");
}
environment.addActiveProfile(ISTIO_PROFILE);
}
} else {
if (LOG.isDebugEnabled()) {
LOG.debug("Not running inside kubernetes with istio enabled. Skipping 'istio' profile activation.");
}
}
}
private boolean hasIstioProfile(Environment environment) {
return Arrays.stream(environment.getActiveProfiles()).anyMatch(ISTIO_PROFILE::equalsIgnoreCase);
}
}
}

View File

@@ -0,0 +1,45 @@
/*
* Copyright 2013-2018 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
*
* http://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.istio;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
@Configuration
@ConfigurationProperties("spring.cloud.istio.client")
public class IstioClientProperties {
private Integer envoyPort = 15090;
private String testPath = "stats/prometheus";
public Integer getEnvoyPort() {
return envoyPort;
}
public void setEnvoyPort(Integer envoyPort) {
this.envoyPort = envoyPort;
}
public String getTestPath() {
return testPath;
}
public void setTestPath(String testPath) {
this.testPath = testPath;
}
}

View File

@@ -0,0 +1,66 @@
/*
* Copyright 2013-2018 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
*
* http://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.istio.utils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.cloud.kubernetes.istio.IstioClientProperties;
import org.springframework.http.ResponseEntity;
import org.springframework.web.client.RestTemplate;
public class MeshUtils {
private static final Log LOG = LogFactory.getLog(MeshUtils.class);
private final IstioClientProperties istioClientProperties;
private RestTemplate restTemplate = new RestTemplateBuilder().build();
public MeshUtils(IstioClientProperties istioClientProperties) {
this.istioClientProperties = istioClientProperties;
}
public Boolean isIstioEnabled() {
return checkIstioServices();
}
private synchronized boolean checkIstioServices() {
try {
//Check if Istio Envoy proxy is installed. Notice that the check is done to localhost.
// TODO: We can improve this initial detection if better methods are found.
String resource = "http://localhost:" + istioClientProperties.getEnvoyPort();
ResponseEntity<String> response = restTemplate.getForEntity(resource + "/" + istioClientProperties.getTestPath(), String.class);
if (response.getStatusCode().is2xxSuccessful()) {
LOG.info("Istio Resources Found.");
return true;
}
LOG.warn("Although Envoy proxy did respond at port" + istioClientProperties.getEnvoyPort() +
", it did not respond with HTTP 200 to path: " + istioClientProperties.getTestPath() +
". You may need to tweak the test path in order to get proper Istio support");
return false;
} catch (Throwable t) {
if (LOG.isDebugEnabled()) {
LOG.debug("Envoy proxy could not be located at port: " + istioClientProperties.getEnvoyPort() +
". Assuming that the application is not running inside the Istio Service Mesh");
}
return false;
}
}
}

View File

@@ -0,0 +1,5 @@
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.cloud.kubernetes.istio.IstioAutoConfiguration
org.springframework.cloud.bootstrap.BootstrapConfiguration=\
org.springframework.cloud.kubernetes.istio.IstioBootstrapConfiguration