Enhanced Ribbon can support access to workloads through services (#388)

* Enhanced Ribbon can support access to workloads through services while being compatible with Istio traffic policies

* update after checkstyle

* update

* add cluster_domian config

* add tests for service mode

* update doc for new feature and refactoring code structure
This commit is contained in:
wuzishu
2019-06-18 22:58:38 +08:00
committed by Ryan Baxter
parent 81f827d610
commit f0eab0e0b8
10 changed files with 558 additions and 56 deletions

View File

@@ -652,6 +652,34 @@ the `PortName` key. If you want to specify in which Kubernetes namespace the tar
the `KubernetesNamespace` key, remembering in both instances to prefix these keys with your service name and
`ribbon` prefix, as specified earlier.
.Spring Cloud Kubernetes Ribbon Configuration
|===
|Property Key |Type |Default Value
|spring.cloud.kubernetes.ribbon.enabled
|boolean
|true
|spring.cloud.kubernetes.ribbon.mode
|`KubernetesRibbonMode`
|POD
|spring.cloud.kubernetes.ribbon.cluster-domain
|string
|cluster.local
|===
* `spring.cloud.kubernetes.ribbon.mode` supports `POD` and `SERVICE` modes.
** The POD mode is to achieve load balancing by obtaining the Pod IP address of Kubernetes and using Ribbon.
POD mode uses the load balancing of the Ribbon Does not support Kubernetes load balancing, The traffic policy of `Istio` is not supported.
** the `SERVICE` mode is directly based on the `service name` of the Ribbon. Get
The Kubernetes service is concatenated into `service-name.{namespace}.svc.{cluster.domain}:{port}` such as: `demo1.default.svc.cluster.local:8080`.
the `SERVICE` mode uses load balancing of the Kubernetes service to support Istio's traffic policy.
* `spring.cloud.kubernetes.ribbon.cluster-domain` Set the custom Kubernetes cluster domain suffix. The default value is: 'cluster.local'
The following examples use this module for ribbon discovery:
* link:./spring-cloud-kubernetes-examples/kubernetes-circuitbreaker-ribbon-example[Spring Cloud Circuitbreaker and Ribbon]

View File

@@ -45,6 +45,32 @@ the `PortName` key. If you want to specify in which Kubernetes namespace the tar
the `KubernetesNamespace` key, remembering in both instances to prefix these keys with your service name and
`ribbon` prefix, as specified earlier.
.Spring Cloud Kubernetes Ribbon Configuration
|===
|Property Key |Type |Default Value
|spring.cloud.kubernetes.ribbon.enabled
|boolean
|true
|spring.cloud.kubernetes.ribbon.mode
|`KubernetesRibbonMode`
|POD
|spring.cloud.kubernetes.ribbon.cluster-domain
|string
|cluster.local
|===
* `spring.cloud.kubernetes.ribbon.mode` supports `POD` and `SERVICE` modes.
** The POD mode is to achieve load balancing by obtaining the Pod IP address of Kubernetes and using Ribbon.
POD mode uses the load balancing of the Ribbon Does not support Kubernetes load balancing, The traffic policy of `Istio` is not supported.
** the `SERVICE` mode is directly based on the `service name` of the Ribbon. Get
The Kubernetes service is concatenated into `service-name.{namespace}.svc.{cluster.domain}:{port}` such as: `demo1.default.svc.cluster.local:8080`.
the `SERVICE` mode uses load balancing of the Kubernetes service to support Istio's traffic policy.
* `spring.cloud.kubernetes.ribbon.cluster-domain` Set the custom Kubernetes cluster domain suffix. The default value is: 'cluster.local'
The following examples use this module for ribbon discovery:
* link:./spring-cloud-kubernetes-examples/kubernetes-circuitbreaker-ribbon-example[Spring Cloud Circuitbreaker and Ribbon]

View File

@@ -99,6 +99,11 @@
<version>${groovy.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
<build>

View File

@@ -0,0 +1,95 @@
/*
* 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.ribbon;
import java.util.ArrayList;
import java.util.List;
import com.netflix.loadbalancer.Server;
import io.fabric8.kubernetes.api.model.EndpointAddress;
import io.fabric8.kubernetes.api.model.EndpointPort;
import io.fabric8.kubernetes.api.model.EndpointSubset;
import io.fabric8.kubernetes.api.model.Endpoints;
import io.fabric8.kubernetes.client.KubernetesClient;
import io.fabric8.kubernetes.client.utils.Utils;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
/**
* the KubernetesEndpointsServerList description.
* @author wuzishu
*/
public class KubernetesEndpointsServerList extends KubernetesServerList {
private static final Log LOG = LogFactory.getLog(KubernetesEndpointsServerList.class);
/**
* Instantiates a new Kubernetes endpoints server list.
*
* @param client the client
* @param properties the properties
*/
KubernetesEndpointsServerList(KubernetesClient client, KubernetesRibbonProperties properties) {
super(client, properties);
}
@Override
public List<Server> getUpdatedListOfServers() {
List<Server> result = new ArrayList<>();
Endpoints endpoints = StringUtils.isNotBlank(this.getNamespace())
? this.getClient().endpoints().inNamespace(this.getNamespace())
.withName(this.getServiceId()).get()
: this.getClient().endpoints().withName(this.getServiceId()).get();
if (endpoints != null) {
if (LOG.isDebugEnabled()) {
LOG.debug(String.format(
"Found [%d] endpoints in l [%s] for name [%s] and portName [%s]",
endpoints.getSubsets().size(),
endpoints.getMetadata().getNamespace(), this.getServiceId(),
this.getPortName()));
}
for (EndpointSubset subset : endpoints.getSubsets()) {
if (subset.getPorts().size() == 1) {
EndpointPort port = subset.getPorts().get(getFIRST());
for (EndpointAddress address : subset.getAddresses()) {
result.add(new Server(address.getIp(), port.getPort()));
}
}
else {
for (EndpointPort port : subset.getPorts()) {
if (Utils.isNullOrEmpty(this.getPortName())
|| this.getPortName().endsWith(port.getName())) {
for (EndpointAddress address : subset.getAddresses()) {
result.add(
new Server(address.getIp(), port.getPort()));
}
}
}
}
}
}
if (result.isEmpty()) {
LOG.warn(String.format(
"Did not find any endpoints in ribbon in namespace [%s] for name [%s] and portName [%s]",
this.getNamespace(), this.getServiceId(), this.getPortName()));
}
return result;
}
}

View File

@@ -21,6 +21,7 @@ import com.netflix.loadbalancer.ServerList;
import io.fabric8.kubernetes.client.KubernetesClient;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@@ -30,6 +31,7 @@ import org.springframework.context.annotation.Configuration;
* @author Ioannis Canellos
*/
@Configuration
@EnableConfigurationProperties(KubernetesRibbonProperties.class)
public class KubernetesRibbonClientConfiguration {
public KubernetesRibbonClientConfiguration() {
@@ -37,8 +39,15 @@ public class KubernetesRibbonClientConfiguration {
@Bean
@ConditionalOnMissingBean
public ServerList<?> ribbonServerList(KubernetesClient client, IClientConfig config) {
KubernetesServerList serverList = new KubernetesServerList(client);
public ServerList<?> ribbonServerList(KubernetesClient client, IClientConfig config,
KubernetesRibbonProperties properties) {
KubernetesServerList serverList;
if (properties.getMode() == KubernetesRibbonMode.SERVICE) {
serverList = new KubernetesServicesServerList(client, properties);
}
else {
serverList = new KubernetesEndpointsServerList(client, properties);
}
serverList.initWithNiwsConfig(config);
return serverList;
}

View File

@@ -0,0 +1,35 @@
/*
* 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.ribbon;
/**
* the KubernetesRibbonMode description.
*
* @author wuzishu
*/
public enum KubernetesRibbonMode {
/**
* using pod ip and port.
*/
POD,
/**
* using kubernetes service name and port.
*/
SERVICE
}

View File

@@ -0,0 +1,91 @@
/*
* 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.ribbon;
import org.springframework.boot.context.properties.ConfigurationProperties;
/**
* The type Kubernetes ribbon properties.
*/
@ConfigurationProperties(prefix = "spring.cloud.kubernetes.ribbon")
public class KubernetesRibbonProperties {
/**
* Ribbon enabled,default true.
*/
private Boolean enabled = true;
/**
* {@link KubernetesRibbonMode} setting ribbon server list with ip of pod or service
* name. default value is POD.
*/
private KubernetesRibbonMode mode = KubernetesRibbonMode.POD;
/**
* cluster domain.
*/
private String clusterDomain = "cluster.local";
/**
* Get cluster domain.
* @return the cluster domain
*/
public String getClusterDomain() {
return clusterDomain;
}
/**
* Sets cluster domain.
* @param clusterDomain the cluster domain
*/
public void setClusterDomain(String clusterDomain) {
this.clusterDomain = clusterDomain;
}
/**
* Gets mode.
* @return the mode
*/
public KubernetesRibbonMode getMode() {
return mode;
}
/**
* Sets mode.
* @param mode the mode
*/
public void setMode(KubernetesRibbonMode mode) {
this.mode = mode;
}
/**
* Gets enabled.
* @return the enabled
*/
public Boolean getEnabled() {
return enabled;
}
/**
* Sets enabled.
* @param enabled the enabled
*/
public void setEnabled(Boolean enabled) {
this.enabled = enabled;
}
}

View File

@@ -16,7 +16,6 @@
package org.springframework.cloud.kubernetes.ribbon;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
@@ -24,27 +23,19 @@ import com.netflix.client.config.IClientConfig;
import com.netflix.loadbalancer.AbstractServerList;
import com.netflix.loadbalancer.Server;
import com.netflix.loadbalancer.ServerList;
import io.fabric8.kubernetes.api.model.EndpointAddress;
import io.fabric8.kubernetes.api.model.EndpointPort;
import io.fabric8.kubernetes.api.model.EndpointSubset;
import io.fabric8.kubernetes.api.model.Endpoints;
import io.fabric8.kubernetes.client.KubernetesClient;
import io.fabric8.kubernetes.client.utils.Utils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
/**
* Kubernetes {@link ServerList}.
*
* @author Ioannis Canellos
* @author wuzishu
*/
public class KubernetesServerList extends AbstractServerList<Server>
implements ServerList<Server> {
public abstract class KubernetesServerList extends AbstractServerList<Server>
implements ServerList<Server> {
private static final int FIRST = 0;
private static final Log LOG = LogFactory.getLog(KubernetesServerList.class);
private final KubernetesClient client;
private String serviceId;
@@ -53,62 +44,84 @@ public class KubernetesServerList extends AbstractServerList<Server>
private String portName;
public KubernetesServerList(KubernetesClient client) {
private KubernetesRibbonProperties properties;
/**
* Instantiates a new Kubernetes server list.
*
* @param client the client
* @param properties the properties
*/
public KubernetesServerList(KubernetesClient client,
KubernetesRibbonProperties properties) {
this.client = client;
this.properties = properties;
}
public void initWithNiwsConfig(IClientConfig clientConfig) {
this.serviceId = clientConfig.getClientName();
this.namespace = clientConfig.getPropertyAsString(KubernetesConfigKey.Namespace,
this.client.getNamespace());
this.client.getNamespace());
this.portName = clientConfig.getPropertyAsString(KubernetesConfigKey.PortName,
null);
null);
}
public List<Server> getInitialListOfServers() {
return Collections.emptyList();
}
public List<Server> getUpdatedListOfServers() {
Endpoints endpoints = this.namespace != null
? this.client.endpoints().inNamespace(this.namespace)
.withName(this.serviceId).get()
: this.client.endpoints().withName(this.serviceId).get();
List<Server> result = new ArrayList<Server>();
if (endpoints != null) {
if (LOG.isDebugEnabled()) {
LOG.debug("Found [" + endpoints.getSubsets().size()
+ "] endpoints in namespace [" + this.namespace + "] for name ["
+ this.serviceId + "] and portName [" + this.portName + "]");
}
for (EndpointSubset subset : endpoints.getSubsets()) {
if (subset.getPorts().size() == 1) {
EndpointPort port = subset.getPorts().get(FIRST);
for (EndpointAddress address : subset.getAddresses()) {
result.add(new Server(address.getIp(), port.getPort()));
}
}
else {
for (EndpointPort port : subset.getPorts()) {
if (Utils.isNullOrEmpty(this.portName)
|| this.portName.endsWith(port.getName())) {
for (EndpointAddress address : subset.getAddresses()) {
result.add(new Server(address.getIp(), port.getPort()));
}
}
}
}
}
}
else {
LOG.warn("Did not find any endpoints in ribbon in namespace ["
+ this.namespace + "] for name [" + this.serviceId
+ "] and portName [" + this.portName + "]");
}
return result;
/**
* Gets first.
*
* @return the first
*/
static int getFIRST() {
return FIRST;
}
/**
* Gets client.
*
* @return the client
*/
KubernetesClient getClient() {
return client;
}
/**
* Gets service id.
*
* @return the service id
*/
String getServiceId() {
return serviceId;
}
/**
* Gets namespace.
*
* @return the namespace
*/
String getNamespace() {
return namespace;
}
/**
* Gets port name.
*
* @return the port name
*/
String getPortName() {
return portName;
}
/**
* Gets properties.
*
* @return the properties
*/
KubernetesRibbonProperties getProperties() {
return properties;
}
}

View File

@@ -0,0 +1,92 @@
/*
* 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.ribbon;
import java.util.ArrayList;
import java.util.List;
import com.netflix.loadbalancer.Server;
import io.fabric8.kubernetes.api.model.Service;
import io.fabric8.kubernetes.api.model.ServicePort;
import io.fabric8.kubernetes.client.KubernetesClient;
import io.fabric8.kubernetes.client.utils.Utils;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
/**
* the KubernetesServicesServerList description.
* @author wuzishu
*/
public class KubernetesServicesServerList extends KubernetesServerList {
private static final Log LOG = LogFactory.getLog(KubernetesServicesServerList.class);
/**
* Instantiates a new Kubernetes services server list.
*
* @param client the client
* @param properties the properties
*/
KubernetesServicesServerList(KubernetesClient client, KubernetesRibbonProperties properties) {
super(client, properties);
}
/**
* Concat service fully qualified domain name.
* @param service Service model
* @return service FQDN
*/
private String concatServiceFQDN(Service service) {
return String.format("%s.%s.svc.%s", service.getMetadata().getName(),
StringUtils.isNotBlank(service.getMetadata().getNamespace()) ? service.getMetadata()
.getNamespace() : "default", this.getProperties().getClusterDomain());
}
@Override
public List<Server> getUpdatedListOfServers() {
List<Server> result = new ArrayList<>();
Service service = StringUtils.isNotBlank(this.getNamespace())
? this.getClient().services().inNamespace(this.getNamespace())
.withName(this.getServiceId()).get()
: this.getClient().services().withName(this.getServiceId()).get();
if (service != null) {
if (LOG.isDebugEnabled()) {
LOG.debug("Found Service[" + service.getMetadata().getName() + "]");
}
if (service.getSpec().getPorts().size() == 1) {
result.add(new Server(this.concatServiceFQDN(service),
service.getSpec().getPorts().get(0).getPort()));
}
else {
for (ServicePort servicePort : service.getSpec().getPorts()) {
if (Utils.isNotNullOrEmpty(this.getPortName())
|| this.getPortName().endsWith(servicePort.getName())) {
result.add(new Server(concatServiceFQDN(service),
servicePort.getPort()));
}
}
}
}
if (result.isEmpty()) {
LOG.warn(String.format(
"Did not find any service in ribbon in namespace [%s] for name [%s] and portName [%s]",
this.getNamespace(), this.getServiceId(), this.getPortName()));
}
return result;
}
}

View File

@@ -0,0 +1,108 @@
/*
* 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.ribbon;
import java.util.List;
import com.netflix.loadbalancer.ILoadBalancer;
import com.netflix.loadbalancer.Server;
import io.fabric8.kubernetes.api.model.IntOrString;
import io.fabric8.kubernetes.api.model.ServiceBuilder;
import io.fabric8.kubernetes.client.Config;
import io.fabric8.kubernetes.client.KubernetesClient;
import io.fabric8.kubernetes.client.server.mock.KubernetesServer;
import org.junit.BeforeClass;
import org.junit.ClassRule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.netflix.ribbon.SpringClientFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.web.client.RestTemplate;
import static org.assertj.core.api.Assertions.assertThat;
/**
* the RibbonWithServiceModeTest description.
* @author wuzishu
*/
@RunWith(SpringRunner.class)
@SpringBootTest(classes = TestApplication.class, properties = {
"spring.application.name=testapp",
"spring.cloud.kubernetes.client.namespace=testns",
"spring.cloud.kubernetes.client.trustCerts=true",
"spring.cloud.kubernetes.config.namespace=testns",
"spring.cloud.kubernetes.enabled=true",
"spring.cloud.kubernetes.discovery.enabled=true",
"spring.cloud.kubernetes.ribbon.enabled=true",
"spring.cloud.kubernetes.ribbon.mode=SERVICE",
"spring.cloud.kubernetes.ribbon.clusterDomain=test.com"})
@EnableAutoConfiguration
@EnableDiscoveryClient
public class RibbonWithServiceModeTest {
@ClassRule
public static KubernetesServer server = new KubernetesServer();
@ClassRule
public static KubernetesServer mockEndpointA = new KubernetesServer(false);
private static KubernetesClient mockClient;
@Autowired
private RestTemplate restTemplate;
@BeforeClass
public static void setUpBefore() {
mockClient = server.getClient();
// Configure the kubernetes master url to point to the mock server
System.setProperty(Config.KUBERNETES_MASTER_SYSTEM_PROPERTY,
mockClient.getConfiguration().getMasterUrl());
System.setProperty(Config.KUBERNETES_TRUST_CERT_SYSTEM_PROPERTY, "true");
System.setProperty(Config.KUBERNETES_AUTH_TRYKUBECONFIG_SYSTEM_PROPERTY, "false");
System.setProperty(Config.KUBERNETES_AUTH_TRYSERVICEACCOUNT_SYSTEM_PROPERTY,
"false");
// Configured
server.expect().get().withPath("/api/v1/namespaces/testns/services/testapp").andReturn(200,
new ServiceBuilder().withNewMetadata().withName("testapp").withNamespace("testns")
.endMetadata().withNewSpec().addToSelector("app", "testapp-a")
.addNewPort().withName("http")
.withPort(mockEndpointA.getMockServer().getPort())
.withTargetPort(new IntOrString(mockEndpointA.getMockServer().getPort()))
.withProtocol("TCP").endPort().endSpec().build())
.always();
}
@Autowired
private ApplicationContext context;
@Test
public void testGreetingWithServiceMode() {
SpringClientFactory springClientFactory = context.getBean(SpringClientFactory.class);
ILoadBalancer testapp = springClientFactory.getLoadBalancer("testapp");
List<Server> allServers = testapp.getAllServers();
assertThat(allServers.stream().map(c -> String.format("%s:%s", c.getHost(), c.getPort())))
.containsOnly("testapp.testns.svc.test.com:" + mockEndpointA.getMockServer().getPort());
}
}