diff --git a/.asf.yaml b/.asf.yaml index c7c008134..563124d16 100644 --- a/.asf.yaml +++ b/.asf.yaml @@ -26,3 +26,10 @@ github: features: # Enable issue management issues: true + enabled_merge_buttons: + # enable squash button: + squash: true + # enable merge button: + merge: false + # disable rebase button: + rebase: false diff --git a/README.md b/README.md index 7da9ec4bb..3037d586c 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,17 @@ [![codecov](https://codecov.io/gh/apache/dubbo-spi-extensions/branch/master/graph/badge.svg)](https://codecov.io/gh/apache/dubbo-spi-extensions) [![Maven Central](https://img.shields.io/maven-central/v/org.apache.dubbo/dubbo-spi-extensions.svg)](https://search.maven.org/search?q=g:org.apache.dubbo%20AND%20a:dubbo-spi-extensions) [![GitHub release](https://img.shields.io/github/release/apache/dubbo-spi-extensions.svg)] + +The purpose of dubbo-spi-extensions is to provide open, community-driven, reusable components to build microservice programs with different needs. These components extend the core of the Apache Dubbo project, but they are separated and decoupled. + +Developers can flexibly choose the required extension dependencies to develop microservice programs based on their needs. The available extensions are as follows:Developers can flexibly choose the required extension dependencies to develop microservice programs based on their needs. + +For version release notes, please refer to the documentation: +- [Release](https://cn.dubbo.apache.org/zh-cn/download/spi-extensions/) +- [Reference](https://cn.dubbo.apache.org/zh-cn/overview/mannual/java-sdk/reference-manual/spi/overview/) + +The available extensions are as follows: + - [dubbo-api-docs](dubbo-api-docs) - [dubbo-api-docs-annotations](dubbo-api-docs/dubbo-api-docs-annotations) - [dubbo-api-docs-core](dubbo-api-docs/dubbo-api-docs-core) @@ -32,6 +43,7 @@ - [dubbo-gateway-common](dubbo-gateway-extensions/dubbo-gateway-common) - [dubbo-gateway-consumer](dubbo-gateway-extensions/dubbo-gateway-consumer) - [dubbo-gateway-provider](dubbo-gateway-extensions/dubbo-gateway-provider) +- [dubbo-kubernetes](dubbo-kubernetes) - [dubbo-metadata-report-extensions](dubbo-metadata-report-extensions) - [dubbo-metadata-report-consul](dubbo-metadata-report-extensions/dubbo-metadata-report-consul) - [dubbo-metadata-report-etcd](dubbo-metadata-report-extensions/dubbo-metadata-report-etcd) @@ -66,7 +78,9 @@ - [dubbo-serialization-avro](dubbo-serialization-extensions/dubbo-serialization-avro) - [dubbo-serialization-fastjson](dubbo-serialization-extensions/dubbo-serialization-fastjson) - [dubbo-serialization-fst](dubbo-serialization-extensions/dubbo-serialization-fst) + - [dubbo-serialization-fury](dubbo-serialization-extensions/dubbo-serialization-fury) - [dubbo-serialization-gson](dubbo-serialization-extensions/dubbo-serialization-gson) + - [dubbo-serialization-jackson](dubbo-serialization-extensions/dubbo-serialization-jackson) - [dubbo-serialization-kryo](dubbo-serialization-extensions/dubbo-serialization-kryo) - [dubbo-serialization-msgpack](dubbo-serialization-extensions/dubbo-serialization-msgpack) - [dubbo-serialization-native-hession](dubbo-serialization-extensions/dubbo-serialization-native-hession) @@ -75,3 +89,16 @@ - [dubbo-serialization-test](dubbo-serialization-extensions/dubbo-serialization-test) - [dubbo-tag-extensions](dubbo-tag-extensions) - [dubbo-tag-subnets](dubbo-tag-extensions/dubbo-tag-subnets) +- [dubbo-xds](dubbo-xds) + +## Contribution + + +Thanks to everyone who has contributed! + + + + + + + diff --git a/README_CN.md b/README_CN.md new file mode 100644 index 000000000..e69de29bb diff --git a/dobbo-doc-auto-gen/src/main/java/org/apache/dubbo/doc/DocAutoGen.java b/dobbo-doc-auto-gen/src/main/java/org/apache/dubbo/doc/DocAutoGen.java index 9d4f715dc..878803f14 100644 --- a/dobbo-doc-auto-gen/src/main/java/org/apache/dubbo/doc/DocAutoGen.java +++ b/dobbo-doc-auto-gen/src/main/java/org/apache/dubbo/doc/DocAutoGen.java @@ -28,12 +28,44 @@ public static void main(String[] args) throws IOException { int level = 0; String parentPath = ""; System.setOut(new PrintStream(filePath + "/" + "README.md")); - System.out.println("# dubbo-spi-extensions"); - System.out.println("[![Build Status](https://travis-ci.org/apache/dubbo-spi-extensions.svg?branch=master)](https://travis-ci.org/apache/dubbo-spi-extensions)\n" + + String title = "# dubbo-spi-extensions"; + System.out.println(title); + String x = "[![Build Status](https://travis-ci.org/apache/dubbo-spi-extensions.svg?branch=master)](https://travis-ci.org/apache/dubbo-spi-extensions)\n" + "[![codecov](https://codecov.io/gh/apache/dubbo-spi-extensions/branch/master/graph/badge.svg)](https://codecov.io/gh/apache/dubbo-spi-extensions)\n" + "[![Maven Central](https://img.shields.io/maven-central/v/org.apache.dubbo/dubbo-spi-extensions.svg)](https://search.maven.org/search?q=g:org.apache.dubbo%20AND%20a:dubbo-spi-extensions)\n" + - "[![GitHub release](https://img.shields.io/github/release/apache/dubbo-spi-extensions.svg)]"); + "[![GitHub release](https://img.shields.io/github/release/apache/dubbo-spi-extensions.svg)]"; + System.out.println(x); + System.out.println(); + String description = "The purpose of dubbo-spi-extensions is to provide open, community-driven, reusable components to build microservice programs with different needs. These components extend the core of the Apache Dubbo project, but they are separated and decoupled."; + System.out.println(description); + + System.out.println(); + String usage = "Developers can flexibly choose the required extension dependencies to develop microservice programs based on their needs. The available extensions are as follows:Developers can flexibly choose the required extension dependencies to develop microservice programs based on their needs. "; + System.out.println(usage); + System.out.println(); + System.out.println("For version release notes, please refer to the documentation:"); + System.out.println("- [Release](https://cn.dubbo.apache.org/zh-cn/download/spi-extensions/)"); + System.out.println("- [Reference](https://cn.dubbo.apache.org/zh-cn/overview/mannual/java-sdk/reference-manual/spi/overview/)"); + System.out.println(); + + String asFollow = "The available extensions are as follows:"; + System.out.println(asFollow); + System.out.println(); + visitFile(file, parentPath, level); + System.out.println(); + String contributorTitle = "## Contribution\n"; + String thanks = "Thanks to everyone who has contributed!\n"; + String contributorImg = + "\n" + + " \n" + + "\n" ; + System.out.println(contributorTitle); + System.out.println(); + System.out.println(thanks); + System.out.println(); + System.out.println(contributorImg); + System.out.println(); } private static void visitFile(File file, String parentPath, int level) { @@ -58,7 +90,7 @@ private static void visitFile(File file, String parentPath, int level) { } String currentPath = level == 0 ? name : parentPath + "/" + name; - System.out.println(blank + "- [" + name + "]" + "(" + currentPath+ ")"); + System.out.println(blank + "- [" + name + "]" + "(" + currentPath + ")"); visitFile(f, currentPath, level + 1); } } diff --git a/dubbo-api-docs/README.md b/dubbo-api-docs/README.md index 64843b96c..0f5c4f7b8 100644 --- a/dubbo-api-docs/README.md +++ b/dubbo-api-docs/README.md @@ -9,7 +9,7 @@ Adding some annotations can generate a swagger like document without turning a n ## Involving repositorys * [dubbo-spi-extensions](https://github.com/apache/dubbo-spi-extensions) - [\branch: 2.7.x\dubbo-api-docs](https://github.com/apache/dubbo-spi-extensions/tree/2.7.x/dubbo-api-docs): + [\branch: 3.2.0\dubbo-api-docs](https://github.com/apache/dubbo-spi-extensions/tree/3.2.0/dubbo-api-docs): Dubbo-Api-Docs related annotation ,annotation parsing * [dubbo-admin](https://github.com/KeRan213539/dubbo-admin): Dubbo-Api-Docs document display, test function @@ -25,7 +25,7 @@ Adding some annotations can generate a swagger like document without turning a n * Of course, Dubbo API Docs consumed a little CPU resources when the project starting and used a little memory for caching. In the future, it will consider putting the contents of the cache into the metadata center -### Current Version: 2.7.8.3 +### Current Version: 3.2.0 ``` diff --git a/dubbo-api-docs/README_ch.md b/dubbo-api-docs/README_ch.md index 63ae7e219..4a19809a4 100644 --- a/dubbo-api-docs/README_ch.md +++ b/dubbo-api-docs/README_ch.md @@ -9,7 +9,7 @@ dubbo 接口文档、测试工具,根据注解生成文档,并提供测试功能 ## 相关项目 * [dubbo-spi-extensions](https://github.com/apache/dubbo-spi-extensions) - [\分支: 2.7.x\dubbo-api-docs](https://github.com/apache/dubbo-spi-extensions/tree/2.7.x/dubbo-api-docs): + [\分支: 3.2.0\dubbo-api-docs](https://github.com/apache/dubbo-spi-extensions/tree/3.2.0/dubbo-api-docs): Dubbo-Api-Docs 相关注解,解析注解 * [dubbo-admin](https://github.com/KeRan213539/dubbo-admin): Dubbo-Api-Docs 文档展示,测试功能 @@ -22,7 +22,7 @@ dubbo 接口文档、测试工具,根据注解生成文档,并提供测试功能 * 为避免增加生产环境中的资源占用, 建议单独创建一个配制类用于启用Dubbo Api Docs, 并配合 @Profile("dev") 注解使用 * 当然, Dubbo Api Docs 仅在项目启动时多消耗了点CPU资源, 并使用了一点点内存用于缓存, 将来会考虑将缓存中的内容放到元数据中心. -### 当前版本: 2.7.8.3 +### 当前版本: 3.2.0 ``` diff --git a/dubbo-api-docs/dubbo-api-docs-annotations/pom.xml b/dubbo-api-docs/dubbo-api-docs-annotations/pom.xml index 37794b445..dc0650f0b 100644 --- a/dubbo-api-docs/dubbo-api-docs-annotations/pom.xml +++ b/dubbo-api-docs/dubbo-api-docs-annotations/pom.xml @@ -21,7 +21,7 @@ org.apache.dubbo.extensions dubbo-api-docs - 1.0.1-SNAPSHOT + 3.2.0-SNAPSHOT ../pom.xml diff --git a/dubbo-api-docs/dubbo-api-docs-core/pom.xml b/dubbo-api-docs/dubbo-api-docs-core/pom.xml index 4d38aecc8..f6386c24f 100644 --- a/dubbo-api-docs/dubbo-api-docs-core/pom.xml +++ b/dubbo-api-docs/dubbo-api-docs-core/pom.xml @@ -22,7 +22,7 @@ org.apache.dubbo.extensions dubbo-api-docs - 1.0.1-SNAPSHOT + 3.2.0-SNAPSHOT ../pom.xml diff --git a/dubbo-api-docs/dubbo-api-docs-core/src/main/java/org/apache/dubbo/apidocs/core/DubboApiDocsAnnotationScanner.java b/dubbo-api-docs/dubbo-api-docs-core/src/main/java/org/apache/dubbo/apidocs/core/DubboApiDocsAnnotationScanner.java index 7e25b61b0..c8867b952 100644 --- a/dubbo-api-docs/dubbo-api-docs-core/src/main/java/org/apache/dubbo/apidocs/core/DubboApiDocsAnnotationScanner.java +++ b/dubbo-api-docs/dubbo-api-docs-core/src/main/java/org/apache/dubbo/apidocs/core/DubboApiDocsAnnotationScanner.java @@ -40,6 +40,7 @@ import com.alibaba.fastjson.JSON; import org.apache.commons.lang3.StringUtils; +import org.apache.dubbo.rpc.model.FrameworkModel; import org.springframework.aop.support.AopUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.context.event.ApplicationReadyEvent; @@ -90,14 +91,7 @@ public class DubboApiDocsAnnotationScanner implements ApplicationListener classType, RequestParam annotation, P */ private void exportDubboService(Class serviceClass, T serviceImplInstance, boolean async) { ServiceConfig service = new ServiceConfig<>(); + ApplicationConfig application = FrameworkModel.defaultModel().defaultApplication().getCurrentConfig(); service.setApplication(application); - service.setRegistry(registry); - service.setProtocol(protocol); + List registrys = FrameworkModel.defaultModel().defaultApplication().getApplicationConfigManager().getDefaultRegistries(); + Collection protocols = FrameworkModel.defaultModel().defaultApplication().getApplicationConfigManager().getProtocols(); + + service.setRegistry(registrys.get(0)); + service.setProtocol(protocols.iterator().next()); service.setInterface(serviceClass); service.setRef(serviceImplInstance); service.setAsync(async); diff --git a/dubbo-api-docs/dubbo-api-docs-examples/examples-api/pom.xml b/dubbo-api-docs/dubbo-api-docs-examples/examples-api/pom.xml index 6dbff83d7..a552bc522 100644 --- a/dubbo-api-docs/dubbo-api-docs-examples/examples-api/pom.xml +++ b/dubbo-api-docs/dubbo-api-docs-examples/examples-api/pom.xml @@ -21,7 +21,7 @@ org.apache.dubbo.extensions.examples.apidocs dubbo-api-docs-examples - 1.0.1-SNAPSHOT + 3.2.0-SNAPSHOT ../pom.xml diff --git a/dubbo-api-docs/dubbo-api-docs-examples/examples-provider-sca/pom.xml b/dubbo-api-docs/dubbo-api-docs-examples/examples-provider-sca/pom.xml index bded94ed4..b1ab55863 100644 --- a/dubbo-api-docs/dubbo-api-docs-examples/examples-provider-sca/pom.xml +++ b/dubbo-api-docs/dubbo-api-docs-examples/examples-provider-sca/pom.xml @@ -21,7 +21,7 @@ org.apache.dubbo.extensions.examples.apidocs dubbo-api-docs-examples - 1.0.1-SNAPSHOT + 3.2.0-SNAPSHOT ../pom.xml @@ -39,7 +39,7 @@ org.apache.dubbo.extensions.examples.apidocs examples-api - 1.0.1-SNAPSHOT + ${parent.version} org.springframework.boot diff --git a/dubbo-api-docs/dubbo-api-docs-examples/examples-provider/pom.xml b/dubbo-api-docs/dubbo-api-docs-examples/examples-provider/pom.xml index d68d91046..315ce71a3 100644 --- a/dubbo-api-docs/dubbo-api-docs-examples/examples-provider/pom.xml +++ b/dubbo-api-docs/dubbo-api-docs-examples/examples-provider/pom.xml @@ -21,7 +21,7 @@ org.apache.dubbo.extensions.examples.apidocs dubbo-api-docs-examples - 1.0.1-SNAPSHOT + 3.2.0-SNAPSHOT ../pom.xml @@ -39,7 +39,7 @@ org.apache.dubbo.extensions.examples.apidocs examples-api - 1.0.1-SNAPSHOT + ${parent.version} org.springframework.boot @@ -75,6 +75,7 @@ dubbo-spring-boot-starter + org.springframework.boot spring-boot-starter-logging @@ -104,6 +105,12 @@ dubbo-monitor-default ${dubbo.version} + + org.apache.dubbo + dubbo-dependencies-zookeeper + pom + ${dubbo.version} + diff --git a/dubbo-api-docs/dubbo-api-docs-examples/examples-provider/src/main/java/org/apache/dubbo/apidocs/examples/ExampleApplication.java b/dubbo-api-docs/dubbo-api-docs-examples/examples-provider/src/main/java/org/apache/dubbo/apidocs/examples/ExampleApplication.java index 8385af83a..f50878931 100644 --- a/dubbo-api-docs/dubbo-api-docs-examples/examples-provider/src/main/java/org/apache/dubbo/apidocs/examples/ExampleApplication.java +++ b/dubbo-api-docs/dubbo-api-docs-examples/examples-provider/src/main/java/org/apache/dubbo/apidocs/examples/ExampleApplication.java @@ -17,23 +17,22 @@ package org.apache.dubbo.apidocs.examples; import org.apache.dubbo.config.spring.context.annotation.EnableDubbo; - -import org.springframework.boot.WebApplicationType; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.builder.SpringApplicationBuilder; +import org.springframework.context.annotation.Profile; /** * example dubbo provider service application. */ + @SpringBootApplication +@Profile("dev") @EnableDubbo(scanBasePackages = {"org.apache.dubbo.apidocs.examples.api"}) public class ExampleApplication { public static void main(String[] args) { new SpringApplicationBuilder(ExampleApplication.class) // Non web applications - .web(WebApplicationType.NONE) .run(args); } - } diff --git a/dubbo-api-docs/dubbo-api-docs-examples/examples-provider/src/main/resources/application.yml b/dubbo-api-docs/dubbo-api-docs-examples/examples-provider/src/main/resources/application.yml index 77213eb0c..0b9880209 100644 --- a/dubbo-api-docs/dubbo-api-docs-examples/examples-provider/src/main/resources/application.yml +++ b/dubbo-api-docs/dubbo-api-docs-examples/examples-provider/src/main/resources/application.yml @@ -33,6 +33,8 @@ dubbo: name: dubbo application: name: dubbo-api-docs-example-provider + qos-enable: true + qos-port: 22222 metadata-report: address: zookeeper://127.0.0.1:2181 # group: DEFAULT_GROUP diff --git a/dubbo-api-docs/dubbo-api-docs-examples/pom.xml b/dubbo-api-docs/dubbo-api-docs-examples/pom.xml index 9b9e22620..c510f46e7 100644 --- a/dubbo-api-docs/dubbo-api-docs-examples/pom.xml +++ b/dubbo-api-docs/dubbo-api-docs-examples/pom.xml @@ -21,7 +21,7 @@ org.apache.dubbo.extensions dubbo-api-docs - 1.0.1-SNAPSHOT + 3.2.0-SNAPSHOT ../pom.xml diff --git a/dubbo-api-docs/pom.xml b/dubbo-api-docs/pom.xml index 12b4594b3..9b34adccf 100644 --- a/dubbo-api-docs/pom.xml +++ b/dubbo-api-docs/pom.xml @@ -25,7 +25,7 @@ dubbo-api-docs - 1.0.1-SNAPSHOT + 3.2.0-SNAPSHOT pom ${project.artifactId} @@ -42,14 +42,13 @@ 3.0.0 2.3.4.RELEASE - 2.7.18 - 2.7.8 + 3.2.7 1.9.4 4.2 3.4.2 3.0.0 1.4.0 - + 3.2.0-SNAPSHOT Hoxton.SR8 2.2.3.RELEASE @@ -75,12 +74,12 @@ org.apache.dubbo.extensions dubbo-api-docs-annotations - 1.0.1-SNAPSHOT + ${dubbo.api.docs.version} org.apache.dubbo.extensions dubbo-api-docs-core - 1.0.1-SNAPSHOT + ${dubbo.api.docs.version} diff --git a/dubbo-cluster-extensions/dubbo-cluster-broadcast-1/README.md b/dubbo-cluster-extensions/dubbo-cluster-broadcast-1/README.md new file mode 100644 index 000000000..e26993fbc --- /dev/null +++ b/dubbo-cluster-extensions/dubbo-cluster-broadcast-1/README.md @@ -0,0 +1,5 @@ + +# Dubbo Cluster BroadCast 1 + +## Introduction +The consumer initiates a broadcast call to all providers and obtains the call results of all providers. diff --git a/dubbo-cluster-extensions/dubbo-cluster-broadcast-1/Readme.md b/dubbo-cluster-extensions/dubbo-cluster-broadcast-1/Readme.md deleted file mode 100644 index 5adf0e4e2..000000000 --- a/dubbo-cluster-extensions/dubbo-cluster-broadcast-1/Readme.md +++ /dev/null @@ -1,3 +0,0 @@ -# Dubbo Cluster BroadCast 1 - -TBD diff --git a/dubbo-cluster-extensions/dubbo-cluster-broadcast-1/pom.xml b/dubbo-cluster-extensions/dubbo-cluster-broadcast-1/pom.xml index ebbb441a3..7056ea62a 100644 --- a/dubbo-cluster-extensions/dubbo-cluster-broadcast-1/pom.xml +++ b/dubbo-cluster-extensions/dubbo-cluster-broadcast-1/pom.xml @@ -27,13 +27,14 @@ 4.0.0 dubbo-cluster-broadcast-1 - 1.0.2-SNAPSHOT + 3.0.0-SNAPSHOT jar org.apache.dubbo dubbo-cluster + 3.2.7 true diff --git a/dubbo-cluster-extensions/dubbo-cluster-broadcast-1/src/test/java/org/apache/dubbo/rpc/cluster/support/BroadcastCluster1InvokerTest.java b/dubbo-cluster-extensions/dubbo-cluster-broadcast-1/src/test/java/org/apache/dubbo/rpc/cluster/support/BroadcastCluster1InvokerTest.java new file mode 100644 index 000000000..68589bab7 --- /dev/null +++ b/dubbo-cluster-extensions/dubbo-cluster-broadcast-1/src/test/java/org/apache/dubbo/rpc/cluster/support/BroadcastCluster1InvokerTest.java @@ -0,0 +1,28 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.dubbo.rpc.cluster.support; + +import org.junit.jupiter.api.Test; + +class BroadcastCluster1InvokerTest { + + @Test + void doInvoke() { + //todo + + } +} diff --git a/dubbo-cluster-extensions/dubbo-cluster-broadcast-1/src/test/java/org/apache/dubbo/rpc/cluster/support/BroadcastCluster1Test.java b/dubbo-cluster-extensions/dubbo-cluster-broadcast-1/src/test/java/org/apache/dubbo/rpc/cluster/support/BroadcastCluster1Test.java new file mode 100644 index 000000000..559e3faab --- /dev/null +++ b/dubbo-cluster-extensions/dubbo-cluster-broadcast-1/src/test/java/org/apache/dubbo/rpc/cluster/support/BroadcastCluster1Test.java @@ -0,0 +1,26 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.dubbo.rpc.cluster.support; + +import org.junit.jupiter.api.Test; + +class BroadcastCluster1Test { + + @Test + void doJoin() { + } +} diff --git a/dubbo-cluster-extensions/dubbo-cluster-loadbalance-peakewma/README.md b/dubbo-cluster-extensions/dubbo-cluster-loadbalance-peakewma/README.md new file mode 100644 index 000000000..0983ebbbd --- /dev/null +++ b/dubbo-cluster-extensions/dubbo-cluster-loadbalance-peakewma/README.md @@ -0,0 +1,13 @@ +# Introduction +PeakEwmaLoadBalance is designed to converge quickly when encountering slow endpoints. + +It is quick to react to latency spikes recovering only cautiously.Peak EWMA takes history into account,so that slow behavior is penalized relative to the supplied `decayTime`. + +if there are multiple invokers and the same cost,then randomly called,which doesn't care about weight. + +Inspiration drawn from: + +https://github.com/twitter/finagle/blob/1bc837c4feafc0096e43c0e98516a8e1c50c4421 + +/finagle-core/src/main/scala/com/twitter/finagle/loadbalancer/PeakEwma.scala + diff --git a/dubbo-cluster-extensions/dubbo-cluster-loadbalance-peakewma/README_CN.md b/dubbo-cluster-extensions/dubbo-cluster-loadbalance-peakewma/README_CN.md new file mode 100644 index 000000000..ac12720b9 --- /dev/null +++ b/dubbo-cluster-extensions/dubbo-cluster-loadbalance-peakewma/README_CN.md @@ -0,0 +1,12 @@ +# 简介 +PeakEwmaLoadBalance 旨在在遇到慢速端点时快速收敛。 + +它对延迟峰值反应很快,只是谨慎地恢复。峰值 EWMA 会考虑历史,因此相对于提供的“decayTime”,缓慢的行为会受到惩罚。 + +如果有多个调用者且成本相同,则随机调用,不关心权重。 + +灵感源自: + +https://github.com/twitter/finagle/blob/1bc837c4feafc0096e43c0e98516a8e1c50c4421 + +/finagle-core/src/main/scala/com/twitter/finagle/loadbalancer/PeakEwma.scala diff --git a/dubbo-cluster-extensions/dubbo-cluster-loadbalance-peakewma/pom.xml b/dubbo-cluster-extensions/dubbo-cluster-loadbalance-peakewma/pom.xml index 21bf5f16f..58fbc181b 100644 --- a/dubbo-cluster-extensions/dubbo-cluster-loadbalance-peakewma/pom.xml +++ b/dubbo-cluster-extensions/dubbo-cluster-loadbalance-peakewma/pom.xml @@ -27,18 +27,25 @@ 4.0.0 dubbo-cluster-loadbalance-peakewma - 1.0.2-SNAPSHOT + 3.0.0-SNAPSHOT jar + + + + + + org.apache.dubbo - dubbo-cluster - true + dubbo + 3.2.7 - org.apache.dubbo - dubbo-rpc-api + com.alibaba + fastjson + test diff --git a/dubbo-cluster-extensions/dubbo-cluster-specify-address-common/pom.xml b/dubbo-cluster-extensions/dubbo-cluster-specify-address-common/pom.xml index 2720aa63f..6343f5b65 100644 --- a/dubbo-cluster-extensions/dubbo-cluster-specify-address-common/pom.xml +++ b/dubbo-cluster-extensions/dubbo-cluster-specify-address-common/pom.xml @@ -27,11 +27,12 @@ 4.0.0 dubbo-cluster-specify-address-common - 1.0.2-SNAPSHOT + 3.0.0-SNAPSHOT org.apache.dubbo dubbo-common + 3.2.7 provided diff --git a/dubbo-cluster-extensions/dubbo-cluster-specify-address-common/src/main/java/org/apache/dubbo/rpc/cluster/specifyaddress/UserSpecifiedAddressUtil.java b/dubbo-cluster-extensions/dubbo-cluster-specify-address-common/src/main/java/org/apache/dubbo/rpc/cluster/specifyaddress/UserSpecifiedAddressUtil.java index 61a6ecb4a..6e0c164d0 100644 --- a/dubbo-cluster-extensions/dubbo-cluster-specify-address-common/src/main/java/org/apache/dubbo/rpc/cluster/specifyaddress/UserSpecifiedAddressUtil.java +++ b/dubbo-cluster-extensions/dubbo-cluster-specify-address-common/src/main/java/org/apache/dubbo/rpc/cluster/specifyaddress/UserSpecifiedAddressUtil.java @@ -19,7 +19,7 @@ import org.apache.dubbo.common.threadlocal.InternalThreadLocal; public class UserSpecifiedAddressUtil { - private final static InternalThreadLocal
ADDRESS = new InternalThreadLocal<>(); + private static final InternalThreadLocal
ADDRESS = new InternalThreadLocal<>(); /** * Set specified address to next invoke diff --git a/dubbo-cluster-extensions/dubbo-cluster-specify-address-dubbo2/pom.xml b/dubbo-cluster-extensions/dubbo-cluster-specify-address-dubbo2/pom.xml index 406039527..0d61eefb5 100644 --- a/dubbo-cluster-extensions/dubbo-cluster-specify-address-dubbo2/pom.xml +++ b/dubbo-cluster-extensions/dubbo-cluster-specify-address-dubbo2/pom.xml @@ -27,13 +27,13 @@ 4.0.0 dubbo-cluster-specify-address-dubbo2 - 1.0.2-SNAPSHOT + 3.0.0-SNAPSHOT org.apache.dubbo dubbo - 2.7.18 + 3.2.7 true diff --git a/dubbo-cluster-extensions/dubbo-cluster-specify-address-dubbo2/src/main/java/org/apache/dubbo/rpc/cluster/specifyaddress/UserSpecifiedAddressRouter.java b/dubbo-cluster-extensions/dubbo-cluster-specify-address-dubbo2/src/main/java/org/apache/dubbo/rpc/cluster/specifyaddress/UserSpecifiedAddressRouter.java index 3ca0ab1d8..1f4cb3729 100644 --- a/dubbo-cluster-extensions/dubbo-cluster-specify-address-dubbo2/src/main/java/org/apache/dubbo/rpc/cluster/specifyaddress/UserSpecifiedAddressRouter.java +++ b/dubbo-cluster-extensions/dubbo-cluster-specify-address-dubbo2/src/main/java/org/apache/dubbo/rpc/cluster/specifyaddress/UserSpecifiedAddressRouter.java @@ -52,21 +52,16 @@ public class UserSpecifiedAddressRouter extends AbstractRouter { - private final static Logger logger = LoggerFactory.getLogger(UserSpecifiedAddressRouter.class); + private static final Logger logger = LoggerFactory.getLogger(UserSpecifiedAddressRouter.class); // protected for ut purpose protected static int EXPIRE_TIME = 10 * 60 * 1000; - private volatile List> invokers = Collections.emptyList(); private volatile Map> ip2Invoker; private volatile Map> address2Invoker; private final Protocol protocol; - private final Lock cacheLock = new ReentrantLock(); - private final ScheduledExecutorService scheduledExecutorService; private final AtomicBoolean launchRemovalTask = new AtomicBoolean(false); - - private final Map>> newInvokerCache = new LinkedHashMap<>(16, 0.75f, true); public UserSpecifiedAddressRouter(URL referenceUrl) { @@ -318,16 +313,17 @@ private class RemovalTask implements Runnable { public void run() { cacheLock.lock(); try { - if (newInvokerCache.size() > 0) { - Iterator>>> iterator = newInvokerCache.entrySet().iterator(); - while (iterator.hasNext()) { - Map.Entry>> entry = iterator.next(); - if (System.currentTimeMillis() - entry.getValue().getLastAccess() > EXPIRE_TIME) { - iterator.remove(); - entry.getValue().getInvoker().destroy(); - } else { - break; - } + if (CollectionUtils.isEmptyMap(newInvokerCache)) { + return; + } + Iterator>>> iterator = newInvokerCache.entrySet().iterator(); + while (iterator.hasNext()) { + Map.Entry>> entry = iterator.next(); + if (System.currentTimeMillis() - entry.getValue().getLastAccess() > EXPIRE_TIME) { + iterator.remove(); + entry.getValue().getInvoker().destroy(); + } else { + break; } } } finally { diff --git a/dubbo-cluster-extensions/dubbo-cluster-specify-address-dubbo2/src/test/java/org/apache/dubbo/rpc/cluster/specifyaddress/InvokerCacheTest.java b/dubbo-cluster-extensions/dubbo-cluster-specify-address-dubbo2/src/test/java/org/apache/dubbo/rpc/cluster/specifyaddress/InvokerCacheTest.java index d86ffc4a9..fad7cfa25 100644 --- a/dubbo-cluster-extensions/dubbo-cluster-specify-address-dubbo2/src/test/java/org/apache/dubbo/rpc/cluster/specifyaddress/InvokerCacheTest.java +++ b/dubbo-cluster-extensions/dubbo-cluster-specify-address-dubbo2/src/test/java/org/apache/dubbo/rpc/cluster/specifyaddress/InvokerCacheTest.java @@ -23,9 +23,10 @@ import org.junit.jupiter.api.Test; import org.mockito.Mockito; -public class InvokerCacheTest { +class InvokerCacheTest { + @Test - public void test() throws InterruptedException { + void test() throws InterruptedException { InvokerCache> cache = new InvokerCache<>(Mockito.mock(Invoker.class)); long originTime = cache.getLastAccess(); Thread.sleep(5); diff --git a/dubbo-cluster-extensions/dubbo-cluster-specify-address-dubbo2/src/test/java/org/apache/dubbo/rpc/cluster/specifyaddress/UserSpecifiedAddressRouterFactoryTest.java b/dubbo-cluster-extensions/dubbo-cluster-specify-address-dubbo2/src/test/java/org/apache/dubbo/rpc/cluster/specifyaddress/UserSpecifiedAddressRouterFactoryTest.java index f3cbc88c7..8b3447aa6 100644 --- a/dubbo-cluster-extensions/dubbo-cluster-specify-address-dubbo2/src/test/java/org/apache/dubbo/rpc/cluster/specifyaddress/UserSpecifiedAddressRouterFactoryTest.java +++ b/dubbo-cluster-extensions/dubbo-cluster-specify-address-dubbo2/src/test/java/org/apache/dubbo/rpc/cluster/specifyaddress/UserSpecifiedAddressRouterFactoryTest.java @@ -23,9 +23,9 @@ import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; -public class UserSpecifiedAddressRouterFactoryTest { +class UserSpecifiedAddressRouterFactoryTest { @Test - public void test() { + void test() { RouterFactory stateRouterFactory = ExtensionLoader.getExtensionLoader(RouterFactory.class).getExtension("user-specified-address"); Assertions.assertEquals(UserSpecifiedAddressRouterFactory.class, stateRouterFactory.getClass()); diff --git a/dubbo-cluster-extensions/dubbo-cluster-specify-address-dubbo2/src/test/java/org/apache/dubbo/rpc/cluster/specifyaddress/UserSpecifiedAddressRouterTest.java b/dubbo-cluster-extensions/dubbo-cluster-specify-address-dubbo2/src/test/java/org/apache/dubbo/rpc/cluster/specifyaddress/UserSpecifiedAddressRouterTest.java index 30b6ea6d0..239073517 100644 --- a/dubbo-cluster-extensions/dubbo-cluster-specify-address-dubbo2/src/test/java/org/apache/dubbo/rpc/cluster/specifyaddress/UserSpecifiedAddressRouterTest.java +++ b/dubbo-cluster-extensions/dubbo-cluster-specify-address-dubbo2/src/test/java/org/apache/dubbo/rpc/cluster/specifyaddress/UserSpecifiedAddressRouterTest.java @@ -32,7 +32,7 @@ import java.util.LinkedList; import java.util.List; -public class UserSpecifiedAddressRouterTest { +class UserSpecifiedAddressRouterTest { private ApplicationModel applicationModel; private URL consumerUrl; @@ -43,7 +43,7 @@ public void setup() { } @Test - public void testNotify() { + void testNotify() { UserSpecifiedAddressRouter userSpecifiedAddressRouter = new UserSpecifiedAddressRouter(consumerUrl); Assertions.assertEquals(Collections.emptyList(), userSpecifiedAddressRouter.getInvokers()); Assertions.assertNull(userSpecifiedAddressRouter.getAddress2Invoker()); @@ -75,7 +75,7 @@ public void testNotify() { } @Test - public void testGetInvokerByURL() { + void testGetInvokerByURL() { UserSpecifiedAddressRouter userSpecifiedAddressRouter = new UserSpecifiedAddressRouter(consumerUrl); Assertions.assertEquals(Collections.emptyList(), @@ -138,7 +138,7 @@ public void testGetInvokerByURL() { } @Test - public void testGetInvokerByIp() { + void testGetInvokerByIp() { UserSpecifiedAddressRouter userSpecifiedAddressRouter = new UserSpecifiedAddressRouter(consumerUrl); Assertions.assertEquals(Collections.emptyList(), diff --git a/dubbo-cluster-extensions/dubbo-cluster-specify-address-dubbo2/src/test/java/org/apache/dubbo/rpc/cluster/specifyaddress/UserSpecifiedAddressUtilTest.java b/dubbo-cluster-extensions/dubbo-cluster-specify-address-dubbo2/src/test/java/org/apache/dubbo/rpc/cluster/specifyaddress/UserSpecifiedAddressUtilTest.java index 0dc1c1680..6126a9783 100644 --- a/dubbo-cluster-extensions/dubbo-cluster-specify-address-dubbo2/src/test/java/org/apache/dubbo/rpc/cluster/specifyaddress/UserSpecifiedAddressUtilTest.java +++ b/dubbo-cluster-extensions/dubbo-cluster-specify-address-dubbo2/src/test/java/org/apache/dubbo/rpc/cluster/specifyaddress/UserSpecifiedAddressUtilTest.java @@ -19,9 +19,9 @@ import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; -public class UserSpecifiedAddressUtilTest { +class UserSpecifiedAddressUtilTest { @Test - public void test() { + void test() { Assertions.assertNull(UserSpecifiedAddressUtil.getAddress()); UserSpecifiedAddressUtil.setAddress(new Address("127.0.0.1", 0)); Assertions.assertEquals(new Address("127.0.0.1", 0), UserSpecifiedAddressUtil.getAddress()); diff --git a/dubbo-cluster-extensions/dubbo-cluster-specify-address-dubbo3/pom.xml b/dubbo-cluster-extensions/dubbo-cluster-specify-address-dubbo3/pom.xml index 72f977c0c..1d9f79126 100644 --- a/dubbo-cluster-extensions/dubbo-cluster-specify-address-dubbo3/pom.xml +++ b/dubbo-cluster-extensions/dubbo-cluster-specify-address-dubbo3/pom.xml @@ -27,12 +27,13 @@ 4.0.0 dubbo-cluster-specify-address-dubbo3 - 1.0.2-SNAPSHOT + 3.0.0-SNAPSHOT org.apache.dubbo dubbo + 3.2.7 true diff --git a/dubbo-cluster-extensions/dubbo-cluster-specify-address-dubbo3/src/main/java/org/apache/dubbo/rpc/cluster/specifyaddress/AddressSpecifyClusterFilter.java b/dubbo-cluster-extensions/dubbo-cluster-specify-address-dubbo3/src/main/java/org/apache/dubbo/rpc/cluster/specifyaddress/AddressSpecifyClusterFilter.java index b7fa85f8b..8bce88201 100644 --- a/dubbo-cluster-extensions/dubbo-cluster-specify-address-dubbo3/src/main/java/org/apache/dubbo/rpc/cluster/specifyaddress/AddressSpecifyClusterFilter.java +++ b/dubbo-cluster-extensions/dubbo-cluster-specify-address-dubbo3/src/main/java/org/apache/dubbo/rpc/cluster/specifyaddress/AddressSpecifyClusterFilter.java @@ -29,10 +29,8 @@ @Activate(group = {"consumer"}) public class AddressSpecifyClusterFilter implements ClusterFilter { - @Override public Result invoke(Invoker invoker, Invocation invocation) throws RpcException { - Address current = UserSpecifiedAddressUtil.getAddress(); if (current != null) { invocation.put(Address.name, current); @@ -40,5 +38,4 @@ public Result invoke(Invoker invoker, Invocation invocation) throws RpcExcept return invoker.invoke(invocation); } - } diff --git a/dubbo-cluster-extensions/dubbo-cluster-specify-address-dubbo3/src/main/java/org/apache/dubbo/rpc/cluster/specifyaddress/DefaultUserSpecifiedServiceAddressBuilder.java b/dubbo-cluster-extensions/dubbo-cluster-specify-address-dubbo3/src/main/java/org/apache/dubbo/rpc/cluster/specifyaddress/DefaultUserSpecifiedServiceAddressBuilder.java index ab2ab3e17..48bde88a7 100644 --- a/dubbo-cluster-extensions/dubbo-cluster-specify-address-dubbo3/src/main/java/org/apache/dubbo/rpc/cluster/specifyaddress/DefaultUserSpecifiedServiceAddressBuilder.java +++ b/dubbo-cluster-extensions/dubbo-cluster-specify-address-dubbo3/src/main/java/org/apache/dubbo/rpc/cluster/specifyaddress/DefaultUserSpecifiedServiceAddressBuilder.java @@ -39,7 +39,7 @@ import static org.apache.dubbo.common.constants.CommonConstants.VERSION_KEY; public class DefaultUserSpecifiedServiceAddressBuilder implements UserSpecifiedServiceAddressBuilder { - public final static String NAME = "default"; + public static final String NAME = "default"; private final ExtensionLoader protocolExtensionLoader; @@ -49,41 +49,27 @@ public DefaultUserSpecifiedServiceAddressBuilder(ApplicationModel applicationMod @Override public URL buildAddress(List> invokers, Address address, Invocation invocation, URL consumerUrl) { - - boolean useFixed = false; - URL template = null; - if (!invokers.isEmpty()) { - template = invokers.iterator().next().getUrl(); - if (template instanceof InstanceAddressURL) { - useFixed = true; - } else { - if (template.getUrlAddress() == null) { - PathURLAddress urlAddress = new PathURLAddress(template.getProtocol(), template.getUsername(), template.getPassword(), template.getPath(), address.getIp(), address.getPort()); - template = new ServiceConfigURL(urlAddress, template.getUrlParam(), template.getAttributes()); - } else { - template = template.setHost(address.getIp()); - if (address.getPort() != 0) { - template = template.setPort(address.getPort()); - } - } - } - - } else { - useFixed = true; - } - - if (useFixed) { + URL template; + if (invokers.isEmpty() || (template = invokers.get(0).getUrl()) instanceof InstanceAddressURL) { String ip = address.getIp(); int port = address.getPort(); String protocol = consumerUrl.getParameter(PROTOCOL_KEY, DUBBO); if (port == 0) { port = protocolExtensionLoader.getExtension(protocol).getDefaultPort(); } - template = new DubboServiceAddressURL( - new PathURLAddress(protocol, null, null, consumerUrl.getPath(), ip, port), - consumerUrl.getUrlParam(), consumerUrl, null); + return new DubboServiceAddressURL( + new PathURLAddress(protocol, null, null, consumerUrl.getPath(), ip, port), + consumerUrl.getUrlParam(), consumerUrl, null); + } + if (template.getUrlAddress() == null) { + PathURLAddress urlAddress = new PathURLAddress(template.getProtocol(), template.getUsername(), + template.getPassword(), template.getPath(), address.getIp(), address.getPort()); + return new ServiceConfigURL(urlAddress, template.getUrlParam(), template.getAttributes()); + } + template = template.setHost(address.getIp()); + if (address.getPort() != 0) { + template = template.setPort(address.getPort()); } - return template; } diff --git a/dubbo-cluster-extensions/dubbo-cluster-specify-address-dubbo3/src/main/java/org/apache/dubbo/rpc/cluster/specifyaddress/UserSpecifiedAddressRouter.java b/dubbo-cluster-extensions/dubbo-cluster-specify-address-dubbo3/src/main/java/org/apache/dubbo/rpc/cluster/specifyaddress/UserSpecifiedAddressRouter.java index 5ce7281b1..84048d575 100644 --- a/dubbo-cluster-extensions/dubbo-cluster-specify-address-dubbo3/src/main/java/org/apache/dubbo/rpc/cluster/specifyaddress/UserSpecifiedAddressRouter.java +++ b/dubbo-cluster-extensions/dubbo-cluster-specify-address-dubbo3/src/main/java/org/apache/dubbo/rpc/cluster/specifyaddress/UserSpecifiedAddressRouter.java @@ -46,20 +46,16 @@ import java.util.concurrent.locks.ReentrantLock; public class UserSpecifiedAddressRouter extends AbstractStateRouter { - private final static Logger logger = LoggerFactory.getLogger(UserSpecifiedAddressRouter.class); + private static final Logger logger = LoggerFactory.getLogger(UserSpecifiedAddressRouter.class); // protected for ut purpose protected static int EXPIRE_TIME = 10 * 60 * 1000; - private final static String USER_SPECIFIED_SERVICE_ADDRESS_BUILDER_KEY = "userSpecifiedServiceAddressBuilder"; - + private static final String USER_SPECIFIED_SERVICE_ADDRESS_BUILDER_KEY = "userSpecifiedServiceAddressBuilder"; private volatile BitList> invokers = BitList.emptyList(); private volatile Map> ip2Invoker; private volatile Map> address2Invoker; - private final Lock cacheLock = new ReentrantLock(); private final Map>> newInvokerCache = new LinkedHashMap<>(16, 0.75f, true); - private final UserSpecifiedServiceAddressBuilder userSpecifiedServiceAddressBuilder; - private final Protocol protocol; private final ScheduledExecutorService scheduledExecutorService; private final AtomicBoolean launchRemovalTask = new AtomicBoolean(false); @@ -213,14 +209,11 @@ public Invoker getInvokerByIp(Address address) { Invoker targetInvoker; if (port != 0) { targetInvoker = address2Invoker.get(ip + ":" + port); - if (targetInvoker != null) { - return targetInvoker; - } } else { targetInvoker = ip2Invoker.get(ip); - if (targetInvoker != null) { - return targetInvoker; - } + } + if (targetInvoker != null) { + return targetInvoker; } if (!address.isNeedToCreate()) { @@ -314,16 +307,17 @@ private class RemovalTask implements Runnable { public void run() { cacheLock.lock(); try { - if (newInvokerCache.size() > 0) { - Iterator>>> iterator = newInvokerCache.entrySet().iterator(); - while (iterator.hasNext()) { - Map.Entry>> entry = iterator.next(); - if (System.currentTimeMillis() - entry.getValue().getLastAccess() > EXPIRE_TIME) { - iterator.remove(); - entry.getValue().getInvoker().destroy(); - } else { - break; - } + if (CollectionUtils.isEmptyMap(newInvokerCache)) { + return; + } + Iterator>>> iterator = newInvokerCache.entrySet().iterator(); + while (iterator.hasNext()) { + Map.Entry>> entry = iterator.next(); + if (System.currentTimeMillis() - entry.getValue().getLastAccess() > EXPIRE_TIME) { + iterator.remove(); + entry.getValue().getInvoker().destroy(); + } else { + break; } } } finally { diff --git a/dubbo-cluster-extensions/dubbo-cluster-specify-address-dubbo3/src/test/java/org/apache/dubbo/rpc/cluster/specifyaddress/DefaultUserSpecifiedServiceAddressBuilderTest.java b/dubbo-cluster-extensions/dubbo-cluster-specify-address-dubbo3/src/test/java/org/apache/dubbo/rpc/cluster/specifyaddress/DefaultUserSpecifiedServiceAddressBuilderTest.java index 4d46f52e2..b2aaf7c62 100644 --- a/dubbo-cluster-extensions/dubbo-cluster-specify-address-dubbo3/src/test/java/org/apache/dubbo/rpc/cluster/specifyaddress/DefaultUserSpecifiedServiceAddressBuilderTest.java +++ b/dubbo-cluster-extensions/dubbo-cluster-specify-address-dubbo3/src/test/java/org/apache/dubbo/rpc/cluster/specifyaddress/DefaultUserSpecifiedServiceAddressBuilderTest.java @@ -28,9 +28,9 @@ import java.util.Collections; -public class DefaultUserSpecifiedServiceAddressBuilderTest { +class DefaultUserSpecifiedServiceAddressBuilderTest { @Test - public void testBuild() { + void testBuild() { ApplicationModel applicationModel = ApplicationModel.defaultModel(); DefaultUserSpecifiedServiceAddressBuilder defaultUserSpecifiedServiceAddressBuilder = new DefaultUserSpecifiedServiceAddressBuilder(applicationModel); @@ -79,7 +79,7 @@ public void testBuild() { } @Test - public void testReBuild() { + void testReBuild() { ApplicationModel applicationModel = ApplicationModel.defaultModel(); DefaultUserSpecifiedServiceAddressBuilder defaultUserSpecifiedServiceAddressBuilder = new DefaultUserSpecifiedServiceAddressBuilder(applicationModel); diff --git a/dubbo-cluster-extensions/dubbo-cluster-specify-address-dubbo3/src/test/java/org/apache/dubbo/rpc/cluster/specifyaddress/InvokerCacheTest.java b/dubbo-cluster-extensions/dubbo-cluster-specify-address-dubbo3/src/test/java/org/apache/dubbo/rpc/cluster/specifyaddress/InvokerCacheTest.java index d86ffc4a9..fe7fdbb79 100644 --- a/dubbo-cluster-extensions/dubbo-cluster-specify-address-dubbo3/src/test/java/org/apache/dubbo/rpc/cluster/specifyaddress/InvokerCacheTest.java +++ b/dubbo-cluster-extensions/dubbo-cluster-specify-address-dubbo3/src/test/java/org/apache/dubbo/rpc/cluster/specifyaddress/InvokerCacheTest.java @@ -23,9 +23,9 @@ import org.junit.jupiter.api.Test; import org.mockito.Mockito; -public class InvokerCacheTest { +class InvokerCacheTest { @Test - public void test() throws InterruptedException { + void test() throws InterruptedException { InvokerCache> cache = new InvokerCache<>(Mockito.mock(Invoker.class)); long originTime = cache.getLastAccess(); Thread.sleep(5); diff --git a/dubbo-cluster-extensions/dubbo-cluster-specify-address-dubbo3/src/test/java/org/apache/dubbo/rpc/cluster/specifyaddress/UserSpecifiedAddressRouterFactoryTest.java b/dubbo-cluster-extensions/dubbo-cluster-specify-address-dubbo3/src/test/java/org/apache/dubbo/rpc/cluster/specifyaddress/UserSpecifiedAddressRouterFactoryTest.java index 5276f74d3..255d371f9 100644 --- a/dubbo-cluster-extensions/dubbo-cluster-specify-address-dubbo3/src/test/java/org/apache/dubbo/rpc/cluster/specifyaddress/UserSpecifiedAddressRouterFactoryTest.java +++ b/dubbo-cluster-extensions/dubbo-cluster-specify-address-dubbo3/src/test/java/org/apache/dubbo/rpc/cluster/specifyaddress/UserSpecifiedAddressRouterFactoryTest.java @@ -23,9 +23,9 @@ import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; -public class UserSpecifiedAddressRouterFactoryTest { +class UserSpecifiedAddressRouterFactoryTest { @Test - public void test() { + void test() { ApplicationModel applicationModel = ApplicationModel.defaultModel(); StateRouterFactory stateRouterFactory = applicationModel.getExtensionLoader(StateRouterFactory.class).getExtension("user-specified-address"); diff --git a/dubbo-cluster-extensions/dubbo-cluster-specify-address-dubbo3/src/test/java/org/apache/dubbo/rpc/cluster/specifyaddress/UserSpecifiedAddressRouterTest.java b/dubbo-cluster-extensions/dubbo-cluster-specify-address-dubbo3/src/test/java/org/apache/dubbo/rpc/cluster/specifyaddress/UserSpecifiedAddressRouterTest.java index e25d64ce3..f874e52eb 100644 --- a/dubbo-cluster-extensions/dubbo-cluster-specify-address-dubbo3/src/test/java/org/apache/dubbo/rpc/cluster/specifyaddress/UserSpecifiedAddressRouterTest.java +++ b/dubbo-cluster-extensions/dubbo-cluster-specify-address-dubbo3/src/test/java/org/apache/dubbo/rpc/cluster/specifyaddress/UserSpecifiedAddressRouterTest.java @@ -33,7 +33,7 @@ import java.util.Collections; -public class UserSpecifiedAddressRouterTest { +class UserSpecifiedAddressRouterTest { private ApplicationModel applicationModel; private URL consumerUrl; @@ -53,12 +53,12 @@ public void teardown() { } @Test - public void test() { + void test() { Assertions.assertTrue(new UserSpecifiedAddressRouter<>(URL.valueOf("").setScopeModel(applicationModel.newModule())).supportContinueRoute()); } @Test - public void testNotify() { + void testNotify() { UserSpecifiedAddressRouter userSpecifiedAddressRouter = new UserSpecifiedAddressRouter<>(consumerUrl); Assertions.assertEquals(BitList.emptyList(), userSpecifiedAddressRouter.getInvokers()); Assertions.assertNull(userSpecifiedAddressRouter.getAddress2Invoker()); @@ -88,7 +88,7 @@ public void testNotify() { } @Test - public void testGetInvokerByURL() { + void testGetInvokerByURL() { UserSpecifiedAddressRouter userSpecifiedAddressRouter = new UserSpecifiedAddressRouter<>(consumerUrl); Assertions.assertEquals(BitList.emptyList(), @@ -195,7 +195,7 @@ public void testGetInvokerByURL() { } @Test - public void testGetInvokerByIp() { + void testGetInvokerByIp() { UserSpecifiedAddressRouter userSpecifiedAddressRouter = new UserSpecifiedAddressRouter<>(consumerUrl); Assertions.assertEquals(BitList.emptyList(), @@ -247,7 +247,7 @@ public void testGetInvokerByIp() { @Test @SuppressWarnings("unchecked") - public void testRemovalTask() throws InterruptedException { + void testRemovalTask() throws InterruptedException { UserSpecifiedAddressRouter.EXPIRE_TIME = 10; UserSpecifiedAddressRouter userSpecifiedAddressRouter = new UserSpecifiedAddressRouter<>(consumerUrl); diff --git a/dubbo-cluster-extensions/dubbo-cluster-specify-address-dubbo3/src/test/java/org/apache/dubbo/rpc/cluster/specifyaddress/UserSpecifiedAddressUtilTest.java b/dubbo-cluster-extensions/dubbo-cluster-specify-address-dubbo3/src/test/java/org/apache/dubbo/rpc/cluster/specifyaddress/UserSpecifiedAddressUtilTest.java index e3048deb1..c54662386 100644 --- a/dubbo-cluster-extensions/dubbo-cluster-specify-address-dubbo3/src/test/java/org/apache/dubbo/rpc/cluster/specifyaddress/UserSpecifiedAddressUtilTest.java +++ b/dubbo-cluster-extensions/dubbo-cluster-specify-address-dubbo3/src/test/java/org/apache/dubbo/rpc/cluster/specifyaddress/UserSpecifiedAddressUtilTest.java @@ -19,9 +19,9 @@ import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; -public class UserSpecifiedAddressUtilTest { +class UserSpecifiedAddressUtilTest { @Test - public void test() { + void test() { Assertions.assertNull(UserSpecifiedAddressUtil.getAddress()); UserSpecifiedAddressUtil.setAddress(new Address("127.0.0.1", 0)); Assertions.assertEquals(new Address("127.0.0.1", 0), UserSpecifiedAddressUtil.getAddress()); diff --git a/dubbo-configcenter-extensions/dubbo-configcenter-consul/pom.xml b/dubbo-configcenter-extensions/dubbo-configcenter-consul/pom.xml index 8581386c5..bf18367cc 100644 --- a/dubbo-configcenter-extensions/dubbo-configcenter-consul/pom.xml +++ b/dubbo-configcenter-extensions/dubbo-configcenter-consul/pom.xml @@ -32,6 +32,7 @@ org.apache.dubbo dubbo-common + 3.2.0 true diff --git a/dubbo-configcenter-extensions/dubbo-configcenter-consul/src/test/java/org/apache/dubbo/configcenter/consul/ConsulDynamicConfigurationTest.java b/dubbo-configcenter-extensions/dubbo-configcenter-consul/src/test/java/org/apache/dubbo/configcenter/consul/ConsulDynamicConfigurationTest.java index 60112b9d6..4f38fa0da 100644 --- a/dubbo-configcenter-extensions/dubbo-configcenter-consul/src/test/java/org/apache/dubbo/configcenter/consul/ConsulDynamicConfigurationTest.java +++ b/dubbo-configcenter-extensions/dubbo-configcenter-consul/src/test/java/org/apache/dubbo/configcenter/consul/ConsulDynamicConfigurationTest.java @@ -1,123 +1,116 @@ -///* -// * Licensed to the Apache Software Foundation (ASF) under one or more -// * contributor license agreements. See the NOTICE file distributed with -// * this work for additional information regarding copyright ownership. -// * The ASF licenses this file to You 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.apache.dubbo.configcenter.consul; -// -//import org.apache.dubbo.common.URL; -// -//import com.google.common.net.HostAndPort; -//import com.orbitz.consul.Consul; -//import com.orbitz.consul.KeyValueClient; -//import com.orbitz.consul.cache.KVCache; -//import com.orbitz.consul.model.kv.Value; -//import com.pszymczyk.consul.ConsulProcess; -//import com.pszymczyk.consul.ConsulStarterBuilder; -//import org.junit.jupiter.api.AfterAll; -//import org.junit.jupiter.api.Assertions; -//import org.junit.jupiter.api.BeforeAll; -//import org.junit.jupiter.api.Test; -// -//import java.util.Arrays; -//import java.util.Optional; -//import java.util.TreeSet; -// -//import static org.junit.jupiter.api.Assertions.assertEquals; -// -///** -// * -// */ -//public class ConsulDynamicConfigurationTest { -// -// private static ConsulProcess consul; -// private static URL configCenterUrl; -// private static ConsulDynamicConfiguration configuration; -// -// private static Consul client; -// private static KeyValueClient kvClient; -// -// @BeforeAll -// public static void setUp() throws Exception { -// consul = ConsulStarterBuilder.consulStarter() -// .build() -// .start(); -// configCenterUrl = URL.valueOf("consul://127.0.0.1:" + consul.getHttpPort()); -// -// configuration = new ConsulDynamicConfiguration(configCenterUrl); -// client = Consul.builder().withHostAndPort(HostAndPort.fromParts("127.0.0.1", consul.getHttpPort())).build(); -// kvClient = client.keyValueClient(); -// } -// -// @AfterAll -// public static void tearDown() throws Exception { -// consul.close(); -// configuration.close(); -// } -// -// @Test -// public void testGetConfig() { -// kvClient.putValue("/dubbo/config/dubbo/foo", "bar"); -// // test equals -// assertEquals("bar", configuration.getConfig("foo", "dubbo")); -// // test does not block -// assertEquals("bar", configuration.getConfig("foo", "dubbo")); -// Assertions.assertNull(configuration.getConfig("not-exist", "dubbo")); -// } -// -// @Test -// public void testPublishConfig() { -// configuration.publishConfig("value", "metadata", "1"); -// // test equals -// assertEquals("1", configuration.getConfig("value", "/metadata")); -// assertEquals("1", kvClient.getValueAsString("/dubbo/config/metadata/value").get()); -// } -// -// @Test -// public void testAddListener() { -// KVCache cache = KVCache.newCache(kvClient, "/dubbo/config/dubbo/foo"); -// cache.addListener(newValues -> { -// // Cache notifies all paths with "foo" the root path -// // If you want to watch only "foo" value, you must filter other paths -// Optional newValue = newValues.values().stream() -// .filter(value -> value.getKey().equals("foo")) -// .findAny(); -// -// newValue.ifPresent(value -> { -// // Values are encoded in key/value store, decode it if needed -// Optional decodedValue = newValue.get().getValueAsString(); -// decodedValue.ifPresent(v -> System.out.println(String.format("Value is: %s", v))); //prints "bar" -// }); -// }); -// cache.start(); -// -// kvClient.putValue("/dubbo/config/dubbo/foo", "new-value"); -// kvClient.putValue("/dubbo/config/dubbo/foo/sub", "sub-value"); -// kvClient.putValue("/dubbo/config/dubbo/foo/sub2", "sub-value2"); -// kvClient.putValue("/dubbo/config/foo", "parent-value"); -// -// System.out.println(kvClient.getKeys("/dubbo/config/dubbo/foo")); -// System.out.println(kvClient.getKeys("/dubbo/config")); -// System.out.println(kvClient.getValues("/dubbo/config/dubbo/foo")); -// } -// -// @Test -// public void testGetConfigKeys() { -// configuration.publishConfig("v1", "metadata", "1"); -// configuration.publishConfig("v2", "metadata", "2"); -// configuration.publishConfig("v3", "metadata", "3"); -// // test equals -// assertEquals(new TreeSet(Arrays.asList("v1", "v2", "v3")), configuration.getConfigKeys("metadata")); -// } -//} +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.dubbo.configcenter.consul; +import org.apache.dubbo.common.URL; +import com.google.common.net.HostAndPort; +import com.orbitz.consul.Consul; +import com.orbitz.consul.KeyValueClient; +import com.orbitz.consul.cache.KVCache; +import com.orbitz.consul.model.kv.Value; +import com.pszymczyk.consul.ConsulProcess; +import com.pszymczyk.consul.ConsulStarterBuilder; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import java.util.Arrays; +import java.util.Optional; +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class ConsulDynamicConfigurationTest { + + private static ConsulProcess consul; + private static URL configCenterUrl; + private static ConsulDynamicConfiguration configuration; + + private static Consul client; + private static KeyValueClient kvClient; + + @BeforeAll + public static void setUp() throws Exception { + consul = ConsulStarterBuilder.consulStarter() + .build() + .start(); + configCenterUrl = URL.valueOf("consul://127.0.0.1:" + consul.getHttpPort()); + + configuration = new ConsulDynamicConfiguration(configCenterUrl); + client = Consul.builder().withHostAndPort(HostAndPort.fromParts("127.0.0.1", consul.getHttpPort())).build(); + kvClient = client.keyValueClient(); + } + + @AfterAll + public static void tearDown() throws Exception { + consul.close(); + configuration.close(); + } + + @Test + public void testGetConfig() { + kvClient.putValue("/dubbo/config/dubbo/foo", "bar"); + // test equals + assertEquals("bar", configuration.getConfig("foo", "dubbo")); + // test does not block + assertEquals("bar", configuration.getConfig("foo", "dubbo")); + Assertions.assertNull(configuration.getConfig("not-exist", "dubbo")); + } + + @Test + public void testPublishConfig() { + configuration.publishConfig("value", "metadata", "1"); + // test equals + assertEquals("1", configuration.getConfig("value", "/metadata")); + assertEquals("1", kvClient.getValueAsString("/dubbo/config/metadata/value").get()); + } + + @Test + public void testAddListener() { + KVCache cache = KVCache.newCache(kvClient, "/dubbo/config/dubbo/foo"); + cache.addListener(newValues -> { + // Cache notifies all paths with "foo" the root path + // If you want to watch only "foo" value, you must filter other paths + Optional newValue = newValues.values().stream() + .filter(value -> value.getKey().equals("foo")) + .findAny(); + + newValue.ifPresent(value -> { + // Values are encoded in key/value store, decode it if needed + Optional decodedValue = newValue.get().getValueAsString(); + decodedValue.ifPresent(v -> System.out.println(String.format("Value is: %s", v))); //prints "bar" + }); + }); + cache.start(); + + kvClient.putValue("/dubbo/config/dubbo/foo", "new-value"); + kvClient.putValue("/dubbo/config/dubbo/foo/sub", "sub-value"); + kvClient.putValue("/dubbo/config/dubbo/foo/sub2", "sub-value2"); + kvClient.putValue("/dubbo/config/foo", "parent-value"); + + System.out.println(kvClient.getKeys("/dubbo/config/dubbo/foo")); + System.out.println(kvClient.getKeys("/dubbo/config")); + System.out.println(kvClient.getValues("/dubbo/config/dubbo/foo")); + } + + @Test + public void testGetConfigKeys() { + configuration.publishConfig("v1", "metadata", "1"); + configuration.publishConfig("v2", "metadata", "2"); + configuration.publishConfig("v3", "metadata", "3"); + // test equals + assertEquals(Arrays.asList("v1", "v2", "v3"), configuration.doGetConfigKeys("/dubbo/config/metadata")); + + } +} diff --git a/dubbo-configcenter-extensions/dubbo-configcenter-etcd/pom.xml b/dubbo-configcenter-extensions/dubbo-configcenter-etcd/pom.xml index 3078cca42..38511eaef 100644 --- a/dubbo-configcenter-extensions/dubbo-configcenter-etcd/pom.xml +++ b/dubbo-configcenter-extensions/dubbo-configcenter-etcd/pom.xml @@ -43,6 +43,16 @@ jetcd-launcher test + + + org.testcontainers testcontainers @@ -52,6 +62,7 @@ org.apache.dubbo dubbo-common + 3.2.0 true @@ -59,6 +70,7 @@ dubbo-remoting-etcd3 1.0.2-SNAPSHOT + @@ -66,6 +78,7 @@ org.apache.maven.plugins maven-surefire-plugin + 2.7.1 ${skipIntegrationTests} diff --git a/dubbo-configcenter-extensions/dubbo-configcenter-etcd/src/test/java/org/apache/dubbo/configcenter/support/etcd/EtcdDynamicConfigurationTest.java b/dubbo-configcenter-extensions/dubbo-configcenter-etcd/src/test/java/org/apache/dubbo/configcenter/support/etcd/EtcdDynamicConfigurationTest.java index e6ed4b5bc..d944c003b 100644 --- a/dubbo-configcenter-extensions/dubbo-configcenter-etcd/src/test/java/org/apache/dubbo/configcenter/support/etcd/EtcdDynamicConfigurationTest.java +++ b/dubbo-configcenter-extensions/dubbo-configcenter-etcd/src/test/java/org/apache/dubbo/configcenter/support/etcd/EtcdDynamicConfigurationTest.java @@ -16,12 +16,10 @@ */ package org.apache.dubbo.configcenter.support.etcd; - import org.apache.dubbo.common.URL; import org.apache.dubbo.common.config.configcenter.ConfigChangedEvent; import org.apache.dubbo.common.config.configcenter.ConfigurationListener; import org.apache.dubbo.common.config.configcenter.DynamicConfiguration; - import io.etcd.jetcd.ByteSequence; import io.etcd.jetcd.Client; import io.etcd.jetcd.launcher.EtcdCluster; @@ -30,7 +28,7 @@ import org.junit.Assert; import org.junit.Before; import org.junit.Test; - +import org.junit.jupiter.api.Disabled; import java.net.URI; import java.util.HashMap; import java.util.List; @@ -45,55 +43,63 @@ * Unit test for etcd config center support * Integrate with https://github.com/etcd-io/jetcd#launcher */ +@Disabled public class EtcdDynamicConfigurationTest { private static EtcdDynamicConfiguration config; public EtcdCluster etcdCluster = EtcdClusterFactory.buildCluster(getClass().getSimpleName(), 3, false); + //public EtcdCluster etcdCluster= new Etcd.Builder().withClusterName(getClass().getSimpleName()).withNodes(3).withSsl(false).build(); + private static Client client; - @Test - public void testGetConfig() { - put("/dubbo/config/org.apache.dubbo.etcd.testService/configurators", "hello"); + @Test + public void testGetConfig() { + put("/dubbo/config/dubbo/org.apache.dubbo.etcd.testService/configurators", "hello"); put("/dubbo/config/test/dubbo.properties", "aaa=bbb"); - Assert.assertEquals("hello", config.getConfig("org.apache.dubbo.etcd.testService.configurators", DynamicConfiguration.DEFAULT_GROUP)); + Assert.assertEquals("hello", config.getConfig("org.apache.dubbo.etcd.testService/configurators", DynamicConfiguration.DEFAULT_GROUP)); Assert.assertEquals("aaa=bbb", config.getConfig("dubbo.properties", "test")); } + @Test - public void testAddListener() throws Exception { + public void testAddListener1() throws Exception { + CountDownLatch latch = new CountDownLatch(4); TestListener listener1 = new TestListener(latch); TestListener listener2 = new TestListener(latch); TestListener listener3 = new TestListener(latch); TestListener listener4 = new TestListener(latch); - config.addListener("AService.configurators", listener1); - config.addListener("AService.configurators", listener2); - config.addListener("testapp.tagrouters", listener3); - config.addListener("testapp.tagrouters", listener4); - put("/dubbo/config/AService/configurators", "new value1"); - Thread.sleep(200); - put("/dubbo/config/testapp/tagrouters", "new value2"); + config.addListener("AService/configurators", listener1); + config.addListener("AService/configurators", listener2); + config.addListener("testapp/tagrouters", listener3); + config.addListener("testapp/tagrouters", listener4); + + //全路径 + put("/dubbo/config/dubbo/AService/configurators", "new value1"); Thread.sleep(200); - put("/dubbo/config/testapp", "new value3"); + put("/dubbo/config/dubbo/testapp/tagrouters", "new value2"); Thread.sleep(1000); Assert.assertTrue(latch.await(5, TimeUnit.SECONDS)); - Assert.assertEquals(1, listener1.getCount("/dubbo/config/AService/configurators")); - Assert.assertEquals(1, listener2.getCount("/dubbo/config/AService/configurators")); - Assert.assertEquals(1, listener3.getCount("/dubbo/config/testapp/tagrouters")); - Assert.assertEquals(1, listener4.getCount("/dubbo/config/testapp/tagrouters")); + Assert.assertEquals(1, listener1.getCount("AService/configurators")); + Assert.assertEquals(1, listener2.getCount("AService/configurators")); + Assert.assertEquals(1, listener3.getCount("testapp/tagrouters")); + Assert.assertEquals(1, listener4.getCount("testapp/tagrouters")); Assert.assertEquals("new value1", listener1.getValue()); Assert.assertEquals("new value1", listener2.getValue()); Assert.assertEquals("new value2", listener3.getValue()); Assert.assertEquals("new value2", listener4.getValue()); + } + + private class TestListener implements ConfigurationListener { private CountDownLatch latch; private String value; @@ -128,6 +134,7 @@ private void put(String key, String value) { } } + //这里会涉及到docker拉取镜像很慢 @Before public void setUp() { @@ -137,18 +144,19 @@ public void setUp() { List clientEndPoints = etcdCluster.getClientEndpoints(); - String ipAddress = clientEndPoints.get(0).getHost() + ":" + clientEndPoints.get(0).getPort(); + String ipAddress =clientEndPoints.get(0).getHost() + ":" + clientEndPoints.get(0).getPort(); //"127.0.0.1:2379"; + String urlForDubbo = "etcd3://" + ipAddress + "/org.apache.dubbo.etcd.testService"; // timeout in 15 seconds. - URL url = URL.valueOf(urlForDubbo) - .addParameter(SESSION_TIMEOUT_KEY, 15000); + URL url = URL.valueOf(urlForDubbo).addParameter(SESSION_TIMEOUT_KEY, 15000); config = new EtcdDynamicConfiguration(url); } @After public void tearDown() { etcdCluster.close(); + client.close(); } } diff --git a/dubbo-extensions-dependencies-bom/pom.xml b/dubbo-extensions-dependencies-bom/pom.xml index 03d221c57..b71288332 100644 --- a/dubbo-extensions-dependencies-bom/pom.xml +++ b/dubbo-extensions-dependencies-bom/pom.xml @@ -95,7 +95,7 @@ 2.4.1 - 5.3.2 + 6.9.2 4.0.51 4.5.13 1.2.0 @@ -132,7 +132,7 @@ 1.5.2 1.9.12 5.2.0 - 1.2.11 + 1.3.12 2.0 3.0.20.Final 0.2.1 @@ -140,6 +140,9 @@ 1.14.5 3.9.0 2.0 + 3.25.1 + 1.70 + 0.1.35 @@ -539,6 +542,36 @@ snakeyaml ${snakeyaml_version} + + com.google.protobuf + protobuf-java + ${protobuf-java_version} + + + com.google.protobuf + protobuf-java-util + ${protobuf-java_version} + + + org.bouncycastle + bcprov-jdk15on + ${bouncycastle-bcprov_version} + + + org.bouncycastle + bcpkix-jdk15on + ${bouncycastle-bcprov_version} + + + org.bouncycastle + bcprov-ext-jdk15on + ${bouncycastle-bcprov_version} + + + io.envoyproxy.controlplane + api + ${envoy_api_version} + diff --git a/dubbo-kubernetes/pom.xml b/dubbo-kubernetes/pom.xml new file mode 100644 index 000000000..945072273 --- /dev/null +++ b/dubbo-kubernetes/pom.xml @@ -0,0 +1,86 @@ + + + + 4.0.0 + + org.apache.dubbo.extensions + extensions-parent + ${revision} + ../pom.xml + + + dubbo-kubernetes + ${project.artifactId} + The Kubernetes Integration + + false + + + + + + org.apache.dubbo + dubbo-bom + 3.2.9 + pom + import + + + + + + + org.apache.dubbo + dubbo-registry-api + true + + + org.apache.dubbo + dubbo-common + true + + + org.apache.dubbo + dubbo-metadata-api + true + + + io.fabric8 + kubernetes-client + + + io.fabric8 + kubernetes-server-mock + test + + + org.mockito + mockito-junit-jupiter + 3.12.4 + test + + + org.junit.jupiter + junit-jupiter-api + + + + + + diff --git a/dubbo-kubernetes/src/main/java/org/apache/dubbo/registry/kubernetes/KubernetesMeshEnvListener.java b/dubbo-kubernetes/src/main/java/org/apache/dubbo/registry/kubernetes/KubernetesMeshEnvListener.java new file mode 100644 index 000000000..476ba22c0 --- /dev/null +++ b/dubbo-kubernetes/src/main/java/org/apache/dubbo/registry/kubernetes/KubernetesMeshEnvListener.java @@ -0,0 +1,207 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.dubbo.registry.kubernetes; + +import org.apache.dubbo.common.logger.ErrorTypeAwareLogger; +import org.apache.dubbo.common.logger.LoggerFactory; +import org.apache.dubbo.rpc.cluster.router.mesh.route.MeshAppRuleListener; +import org.apache.dubbo.rpc.cluster.router.mesh.route.MeshEnvListener; + +import io.fabric8.kubernetes.api.model.GenericKubernetesResource; +import io.fabric8.kubernetes.client.KubernetesClient; +import io.fabric8.kubernetes.client.Watch; +import io.fabric8.kubernetes.client.Watcher; +import io.fabric8.kubernetes.client.WatcherException; +import org.yaml.snakeyaml.LoaderOptions; +import org.yaml.snakeyaml.Yaml; +import org.yaml.snakeyaml.constructor.SafeConstructor; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import static org.apache.dubbo.common.constants.LoggerCodeConstants.REGISTRY_ERROR_LISTEN_KUBERNETES; + +public class KubernetesMeshEnvListener implements MeshEnvListener { + public static final ErrorTypeAwareLogger logger = + LoggerFactory.getErrorTypeAwareLogger(KubernetesMeshEnvListener.class); + private static volatile boolean usingApiServer = false; + private static volatile KubernetesClient kubernetesClient; + private static volatile String namespace; + + private final Map appRuleListenerMap = new ConcurrentHashMap<>(); + + private final Map vsAppWatch = new ConcurrentHashMap<>(); + private final Map drAppWatch = new ConcurrentHashMap<>(); + + private final Map vsAppCache = new ConcurrentHashMap<>(); + private final Map drAppCache = new ConcurrentHashMap<>(); + + public static void injectKubernetesEnv(KubernetesClient client, String configuredNamespace) { + usingApiServer = true; + kubernetesClient = client; + namespace = configuredNamespace; + } + + @Override + public boolean isEnable() { + return usingApiServer; + } + + @Override + public void onSubscribe(String appName, MeshAppRuleListener listener) { + appRuleListenerMap.put(appName, listener); + logger.info("Subscribe Mesh Rule in Kubernetes. AppName: " + appName); + + // subscribe VisualService + subscribeVs(appName); + + // subscribe DestinationRule + subscribeDr(appName); + + // notify for start + notifyOnce(appName); + } + + private void subscribeVs(String appName) { + if (vsAppWatch.containsKey(appName)) { + return; + } + + try { + Watch watch = kubernetesClient + .genericKubernetesResources(MeshConstant.getVsDefinition()) + .inNamespace(namespace) + .withName(appName) + .watch(new Watcher() { + @Override + public void eventReceived(Action action, GenericKubernetesResource resource) { + if (logger.isInfoEnabled()) { + logger.info("Received VS Rule notification. AppName: " + appName + " Action:" + action + + " Resource:" + resource); + } + + if (action == Action.ADDED || action == Action.MODIFIED) { + String vsRule = new Yaml(new SafeConstructor(new LoaderOptions())).dump(resource); + vsAppCache.put(appName, vsRule); + if (drAppCache.containsKey(appName)) { + notifyListener(vsRule, appName, drAppCache.get(appName)); + } + } else { + appRuleListenerMap.get(appName).receiveConfigInfo(""); + } + } + + @Override + public void onClose(WatcherException cause) { + // ignore + } + }); + vsAppWatch.put(appName, watch); + try { + GenericKubernetesResource vsRule = kubernetesClient + .genericKubernetesResources(MeshConstant.getVsDefinition()) + .inNamespace(namespace) + .withName(appName) + .get(); + vsAppCache.put(appName, new Yaml(new SafeConstructor(new LoaderOptions())).dump(vsRule)); + } catch (Throwable ignore) { + + } + } catch (Exception e) { + logger.error(REGISTRY_ERROR_LISTEN_KUBERNETES, "", "", "Error occurred when listen kubernetes crd.", e); + } + } + + private void notifyListener(String vsRule, String appName, String drRule) { + String rule = vsRule + "\n---\n" + drRule; + logger.info("Notify App Rule Listener. AppName: " + appName + " Rule:" + rule); + + appRuleListenerMap.get(appName).receiveConfigInfo(rule); + } + + private void subscribeDr(String appName) { + if (drAppWatch.containsKey(appName)) { + return; + } + + try { + Watch watch = kubernetesClient + .genericKubernetesResources(MeshConstant.getDrDefinition()) + .inNamespace(namespace) + .withName(appName) + .watch(new Watcher() { + @Override + public void eventReceived(Action action, GenericKubernetesResource resource) { + if (logger.isInfoEnabled()) { + logger.info("Received VS Rule notification. AppName: " + appName + " Action:" + action + + " Resource:" + resource); + } + + if (action == Action.ADDED || action == Action.MODIFIED) { + String drRule = new Yaml(new SafeConstructor(new LoaderOptions())).dump(resource); + + drAppCache.put(appName, drRule); + if (vsAppCache.containsKey(appName)) { + notifyListener(vsAppCache.get(appName), appName, drRule); + } + } else { + appRuleListenerMap.get(appName).receiveConfigInfo(""); + } + } + + @Override + public void onClose(WatcherException cause) { + // ignore + } + }); + drAppWatch.put(appName, watch); + try { + GenericKubernetesResource drRule = kubernetesClient + .genericKubernetesResources(MeshConstant.getDrDefinition()) + .inNamespace(namespace) + .withName(appName) + .get(); + drAppCache.put(appName, new Yaml(new SafeConstructor(new LoaderOptions())).dump(drRule)); + } catch (Throwable ignore) { + + } + } catch (Exception e) { + logger.error(REGISTRY_ERROR_LISTEN_KUBERNETES, "", "", "Error occurred when listen kubernetes crd.", e); + } + } + + private void notifyOnce(String appName) { + if (vsAppCache.containsKey(appName) && drAppCache.containsKey(appName)) { + notifyListener(vsAppCache.get(appName), appName, drAppCache.get(appName)); + } + } + + @Override + public void onUnSubscribe(String appName) { + appRuleListenerMap.remove(appName); + + if (vsAppWatch.containsKey(appName)) { + vsAppWatch.remove(appName).close(); + } + vsAppCache.remove(appName); + + if (drAppWatch.containsKey(appName)) { + drAppWatch.remove(appName).close(); + } + drAppCache.remove(appName); + } +} diff --git a/dubbo-kubernetes/src/main/java/org/apache/dubbo/registry/kubernetes/KubernetesMeshEnvListenerFactory.java b/dubbo-kubernetes/src/main/java/org/apache/dubbo/registry/kubernetes/KubernetesMeshEnvListenerFactory.java new file mode 100644 index 000000000..9d1c6d06c --- /dev/null +++ b/dubbo-kubernetes/src/main/java/org/apache/dubbo/registry/kubernetes/KubernetesMeshEnvListenerFactory.java @@ -0,0 +1,42 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.dubbo.registry.kubernetes; + +import org.apache.dubbo.common.logger.Logger; +import org.apache.dubbo.common.logger.LoggerFactory; +import org.apache.dubbo.rpc.cluster.router.mesh.route.MeshEnvListener; +import org.apache.dubbo.rpc.cluster.router.mesh.route.MeshEnvListenerFactory; + +import java.util.concurrent.atomic.AtomicBoolean; + +public class KubernetesMeshEnvListenerFactory implements MeshEnvListenerFactory { + public static final Logger logger = LoggerFactory.getLogger(KubernetesMeshEnvListenerFactory.class); + private final AtomicBoolean initialized = new AtomicBoolean(false); + private MeshEnvListener listener = null; + + @Override + public MeshEnvListener getListener() { + try { + if (initialized.compareAndSet(false, true)) { + listener = new NopKubernetesMeshEnvListener(); + } + } catch (Throwable t) { + logger.info("Current Env not support Kubernetes."); + } + return listener; + } +} diff --git a/dubbo-kubernetes/src/main/java/org/apache/dubbo/registry/kubernetes/KubernetesRegistry.java b/dubbo-kubernetes/src/main/java/org/apache/dubbo/registry/kubernetes/KubernetesRegistry.java new file mode 100644 index 000000000..2d51c1bfa --- /dev/null +++ b/dubbo-kubernetes/src/main/java/org/apache/dubbo/registry/kubernetes/KubernetesRegistry.java @@ -0,0 +1,50 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.dubbo.registry.kubernetes; + +import org.apache.dubbo.common.URL; +import org.apache.dubbo.registry.NotifyListener; +import org.apache.dubbo.registry.support.FailbackRegistry; + +/** + * Empty implements for Kubernetes
+ * Kubernetes only support `Service Discovery` mode register
+ * Used to compat past version like 2.6.x, 2.7.x with interface level register
+ * {@link KubernetesServiceDiscovery} is the real implementation of Kubernetes + */ +public class KubernetesRegistry extends FailbackRegistry { + public KubernetesRegistry(URL url) { + super(url); + } + + @Override + public boolean isAvailable() { + return true; + } + + @Override + public void doRegister(URL url) {} + + @Override + public void doUnregister(URL url) {} + + @Override + public void doSubscribe(URL url, NotifyListener listener) {} + + @Override + public void doUnsubscribe(URL url, NotifyListener listener) {} +} diff --git a/dubbo-kubernetes/src/main/java/org/apache/dubbo/registry/kubernetes/KubernetesRegistryFactory.java b/dubbo-kubernetes/src/main/java/org/apache/dubbo/registry/kubernetes/KubernetesRegistryFactory.java new file mode 100644 index 000000000..fe0e0477d --- /dev/null +++ b/dubbo-kubernetes/src/main/java/org/apache/dubbo/registry/kubernetes/KubernetesRegistryFactory.java @@ -0,0 +1,34 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.dubbo.registry.kubernetes; + +import org.apache.dubbo.common.URL; +import org.apache.dubbo.registry.Registry; +import org.apache.dubbo.registry.support.AbstractRegistryFactory; + +public class KubernetesRegistryFactory extends AbstractRegistryFactory { + + @Override + protected String createRegistryCacheKey(URL url) { + return url.toFullString(); + } + + @Override + protected Registry createRegistry(URL url) { + return new KubernetesRegistry(url); + } +} diff --git a/dubbo-kubernetes/src/main/java/org/apache/dubbo/registry/kubernetes/KubernetesServiceDiscovery.java b/dubbo-kubernetes/src/main/java/org/apache/dubbo/registry/kubernetes/KubernetesServiceDiscovery.java new file mode 100644 index 000000000..396ef4bdf --- /dev/null +++ b/dubbo-kubernetes/src/main/java/org/apache/dubbo/registry/kubernetes/KubernetesServiceDiscovery.java @@ -0,0 +1,451 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.dubbo.registry.kubernetes; + +import org.apache.dubbo.common.URL; +import org.apache.dubbo.common.logger.ErrorTypeAwareLogger; +import org.apache.dubbo.common.logger.LoggerFactory; +import org.apache.dubbo.common.utils.JsonUtils; +import org.apache.dubbo.common.utils.StringUtils; +import org.apache.dubbo.registry.client.AbstractServiceDiscovery; +import org.apache.dubbo.registry.client.DefaultServiceInstance; +import org.apache.dubbo.registry.client.ServiceInstance; +import org.apache.dubbo.registry.client.event.ServiceInstancesChangedEvent; +import org.apache.dubbo.registry.client.event.listener.ServiceInstancesChangedListener; +import org.apache.dubbo.registry.kubernetes.util.KubernetesClientConst; +import org.apache.dubbo.registry.kubernetes.util.KubernetesConfigUtils; +import org.apache.dubbo.rpc.model.ApplicationModel; +import org.apache.dubbo.rpc.model.ScopeModelUtil; + +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.api.model.Pod; +import io.fabric8.kubernetes.api.model.PodBuilder; +import io.fabric8.kubernetes.api.model.Service; +import io.fabric8.kubernetes.client.Config; +import io.fabric8.kubernetes.client.KubernetesClient; +import io.fabric8.kubernetes.client.KubernetesClientBuilder; +import io.fabric8.kubernetes.client.informers.ResourceEventHandler; +import io.fabric8.kubernetes.client.informers.SharedIndexInformer; + +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicLong; +import java.util.stream.Collectors; + +import static org.apache.dubbo.common.constants.LoggerCodeConstants.REGISTRY_UNABLE_ACCESS_KUBERNETES; +import static org.apache.dubbo.common.constants.LoggerCodeConstants.REGISTRY_UNABLE_FIND_SERVICE_KUBERNETES; +import static org.apache.dubbo.common.constants.LoggerCodeConstants.REGISTRY_UNABLE_MATCH_KUBERNETES; + +public class KubernetesServiceDiscovery extends AbstractServiceDiscovery { + private final ErrorTypeAwareLogger logger = LoggerFactory.getErrorTypeAwareLogger(getClass()); + + private KubernetesClient kubernetesClient; + + private String currentHostname; + + private final URL registryURL; + + private final String namespace; + + private final boolean enableRegister; + + public static final String KUBERNETES_PROPERTIES_KEY = "io.dubbo/metadata"; + + private static final ConcurrentHashMap SERVICE_UPDATE_TIME = new ConcurrentHashMap<>(64); + + private static final ConcurrentHashMap> SERVICE_INFORMER = + new ConcurrentHashMap<>(64); + + private static final ConcurrentHashMap> PODS_INFORMER = + new ConcurrentHashMap<>(64); + + private static final ConcurrentHashMap> ENDPOINTS_INFORMER = + new ConcurrentHashMap<>(64); + + public KubernetesServiceDiscovery(ApplicationModel applicationModel, URL registryURL) { + super(applicationModel, registryURL); + Config config = KubernetesConfigUtils.createKubernetesConfig(registryURL); + this.kubernetesClient = new KubernetesClientBuilder().withConfig(config).build(); + this.currentHostname = System.getenv("HOSTNAME"); + this.registryURL = registryURL; + this.namespace = config.getNamespace(); + this.enableRegister = registryURL.getParameter(KubernetesClientConst.ENABLE_REGISTER, true); + + boolean availableAccess; + try { + availableAccess = kubernetesClient.pods().withName(currentHostname).get() != null; + } catch (Throwable e) { + availableAccess = false; + } + if (!availableAccess) { + String message = "Unable to access api server. " + "Please check your url config." + + " Master URL: " + + config.getMasterUrl() + " Hostname: " + + currentHostname; + logger.error(REGISTRY_UNABLE_ACCESS_KUBERNETES, "", "", message); + } else { + KubernetesMeshEnvListener.injectKubernetesEnv(kubernetesClient, namespace); + } + } + + @Override + public void doDestroy() { + SERVICE_INFORMER.forEach((k, v) -> v.close()); + SERVICE_INFORMER.clear(); + + PODS_INFORMER.forEach((k, v) -> v.close()); + PODS_INFORMER.clear(); + + ENDPOINTS_INFORMER.forEach((k, v) -> v.close()); + ENDPOINTS_INFORMER.clear(); + + kubernetesClient.close(); + } + + @Override + public void doRegister(ServiceInstance serviceInstance) throws RuntimeException { + if (enableRegister) { + kubernetesClient + .pods() + .inNamespace(namespace) + .withName(currentHostname) + .edit(pod -> new PodBuilder(pod) + .editOrNewMetadata() + .addToAnnotations( + KUBERNETES_PROPERTIES_KEY, JsonUtils.toJson(serviceInstance.getMetadata())) + .endMetadata() + .build()); + if (logger.isInfoEnabled()) { + logger.info("Write Current Service Instance Metadata to Kubernetes pod. " + "Current pod name: " + + currentHostname); + } + } + } + + /** + * Comparing to {@link AbstractServiceDiscovery#doUpdate(ServiceInstance, ServiceInstance)}, unregister() is unnecessary here. + */ + @Override + public void doUpdate(ServiceInstance oldServiceInstance, ServiceInstance newServiceInstance) + throws RuntimeException { + reportMetadata(newServiceInstance.getServiceMetadata()); + this.doRegister(newServiceInstance); + } + + @Override + public void doUnregister(ServiceInstance serviceInstance) throws RuntimeException { + if (enableRegister) { + kubernetesClient + .pods() + .inNamespace(namespace) + .withName(currentHostname) + .edit(pod -> new PodBuilder(pod) + .editOrNewMetadata() + .removeFromAnnotations(KUBERNETES_PROPERTIES_KEY) + .endMetadata() + .build()); + if (logger.isInfoEnabled()) { + logger.info( + "Remove Current Service Instance from Kubernetes pod. Current pod name: " + currentHostname); + } + } + } + + @Override + public Set getServices() { + return kubernetesClient.services().inNamespace(namespace).list().getItems().stream() + .map(service -> service.getMetadata().getName()) + .collect(Collectors.toSet()); + } + + @Override + public List getInstances(String serviceName) throws NullPointerException { + Endpoints endpoints = null; + SharedIndexInformer endInformer = ENDPOINTS_INFORMER.get(serviceName); + if (endInformer != null) { + // get endpoints directly from informer local store + List endpointsList = endInformer.getStore().list(); + if (endpointsList.size() > 0) { + endpoints = endpointsList.get(0); + } + } + if (endpoints == null) { + endpoints = kubernetesClient + .endpoints() + .inNamespace(namespace) + .withName(serviceName) + .get(); + } + + return toServiceInstance(endpoints, serviceName); + } + + @Override + public void addServiceInstancesChangedListener(ServiceInstancesChangedListener listener) + throws NullPointerException, IllegalArgumentException { + listener.getServiceNames().forEach(serviceName -> { + SERVICE_UPDATE_TIME.put(serviceName, new AtomicLong(0L)); + + // Watch Service Endpoint Modification + watchEndpoints(listener, serviceName); + + // Watch Pods Modification, happens when ServiceInstance updated + watchPods(listener, serviceName); + + // Watch Service Modification, happens when Service Selector updated, used to update pods watcher + watchService(listener, serviceName); + }); + } + + private void watchEndpoints(ServiceInstancesChangedListener listener, String serviceName) { + SharedIndexInformer endInformer = kubernetesClient + .endpoints() + .inNamespace(namespace) + .withName(serviceName) + .inform(new ResourceEventHandler() { + @Override + public void onAdd(Endpoints endpoints) { + if (logger.isDebugEnabled()) { + logger.debug("Received Endpoint Event. Event type: added. Current pod name: " + + currentHostname + ". Endpoints is: " + endpoints); + } + notifyServiceChanged(serviceName, listener, toServiceInstance(endpoints, serviceName)); + } + + @Override + public void onUpdate(Endpoints oldEndpoints, Endpoints newEndpoints) { + if (logger.isDebugEnabled()) { + logger.debug("Received Endpoint Event. Event type: updated. Current pod name: " + + currentHostname + ". The new Endpoints is: " + newEndpoints); + } + notifyServiceChanged(serviceName, listener, toServiceInstance(newEndpoints, serviceName)); + } + + @Override + public void onDelete(Endpoints endpoints, boolean deletedFinalStateUnknown) { + if (logger.isDebugEnabled()) { + logger.debug("Received Endpoint Event. Event type: deleted. Current pod name: " + + currentHostname + ". Endpoints is: " + endpoints); + } + notifyServiceChanged(serviceName, listener, toServiceInstance(endpoints, serviceName)); + } + }); + + ENDPOINTS_INFORMER.put(serviceName, endInformer); + } + + private void watchPods(ServiceInstancesChangedListener listener, String serviceName) { + Map serviceSelector = getServiceSelector(serviceName); + if (serviceSelector == null) { + return; + } + + SharedIndexInformer podInformer = kubernetesClient + .pods() + .inNamespace(namespace) + .withLabels(serviceSelector) + .inform(new ResourceEventHandler() { + @Override + public void onAdd(Pod pod) { + if (logger.isDebugEnabled()) { + logger.debug("Received Pods Event. Event type: added. Current pod name: " + currentHostname + + ". Pod is: " + pod); + } + } + + @Override + public void onUpdate(Pod oldPod, Pod newPod) { + if (logger.isDebugEnabled()) { + logger.debug("Received Pods Event. Event type: updated. Current pod name: " + + currentHostname + ". new Pod is: " + newPod); + } + + notifyServiceChanged(serviceName, listener, getInstances(serviceName)); + } + + @Override + public void onDelete(Pod pod, boolean deletedFinalStateUnknown) { + if (logger.isDebugEnabled()) { + logger.debug("Received Pods Event. Event type: deleted. Current pod name: " + + currentHostname + ". Pod is: " + pod); + } + } + }); + + PODS_INFORMER.put(serviceName, podInformer); + } + + private void watchService(ServiceInstancesChangedListener listener, String serviceName) { + SharedIndexInformer serviceInformer = kubernetesClient + .services() + .inNamespace(namespace) + .withName(serviceName) + .inform(new ResourceEventHandler() { + @Override + public void onAdd(Service service) { + if (logger.isDebugEnabled()) { + logger.debug("Received Service Added Event. " + "Current pod name: " + currentHostname); + } + } + + @Override + public void onUpdate(Service oldService, Service newService) { + if (logger.isDebugEnabled()) { + logger.debug("Received Service Update Event. Update Pods Watcher. Current pod name: " + + currentHostname + ". The new Service is: " + newService); + } + if (PODS_INFORMER.containsKey(serviceName)) { + PODS_INFORMER.get(serviceName).close(); + PODS_INFORMER.remove(serviceName); + } + watchPods(listener, serviceName); + } + + @Override + public void onDelete(Service service, boolean deletedFinalStateUnknown) { + if (logger.isDebugEnabled()) { + logger.debug("Received Service Delete Event. " + "Current pod name: " + currentHostname); + } + } + }); + + SERVICE_INFORMER.put(serviceName, serviceInformer); + } + + private void notifyServiceChanged( + String serviceName, ServiceInstancesChangedListener listener, List serviceInstanceList) { + long receivedTime = System.nanoTime(); + + ServiceInstancesChangedEvent event; + + event = new ServiceInstancesChangedEvent(serviceName, serviceInstanceList); + + AtomicLong updateTime = SERVICE_UPDATE_TIME.get(serviceName); + long lastUpdateTime = updateTime.get(); + + if (lastUpdateTime <= receivedTime) { + if (updateTime.compareAndSet(lastUpdateTime, receivedTime)) { + listener.onEvent(event); + return; + } + } + + if (logger.isInfoEnabled()) { + logger.info("Discard Service Instance Data. " + + "Possible Cause: Newer message has been processed or Failed to update time record by CAS. " + + "Current Data received time: " + + receivedTime + ". " + "Newer Data received time: " + + lastUpdateTime + "."); + } + } + + @Override + public URL getUrl() { + return registryURL; + } + + private Map getServiceSelector(String serviceName) { + Service service = kubernetesClient + .services() + .inNamespace(namespace) + .withName(serviceName) + .get(); + if (service == null) { + return null; + } + return service.getSpec().getSelector(); + } + + private List toServiceInstance(Endpoints endpoints, String serviceName) { + Map serviceSelector = getServiceSelector(serviceName); + if (serviceSelector == null) { + return new LinkedList<>(); + } + Map pods = + kubernetesClient.pods().inNamespace(namespace).withLabels(serviceSelector).list().getItems().stream() + .collect(Collectors.toMap(pod -> pod.getMetadata().getName(), pod -> pod)); + + List instances = new LinkedList<>(); + Set instancePorts = new HashSet<>(); + + for (EndpointSubset endpointSubset : endpoints.getSubsets()) { + instancePorts.addAll(endpointSubset.getPorts().stream() + .map(EndpointPort::getPort) + .collect(Collectors.toSet())); + } + + for (EndpointSubset endpointSubset : endpoints.getSubsets()) { + for (EndpointAddress address : endpointSubset.getAddresses()) { + Pod pod = pods.get(address.getTargetRef().getName()); + String ip = address.getIp(); + if (pod == null) { + logger.warn( + REGISTRY_UNABLE_MATCH_KUBERNETES, + "", + "", + "Unable to match Kubernetes Endpoint address with Pod. " + "EndpointAddress Hostname: " + + address.getTargetRef().getName()); + continue; + } + instancePorts.forEach(port -> { + ServiceInstance serviceInstance = new DefaultServiceInstance( + serviceName, ip, port, ScopeModelUtil.getApplicationModel(getUrl().getScopeModel())); + + String properties = pod.getMetadata().getAnnotations().get(KUBERNETES_PROPERTIES_KEY); + if (StringUtils.isNotEmpty(properties)) { + serviceInstance.getMetadata().putAll(JsonUtils.toJavaObject(properties, Map.class)); + instances.add(serviceInstance); + } else { + logger.warn( + REGISTRY_UNABLE_FIND_SERVICE_KUBERNETES, + "", + "", + "Unable to find Service Instance metadata in Pod Annotations. " + + "Possibly cause: provider has not been initialized successfully. " + + "EndpointAddress Hostname: " + + address.getTargetRef().getName()); + } + }); + } + } + + return instances; + } + + /** + * UT used only + */ + @Deprecated + public void setCurrentHostname(String currentHostname) { + this.currentHostname = currentHostname; + } + + /** + * UT used only + */ + @Deprecated + public void setKubernetesClient(KubernetesClient kubernetesClient) { + this.kubernetesClient = kubernetesClient; + } +} diff --git a/dubbo-kubernetes/src/main/java/org/apache/dubbo/registry/kubernetes/KubernetesServiceDiscoveryFactory.java b/dubbo-kubernetes/src/main/java/org/apache/dubbo/registry/kubernetes/KubernetesServiceDiscoveryFactory.java new file mode 100644 index 000000000..7d11dfaaa --- /dev/null +++ b/dubbo-kubernetes/src/main/java/org/apache/dubbo/registry/kubernetes/KubernetesServiceDiscoveryFactory.java @@ -0,0 +1,28 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.dubbo.registry.kubernetes; + +import org.apache.dubbo.common.URL; +import org.apache.dubbo.registry.client.AbstractServiceDiscoveryFactory; +import org.apache.dubbo.registry.client.ServiceDiscovery; + +public class KubernetesServiceDiscoveryFactory extends AbstractServiceDiscoveryFactory { + @Override + protected ServiceDiscovery createDiscovery(URL registryURL) { + return new KubernetesServiceDiscovery(applicationModel, registryURL); + } +} diff --git a/dubbo-kubernetes/src/main/java/org/apache/dubbo/registry/kubernetes/MeshConstant.java b/dubbo-kubernetes/src/main/java/org/apache/dubbo/registry/kubernetes/MeshConstant.java new file mode 100644 index 000000000..e8b8f8407 --- /dev/null +++ b/dubbo-kubernetes/src/main/java/org/apache/dubbo/registry/kubernetes/MeshConstant.java @@ -0,0 +1,45 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.dubbo.registry.kubernetes; + +import io.fabric8.kubernetes.client.dsl.base.CustomResourceDefinitionContext; + +public class MeshConstant { + public static CustomResourceDefinitionContext getVsDefinition() { + // TODO cache + return new CustomResourceDefinitionContext.Builder() + .withGroup("service.dubbo.apache.org") + .withVersion("v1alpha1") + .withScope("Namespaced") + .withName("virtualservices.service.dubbo.apache.org") + .withPlural("virtualservices") + .withKind("VirtualService") + .build(); + } + + public static CustomResourceDefinitionContext getDrDefinition() { + // TODO cache + return new CustomResourceDefinitionContext.Builder() + .withGroup("service.dubbo.apache.org") + .withVersion("v1alpha1") + .withScope("Namespaced") + .withName("destinationrules.service.dubbo.apache.org") + .withPlural("destinationrules") + .withKind("DestinationRule") + .build(); + } +} diff --git a/dubbo-kubernetes/src/main/java/org/apache/dubbo/registry/kubernetes/NopKubernetesMeshEnvListener.java b/dubbo-kubernetes/src/main/java/org/apache/dubbo/registry/kubernetes/NopKubernetesMeshEnvListener.java new file mode 100644 index 000000000..818b8df64 --- /dev/null +++ b/dubbo-kubernetes/src/main/java/org/apache/dubbo/registry/kubernetes/NopKubernetesMeshEnvListener.java @@ -0,0 +1,34 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.dubbo.registry.kubernetes; + +import org.apache.dubbo.rpc.cluster.router.mesh.route.MeshAppRuleListener; +import org.apache.dubbo.rpc.cluster.router.mesh.route.MeshEnvListener; + +public class NopKubernetesMeshEnvListener implements MeshEnvListener { + + @Override + public boolean isEnable() { + return false; + } + + @Override + public void onSubscribe(String appName, MeshAppRuleListener listener) {} + + @Override + public void onUnSubscribe(String appName) {} +} diff --git a/dubbo-kubernetes/src/main/java/org/apache/dubbo/registry/kubernetes/util/KubernetesClientConst.java b/dubbo-kubernetes/src/main/java/org/apache/dubbo/registry/kubernetes/util/KubernetesClientConst.java new file mode 100644 index 000000000..b7ace5389 --- /dev/null +++ b/dubbo-kubernetes/src/main/java/org/apache/dubbo/registry/kubernetes/util/KubernetesClientConst.java @@ -0,0 +1,78 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.dubbo.registry.kubernetes.util; + +public class KubernetesClientConst { + public static final String DEFAULT_MASTER_PLACEHOLDER = "DEFAULT_MASTER_HOST"; + public static final String DEFAULT_MASTER_URL = "https://kubernetes.default.svc"; + + public static final String ENABLE_REGISTER = "enableRegister"; + + public static final String TRUST_CERTS = "trustCerts"; + + public static final String USE_HTTPS = "useHttps"; + + public static final String HTTP2_DISABLE = "http2Disable"; + + public static final String NAMESPACE = "namespace"; + + public static final String API_VERSION = "apiVersion"; + + public static final String CA_CERT_FILE = "caCertFile"; + + public static final String CA_CERT_DATA = "caCertData"; + + public static final String CLIENT_CERT_FILE = "clientCertFile"; + + public static final String CLIENT_CERT_DATA = "clientCertData"; + + public static final String CLIENT_KEY_FILE = "clientKeyFile"; + + public static final String CLIENT_KEY_DATA = "clientKeyData"; + + public static final String CLIENT_KEY_ALGO = "clientKeyAlgo"; + + public static final String CLIENT_KEY_PASSPHRASE = "clientKeyPassphrase"; + + public static final String OAUTH_TOKEN = "oauthToken"; + + public static final String USERNAME = "username"; + + public static final String PASSWORD = "password"; + + public static final String WATCH_RECONNECT_INTERVAL = "watchReconnectInterval"; + + public static final String WATCH_RECONNECT_LIMIT = "watchReconnectLimit"; + + public static final String CONNECTION_TIMEOUT = "connectionTimeout"; + + public static final String REQUEST_TIMEOUT = "requestTimeout"; + + public static final String ROLLING_TIMEOUT = "rollingTimeout"; + + public static final String LOGGING_INTERVAL = "loggingInterval"; + + public static final String HTTP_PROXY = "httpProxy"; + + public static final String HTTPS_PROXY = "httpsProxy"; + + public static final String PROXY_USERNAME = "proxyUsername"; + + public static final String PROXY_PASSWORD = "proxyPassword"; + + public static final String NO_PROXY = "noProxy"; +} diff --git a/dubbo-kubernetes/src/main/java/org/apache/dubbo/registry/kubernetes/util/KubernetesConfigUtils.java b/dubbo-kubernetes/src/main/java/org/apache/dubbo/registry/kubernetes/util/KubernetesConfigUtils.java new file mode 100644 index 000000000..1d29884e0 --- /dev/null +++ b/dubbo-kubernetes/src/main/java/org/apache/dubbo/registry/kubernetes/util/KubernetesConfigUtils.java @@ -0,0 +1,104 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.dubbo.registry.kubernetes.util; + +import org.apache.dubbo.common.URL; +import org.apache.dubbo.common.utils.StringUtils; + +import io.fabric8.kubernetes.client.Config; +import io.fabric8.kubernetes.client.ConfigBuilder; + +import java.util.Base64; + +import static org.apache.dubbo.registry.kubernetes.util.KubernetesClientConst.API_VERSION; +import static org.apache.dubbo.registry.kubernetes.util.KubernetesClientConst.CA_CERT_DATA; +import static org.apache.dubbo.registry.kubernetes.util.KubernetesClientConst.CA_CERT_FILE; +import static org.apache.dubbo.registry.kubernetes.util.KubernetesClientConst.CLIENT_CERT_DATA; +import static org.apache.dubbo.registry.kubernetes.util.KubernetesClientConst.CLIENT_CERT_FILE; +import static org.apache.dubbo.registry.kubernetes.util.KubernetesClientConst.CLIENT_KEY_ALGO; +import static org.apache.dubbo.registry.kubernetes.util.KubernetesClientConst.CLIENT_KEY_DATA; +import static org.apache.dubbo.registry.kubernetes.util.KubernetesClientConst.CLIENT_KEY_FILE; +import static org.apache.dubbo.registry.kubernetes.util.KubernetesClientConst.CLIENT_KEY_PASSPHRASE; +import static org.apache.dubbo.registry.kubernetes.util.KubernetesClientConst.CONNECTION_TIMEOUT; +import static org.apache.dubbo.registry.kubernetes.util.KubernetesClientConst.DEFAULT_MASTER_PLACEHOLDER; +import static org.apache.dubbo.registry.kubernetes.util.KubernetesClientConst.DEFAULT_MASTER_URL; +import static org.apache.dubbo.registry.kubernetes.util.KubernetesClientConst.HTTP2_DISABLE; +import static org.apache.dubbo.registry.kubernetes.util.KubernetesClientConst.HTTPS_PROXY; +import static org.apache.dubbo.registry.kubernetes.util.KubernetesClientConst.HTTP_PROXY; +import static org.apache.dubbo.registry.kubernetes.util.KubernetesClientConst.LOGGING_INTERVAL; +import static org.apache.dubbo.registry.kubernetes.util.KubernetesClientConst.NAMESPACE; +import static org.apache.dubbo.registry.kubernetes.util.KubernetesClientConst.NO_PROXY; +import static org.apache.dubbo.registry.kubernetes.util.KubernetesClientConst.OAUTH_TOKEN; +import static org.apache.dubbo.registry.kubernetes.util.KubernetesClientConst.PASSWORD; +import static org.apache.dubbo.registry.kubernetes.util.KubernetesClientConst.PROXY_PASSWORD; +import static org.apache.dubbo.registry.kubernetes.util.KubernetesClientConst.PROXY_USERNAME; +import static org.apache.dubbo.registry.kubernetes.util.KubernetesClientConst.REQUEST_TIMEOUT; +import static org.apache.dubbo.registry.kubernetes.util.KubernetesClientConst.TRUST_CERTS; +import static org.apache.dubbo.registry.kubernetes.util.KubernetesClientConst.USERNAME; +import static org.apache.dubbo.registry.kubernetes.util.KubernetesClientConst.USE_HTTPS; +import static org.apache.dubbo.registry.kubernetes.util.KubernetesClientConst.WATCH_RECONNECT_INTERVAL; +import static org.apache.dubbo.registry.kubernetes.util.KubernetesClientConst.WATCH_RECONNECT_LIMIT; + +public class KubernetesConfigUtils { + + public static Config createKubernetesConfig(URL url) { + // Init default config + Config base = Config.autoConfigure(null); + + // replace config with parameters if presents + return new ConfigBuilder(base) // + .withMasterUrl(buildMasterUrl(url)) // + .withApiVersion(url.getParameter(API_VERSION, base.getApiVersion())) // + .withNamespace(url.getParameter(NAMESPACE, base.getNamespace())) // + .withUsername(url.getParameter(USERNAME, base.getUsername())) // + .withPassword(url.getParameter(PASSWORD, base.getPassword())) // + .withOauthToken(url.getParameter(OAUTH_TOKEN, base.getOauthToken())) // + .withCaCertFile(url.getParameter(CA_CERT_FILE, base.getCaCertFile())) // + .withCaCertData(url.getParameter(CA_CERT_DATA, decodeBase64(base.getCaCertData()))) // + .withClientKeyFile(url.getParameter(CLIENT_KEY_FILE, base.getClientKeyFile())) // + .withClientKeyData(url.getParameter(CLIENT_KEY_DATA, decodeBase64(base.getClientKeyData()))) // + .withClientCertFile(url.getParameter(CLIENT_CERT_FILE, base.getClientCertFile())) // + .withClientCertData(url.getParameter(CLIENT_CERT_DATA, decodeBase64(base.getClientCertData()))) // + .withClientKeyAlgo(url.getParameter(CLIENT_KEY_ALGO, base.getClientKeyAlgo())) // + .withClientKeyPassphrase(url.getParameter(CLIENT_KEY_PASSPHRASE, base.getClientKeyPassphrase())) // + .withConnectionTimeout(url.getParameter(CONNECTION_TIMEOUT, base.getConnectionTimeout())) // + .withRequestTimeout(url.getParameter(REQUEST_TIMEOUT, base.getRequestTimeout())) // + .withWatchReconnectInterval( + url.getParameter(WATCH_RECONNECT_INTERVAL, base.getWatchReconnectInterval())) // + .withWatchReconnectLimit(url.getParameter(WATCH_RECONNECT_LIMIT, base.getWatchReconnectLimit())) // + .withLoggingInterval(url.getParameter(LOGGING_INTERVAL, base.getLoggingInterval())) // + .withTrustCerts(url.getParameter(TRUST_CERTS, base.isTrustCerts())) // + .withHttp2Disable(url.getParameter(HTTP2_DISABLE, base.isHttp2Disable())) // + .withHttpProxy(url.getParameter(HTTP_PROXY, base.getHttpProxy())) // + .withHttpsProxy(url.getParameter(HTTPS_PROXY, base.getHttpsProxy())) // + .withProxyUsername(url.getParameter(PROXY_USERNAME, base.getProxyUsername())) // + .withProxyPassword(url.getParameter(PROXY_PASSWORD, base.getProxyPassword())) // + .withNoProxy(url.getParameter(NO_PROXY, base.getNoProxy())) // + .build(); + } + + private static String buildMasterUrl(URL url) { + if (DEFAULT_MASTER_PLACEHOLDER.equalsIgnoreCase(url.getHost())) { + return DEFAULT_MASTER_URL; + } + return (url.getParameter(USE_HTTPS, true) ? "https://" : "http://") + url.getHost() + ":" + url.getPort(); + } + + private static String decodeBase64(String str) { + return StringUtils.isNotEmpty(str) ? new String(Base64.getDecoder().decode(str)) : null; + } +} diff --git a/dubbo-kubernetes/src/main/resources/META-INF/dubbo/internal/org.apache.dubbo.registry.RegistryFactory b/dubbo-kubernetes/src/main/resources/META-INF/dubbo/internal/org.apache.dubbo.registry.RegistryFactory new file mode 100644 index 000000000..94177d81e --- /dev/null +++ b/dubbo-kubernetes/src/main/resources/META-INF/dubbo/internal/org.apache.dubbo.registry.RegistryFactory @@ -0,0 +1 @@ +kubernetes=org.apache.dubbo.registry.kubernetes.KubernetesRegistryFactory \ No newline at end of file diff --git a/dubbo-kubernetes/src/main/resources/META-INF/dubbo/internal/org.apache.dubbo.registry.client.ServiceDiscoveryFactory b/dubbo-kubernetes/src/main/resources/META-INF/dubbo/internal/org.apache.dubbo.registry.client.ServiceDiscoveryFactory new file mode 100644 index 000000000..4301ab8b4 --- /dev/null +++ b/dubbo-kubernetes/src/main/resources/META-INF/dubbo/internal/org.apache.dubbo.registry.client.ServiceDiscoveryFactory @@ -0,0 +1 @@ +kubernetes=org.apache.dubbo.registry.kubernetes.KubernetesServiceDiscoveryFactory \ No newline at end of file diff --git a/dubbo-kubernetes/src/main/resources/META-INF/dubbo/internal/org.apache.dubbo.rpc.cluster.router.mesh.route.MeshEnvListenerFactory b/dubbo-kubernetes/src/main/resources/META-INF/dubbo/internal/org.apache.dubbo.rpc.cluster.router.mesh.route.MeshEnvListenerFactory new file mode 100644 index 000000000..4dfae84b8 --- /dev/null +++ b/dubbo-kubernetes/src/main/resources/META-INF/dubbo/internal/org.apache.dubbo.rpc.cluster.router.mesh.route.MeshEnvListenerFactory @@ -0,0 +1 @@ +kubernetes=org.apache.dubbo.registry.kubernetes.KubernetesMeshEnvListenerFactory diff --git a/dubbo-kubernetes/src/test/java/org/apache/dubbo/registry/kubernetes/KubernetesServiceDiscoveryTest.java b/dubbo-kubernetes/src/test/java/org/apache/dubbo/registry/kubernetes/KubernetesServiceDiscoveryTest.java new file mode 100644 index 000000000..5f6913451 --- /dev/null +++ b/dubbo-kubernetes/src/test/java/org/apache/dubbo/registry/kubernetes/KubernetesServiceDiscoveryTest.java @@ -0,0 +1,289 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.dubbo.registry.kubernetes; + +import org.apache.dubbo.common.URL; +import org.apache.dubbo.config.ApplicationConfig; +import org.apache.dubbo.registry.client.DefaultServiceInstance; +import org.apache.dubbo.registry.client.ServiceInstance; +import org.apache.dubbo.registry.client.event.ServiceInstancesChangedEvent; +import org.apache.dubbo.registry.client.event.listener.ServiceInstancesChangedListener; +import org.apache.dubbo.registry.kubernetes.util.KubernetesClientConst; +import org.apache.dubbo.rpc.model.ApplicationModel; +import org.apache.dubbo.rpc.model.ScopeModelUtil; + +import io.fabric8.kubernetes.api.model.Endpoints; +import io.fabric8.kubernetes.api.model.EndpointsBuilder; +import io.fabric8.kubernetes.api.model.Pod; +import io.fabric8.kubernetes.api.model.PodBuilder; +import io.fabric8.kubernetes.api.model.Service; +import io.fabric8.kubernetes.api.model.ServiceBuilder; +import io.fabric8.kubernetes.client.Config; +import io.fabric8.kubernetes.client.NamespacedKubernetesClient; +import io.fabric8.kubernetes.client.server.mock.KubernetesServer; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; + +import static org.apache.dubbo.registry.kubernetes.util.KubernetesClientConst.NAMESPACE; +import static org.awaitility.Awaitility.await; + +@ExtendWith({MockitoExtension.class}) +class KubernetesServiceDiscoveryTest { + + private static final String SERVICE_NAME = "TestService"; + + private static final String POD_NAME = "TestServer"; + + public KubernetesServer mockServer = new KubernetesServer(false, true); + + private NamespacedKubernetesClient mockClient; + + private ServiceInstancesChangedListener mockListener = Mockito.mock(ServiceInstancesChangedListener.class); + + private URL serverUrl; + + private Map selector; + + private KubernetesServiceDiscovery serviceDiscovery; + + @BeforeEach + public void setUp() { + mockServer.before(); + mockClient = mockServer.getClient().inNamespace("dubbo-demo"); + + ApplicationModel applicationModel = ApplicationModel.defaultModel(); + applicationModel.getApplicationConfigManager().setApplication(new ApplicationConfig()); + + serverUrl = URL.valueOf(mockClient.getConfiguration().getMasterUrl()) + .setProtocol("kubernetes") + .addParameter(NAMESPACE, "dubbo-demo") + .addParameter(KubernetesClientConst.USE_HTTPS, "false") + .addParameter(KubernetesClientConst.HTTP2_DISABLE, "true"); + serverUrl.setScopeModel(applicationModel); + + this.serviceDiscovery = new KubernetesServiceDiscovery(applicationModel, serverUrl); + + System.setProperty(Config.KUBERNETES_AUTH_TRYKUBECONFIG_SYSTEM_PROPERTY, "false"); + System.setProperty(Config.KUBERNETES_AUTH_TRYSERVICEACCOUNT_SYSTEM_PROPERTY, "false"); + + selector = new HashMap<>(4); + selector.put("l", "v"); + Pod pod = new PodBuilder() + .withNewMetadata() + .withName(POD_NAME) + .withLabels(selector) + .endMetadata() + .build(); + + Service service = new ServiceBuilder() + .withNewMetadata() + .withName(SERVICE_NAME) + .endMetadata() + .withNewSpec() + .withSelector(selector) + .endSpec() + .build(); + + Endpoints endPoints = new EndpointsBuilder() + .withNewMetadata() + .withName(SERVICE_NAME) + .endMetadata() + .addNewSubset() + .addNewAddress() + .withIp("ip1") + .withNewTargetRef() + .withUid("uid1") + .withName(POD_NAME) + .endTargetRef() + .endAddress() + .addNewPort("Test", "Test", 12345, "TCP") + .endSubset() + .build(); + + mockClient.pods().resource(pod).create(); + mockClient.services().resource(service).create(); + mockClient.endpoints().resource(endPoints).create(); + } + + @AfterEach + public void destroy() throws Exception { + serviceDiscovery.destroy(); + mockClient.close(); + mockServer.after(); + } + + @Test + void testEndpointsUpdate() { + serviceDiscovery.setCurrentHostname(POD_NAME); + serviceDiscovery.setKubernetesClient(mockClient); + + ServiceInstance serviceInstance = new DefaultServiceInstance( + SERVICE_NAME, + "Test", + 12345, + ScopeModelUtil.getApplicationModel(serviceDiscovery.getUrl().getScopeModel())); + + serviceDiscovery.doRegister(serviceInstance); + + HashSet serviceList = new HashSet<>(4); + serviceList.add(SERVICE_NAME); + Mockito.when(mockListener.getServiceNames()).thenReturn(serviceList); + Mockito.doNothing().when(mockListener).onEvent(Mockito.any()); + + serviceDiscovery.addServiceInstancesChangedListener(mockListener); + mockClient.endpoints().withName(SERVICE_NAME).edit(endpoints -> new EndpointsBuilder(endpoints) + .editFirstSubset() + .addNewAddress() + .withIp("ip2") + .withNewTargetRef() + .withUid("uid2") + .withName(POD_NAME) + .endTargetRef() + .endAddress() + .endSubset() + .build()); + + await().until(() -> { + ArgumentCaptor captor = + ArgumentCaptor.forClass(ServiceInstancesChangedEvent.class); + Mockito.verify(mockListener, Mockito.atLeast(0)).onEvent(captor.capture()); + return captor.getValue().getServiceInstances().size() == 2; + }); + ArgumentCaptor eventArgumentCaptor = + ArgumentCaptor.forClass(ServiceInstancesChangedEvent.class); + Mockito.verify(mockListener, Mockito.times(2)).onEvent(eventArgumentCaptor.capture()); + Assertions.assertEquals( + 2, eventArgumentCaptor.getValue().getServiceInstances().size()); + + serviceDiscovery.doUnregister(serviceInstance); + } + + @Test + void testPodsUpdate() throws Exception { + serviceDiscovery.setCurrentHostname(POD_NAME); + serviceDiscovery.setKubernetesClient(mockClient); + + ServiceInstance serviceInstance = new DefaultServiceInstance( + SERVICE_NAME, + "Test", + 12345, + ScopeModelUtil.getApplicationModel(serviceDiscovery.getUrl().getScopeModel())); + + serviceDiscovery.doRegister(serviceInstance); + + HashSet serviceList = new HashSet<>(4); + serviceList.add(SERVICE_NAME); + Mockito.when(mockListener.getServiceNames()).thenReturn(serviceList); + Mockito.doNothing().when(mockListener).onEvent(Mockito.any()); + + serviceDiscovery.addServiceInstancesChangedListener(mockListener); + + serviceInstance = new DefaultServiceInstance( + SERVICE_NAME, + "Test12345", + 12345, + ScopeModelUtil.getApplicationModel(serviceDiscovery.getUrl().getScopeModel())); + serviceDiscovery.doUpdate(serviceInstance, serviceInstance); + + await().until(() -> { + ArgumentCaptor captor = + ArgumentCaptor.forClass(ServiceInstancesChangedEvent.class); + Mockito.verify(mockListener, Mockito.atLeast(0)).onEvent(captor.capture()); + return captor.getValue().getServiceInstances().size() == 1; + }); + ArgumentCaptor eventArgumentCaptor = + ArgumentCaptor.forClass(ServiceInstancesChangedEvent.class); + Mockito.verify(mockListener, Mockito.times(1)).onEvent(eventArgumentCaptor.capture()); + Assertions.assertEquals( + 1, eventArgumentCaptor.getValue().getServiceInstances().size()); + + serviceDiscovery.doUnregister(serviceInstance); + } + + @Test + void testServiceUpdate() { + serviceDiscovery.setCurrentHostname(POD_NAME); + serviceDiscovery.setKubernetesClient(mockClient); + + ServiceInstance serviceInstance = new DefaultServiceInstance( + SERVICE_NAME, + "Test", + 12345, + ScopeModelUtil.getApplicationModel(serviceDiscovery.getUrl().getScopeModel())); + + serviceDiscovery.doRegister(serviceInstance); + + HashSet serviceList = new HashSet<>(4); + serviceList.add(SERVICE_NAME); + Mockito.when(mockListener.getServiceNames()).thenReturn(serviceList); + Mockito.doNothing().when(mockListener).onEvent(Mockito.any()); + + serviceDiscovery.addServiceInstancesChangedListener(mockListener); + + selector.put("app", "test"); + mockClient.services().withName(SERVICE_NAME).edit(service -> new ServiceBuilder(service) + .editSpec() + .addToSelector(selector) + .endSpec() + .build()); + + await().until(() -> { + ArgumentCaptor captor = + ArgumentCaptor.forClass(ServiceInstancesChangedEvent.class); + Mockito.verify(mockListener, Mockito.atLeast(0)).onEvent(captor.capture()); + return captor.getValue().getServiceInstances().size() == 1; + }); + ArgumentCaptor eventArgumentCaptor = + ArgumentCaptor.forClass(ServiceInstancesChangedEvent.class); + Mockito.verify(mockListener, Mockito.times(1)).onEvent(eventArgumentCaptor.capture()); + Assertions.assertEquals( + 1, eventArgumentCaptor.getValue().getServiceInstances().size()); + + serviceDiscovery.doUnregister(serviceInstance); + } + + @Test + void testGetInstance() { + serviceDiscovery.setCurrentHostname(POD_NAME); + serviceDiscovery.setKubernetesClient(mockClient); + + ServiceInstance serviceInstance = new DefaultServiceInstance( + SERVICE_NAME, + "Test", + 12345, + ScopeModelUtil.getApplicationModel(serviceDiscovery.getUrl().getScopeModel())); + + serviceDiscovery.doRegister(serviceInstance); + + serviceDiscovery.doUpdate(serviceInstance, serviceInstance); + + Assertions.assertEquals(1, serviceDiscovery.getServices().size()); + Assertions.assertEquals(1, serviceDiscovery.getInstances(SERVICE_NAME).size()); + + serviceDiscovery.doUnregister(serviceInstance); + } +} diff --git a/dubbo-kubernetes/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/dubbo-kubernetes/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker new file mode 100644 index 000000000..ca6ee9cea --- /dev/null +++ b/dubbo-kubernetes/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker @@ -0,0 +1 @@ +mock-maker-inline \ No newline at end of file diff --git a/dubbo-metadata-report-extensions/dubbo-metadata-report-etcd/pom.xml b/dubbo-metadata-report-extensions/dubbo-metadata-report-etcd/pom.xml index 9a6f2609d..8b2415ac7 100644 --- a/dubbo-metadata-report-extensions/dubbo-metadata-report-etcd/pom.xml +++ b/dubbo-metadata-report-extensions/dubbo-metadata-report-etcd/pom.xml @@ -39,8 +39,57 @@ org.apache.dubbo dubbo-metadata-api + 3.2.7 + + + dubbo-rpc-api + org.apache.dubbo + + + dubbo-common + org.apache.dubbo + + + dubbo-cluster + org.apache.dubbo + + true + + + org.apache.dubbo + dubbo-rpc-api + 3.2.7 + + + dubbo-common + org.apache.dubbo + + + true + + + + org.apache.dubbo + dubbo-cluster + 3.2.7 + + + dubbo-rpc-api + org.apache.dubbo + + + true + + + + org.apache.dubbo + dubbo-common + 3.2.7 + true + + org.apache.dubbo.extensions dubbo-remoting-etcd3 diff --git a/dubbo-registry-extensions/dubbo-registry-consul/pom.xml b/dubbo-registry-extensions/dubbo-registry-consul/pom.xml index 6d54af330..cd38639c4 100644 --- a/dubbo-registry-extensions/dubbo-registry-consul/pom.xml +++ b/dubbo-registry-extensions/dubbo-registry-consul/pom.xml @@ -36,6 +36,13 @@ org.apache.dubbo dubbo-registry-api + ${dubbo3.version} + true + + + org.apache.dubbo + dubbo-common + ${dubbo3.version} true diff --git a/dubbo-registry-extensions/dubbo-registry-consul/src/test/java/org/apache/dubbo/registry/consul/ConsulServiceDiscoveryTest.java b/dubbo-registry-extensions/dubbo-registry-consul/src/test/java/org/apache/dubbo/registry/consul/ConsulServiceDiscoveryTest.java index f6e468f5d..81987422e 100644 --- a/dubbo-registry-extensions/dubbo-registry-consul/src/test/java/org/apache/dubbo/registry/consul/ConsulServiceDiscoveryTest.java +++ b/dubbo-registry-extensions/dubbo-registry-consul/src/test/java/org/apache/dubbo/registry/consul/ConsulServiceDiscoveryTest.java @@ -29,6 +29,7 @@ import com.pszymczyk.consul.ConsulStarterBuilder; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.mockito.ArgumentCaptor; import org.mockito.Mockito; @@ -39,6 +40,8 @@ import static org.junit.jupiter.api.Assertions.assertEquals; + +@Disabled public class ConsulServiceDiscoveryTest { private URL url; @@ -124,8 +127,8 @@ public void testNotify() throws InterruptedException { serviceInstance2.getMetadata().put("test", "test"); serviceInstance2.getMetadata().put("test123", "test"); consulServiceDiscovery.doRegister(serviceInstance2); - Thread.sleep(3000); + Mockito.verify(serviceInstancesChangedListener1, Mockito.atLeast(2)).onEvent(eventArgumentCaptor.capture()); serviceInstances = eventArgumentCaptor.getValue().getServiceInstances(); assertEquals(2, serviceInstances.size()); diff --git a/dubbo-registry-extensions/dubbo-registry-etcd3/pom.xml b/dubbo-registry-extensions/dubbo-registry-etcd3/pom.xml index e56306820..99c445b30 100644 --- a/dubbo-registry-extensions/dubbo-registry-etcd3/pom.xml +++ b/dubbo-registry-extensions/dubbo-registry-etcd3/pom.xml @@ -32,11 +32,14 @@ The etcd3 registry module of Dubbo project + org.apache.dubbo dubbo-registry-api + 3.2.7 true + com.google.code.gson gson @@ -44,8 +47,16 @@ org.apache.dubbo dubbo-common + 3.2.7 true + + + com.google.code.gson + gson + provided + + org.apache.dubbo.extensions dubbo-remoting-etcd3 diff --git a/dubbo-registry-extensions/dubbo-registry-etcd3/src/test/java/org/apache/dubbo/registry/etcd/EtcdServiceDiscoveryTest.java b/dubbo-registry-extensions/dubbo-registry-etcd3/src/test/java/org/apache/dubbo/registry/etcd/EtcdServiceDiscoveryTest.java index 50f66e602..a84104870 100644 --- a/dubbo-registry-extensions/dubbo-registry-etcd3/src/test/java/org/apache/dubbo/registry/etcd/EtcdServiceDiscoveryTest.java +++ b/dubbo-registry-extensions/dubbo-registry-etcd3/src/test/java/org/apache/dubbo/registry/etcd/EtcdServiceDiscoveryTest.java @@ -1,124 +1,125 @@ -///* -// * Licensed to the Apache Software Foundation (ASF) under one or more -// * contributor license agreements. See the NOTICE file distributed with -// * this work for additional information regarding copyright ownership. -// * The ASF licenses this file to You 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.apache.dubbo.registry.etcd; -// -//import org.apache.dubbo.common.URL; -//import org.apache.dubbo.registry.client.DefaultServiceInstance; -//import org.apache.dubbo.registry.client.ServiceInstance; -// -//import com.google.gson.Gson; -//import org.junit.jupiter.api.AfterAll; -//import org.junit.jupiter.api.Assertions; -//import org.junit.jupiter.api.BeforeAll; -//import org.junit.jupiter.api.Disabled; -//import org.junit.jupiter.api.Test; -// -//import java.util.ArrayList; -//import java.util.List; -// -//import static java.lang.String.valueOf; -// -///** -// * 2019-08-30 -// *

-// * There is no embedded server. so it works depend on etcd local server. -// */ -//@Disabled -//public class EtcdServiceDiscoveryTest { -// -// static EtcdServiceDiscovery etcdServiceDiscovery; -// -// @BeforeAll -// public static void setUp() throws Exception { -// URL url = URL.valueOf("etcd3://127.0.0.1:2379/org.apache.dubbo.registry.RegistryService"); -// etcdServiceDiscovery = new EtcdServiceDiscovery(); -// Assertions.assertNull(etcdServiceDiscovery.etcdClient); -// etcdServiceDiscovery.initialize(url); -// } -// -// @AfterAll -// public static void destroy() throws Exception { -//// etcdServiceDiscovery.destroy(); -// } -// -// -// @Test -// public void testLifecycle() throws Exception { -// URL url = URL.valueOf("etcd3://127.0.0.1:2233/org.apache.dubbo.registry.RegistryService"); -// EtcdServiceDiscovery etcdServiceDiscoveryTmp = new EtcdServiceDiscovery(); -// Assertions.assertNull(etcdServiceDiscoveryTmp.etcdClient); -// etcdServiceDiscoveryTmp.initialize(url); -// Assertions.assertNotNull(etcdServiceDiscoveryTmp.etcdClient); -// Assertions.assertTrue(etcdServiceDiscoveryTmp.etcdClient.isConnected()); -// etcdServiceDiscoveryTmp.destroy(); -// Assertions.assertFalse(etcdServiceDiscoveryTmp.etcdClient.isConnected()); -// } -// -// @Test -// public void testRegistry() throws Exception { -// ServiceInstance serviceInstance = new DefaultServiceInstance(valueOf(System.nanoTime()), "EtcdTestService", "127.0.0.1", 8080); -// Assertions.assertNull(etcdServiceDiscovery.etcdClient.getKVValue(etcdServiceDiscovery.toPath(serviceInstance))); -// etcdServiceDiscovery.register(serviceInstance); -// Assertions.assertNotNull(etcdServiceDiscovery.etcdClient.getKVValue(etcdServiceDiscovery.toPath(serviceInstance))); -// } -// -// @Test -// public void testUnRegistry() throws Exception { -// ServiceInstance serviceInstance = new DefaultServiceInstance(valueOf(System.nanoTime()), "EtcdTest2Service", "127.0.0.1", 8080); -// Assertions.assertNull(etcdServiceDiscovery.etcdClient.getKVValue(etcdServiceDiscovery.toPath(serviceInstance))); -// etcdServiceDiscovery.register(serviceInstance); -// Assertions.assertNotNull(etcdServiceDiscovery.etcdClient.getKVValue(etcdServiceDiscovery.toPath(serviceInstance))); -// etcdServiceDiscovery.unregister(serviceInstance); -// Assertions.assertNull(etcdServiceDiscovery.etcdClient.getKVValue(etcdServiceDiscovery.toPath(serviceInstance))); -// } -// -// @Test -// public void testUpdate() throws Exception { -// DefaultServiceInstance serviceInstance = new DefaultServiceInstance(valueOf(System.nanoTime()), "EtcdTest34Service", "127.0.0.1", 8080); -// Assertions.assertNull(etcdServiceDiscovery.etcdClient.getKVValue(etcdServiceDiscovery.toPath(serviceInstance))); -// etcdServiceDiscovery.register(serviceInstance); -// Assertions.assertNotNull(etcdServiceDiscovery.etcdClient.getKVValue(etcdServiceDiscovery.toPath(serviceInstance))); -// Assertions.assertEquals(etcdServiceDiscovery.etcdClient.getKVValue(etcdServiceDiscovery.toPath(serviceInstance)), -// new Gson().toJson(serviceInstance)); -// serviceInstance.setPort(9999); -// etcdServiceDiscovery.update(serviceInstance); -// Assertions.assertNotNull(etcdServiceDiscovery.etcdClient.getKVValue(etcdServiceDiscovery.toPath(serviceInstance))); -// Assertions.assertEquals(etcdServiceDiscovery.etcdClient.getKVValue(etcdServiceDiscovery.toPath(serviceInstance)), -// new Gson().toJson(serviceInstance)); -// } -// -// @Test -// public void testGetInstances() throws Exception { -// String serviceName = "EtcdTest77Service"; -// Assertions.assertTrue(etcdServiceDiscovery.getInstances(serviceName).isEmpty()); -// etcdServiceDiscovery.register(new DefaultServiceInstance(valueOf(System.nanoTime()), serviceName, "127.0.0.1", 8080)); -// etcdServiceDiscovery.register(new DefaultServiceInstance(valueOf(System.nanoTime()), serviceName, "127.0.0.1", 9809)); -// Assertions.assertFalse(etcdServiceDiscovery.getInstances(serviceName).isEmpty()); -// List r = convertToIpPort(etcdServiceDiscovery.getInstances(serviceName)); -// Assertions.assertTrue(r.contains("127.0.0.1:8080")); -// Assertions.assertTrue(r.contains("127.0.0.1:9809")); -// } -// -// private List convertToIpPort(List serviceInstances) { -// List result = new ArrayList<>(); -// for (ServiceInstance serviceInstance : serviceInstances) { -// result.add(serviceInstance.getHost() + ":" + serviceInstance.getPort()); -// } -// return result; -// } -// -//} +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.dubbo.registry.etcd; +import org.apache.dubbo.common.URL; +import org.apache.dubbo.config.ApplicationConfig; +import org.apache.dubbo.registry.client.DefaultServiceInstance; +import org.apache.dubbo.registry.client.ServiceInstance; +import com.google.gson.Gson; +import org.apache.dubbo.rpc.model.ApplicationModel; +import org.apache.dubbo.rpc.model.FrameworkModel; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.List; + +/** + * 2019-08-30 + *

+ * There is no embedded server. so it works depend on etcd local server. + */ +@Disabled +public class EtcdServiceDiscoveryTest { + + private EtcdServiceDiscovery etcdServiceDiscovery; + + private ApplicationModel applicationModel; + + @BeforeEach + public void setUp() { + URL url = URL.valueOf("etcd3://127.0.0.1:2379/org.apache.dubbo.registry.RegistryService"); + FrameworkModel frameworkModel = FrameworkModel.defaultModel(); + applicationModel = frameworkModel.newApplication(); + ApplicationConfig config = new ApplicationConfig(); + config.setName("MockMetrics"); + applicationModel.getApplicationConfigManager().setApplication(config); + etcdServiceDiscovery = new EtcdServiceDiscovery(applicationModel,url); + } + + @AfterEach + public void destroy() throws Exception { + etcdServiceDiscovery.destroy(); + } + + + @Test + public void testLifecycle() throws Exception { + Assertions.assertNotNull(etcdServiceDiscovery.etcdClient); + Assertions.assertTrue(etcdServiceDiscovery.etcdClient.isConnected()); + etcdServiceDiscovery.destroy(); + Assertions.assertFalse(etcdServiceDiscovery.etcdClient.isConnected()); + } + + @Test + public void testRegistry(){ + ServiceInstance serviceInstance = new DefaultServiceInstance("EtcdTestService", "127.0.0.1", 8080,applicationModel); + Assertions.assertNull(etcdServiceDiscovery.etcdClient.getKVValue(etcdServiceDiscovery.toPath(serviceInstance))); + etcdServiceDiscovery.doRegister(serviceInstance); + Assertions.assertNotNull(etcdServiceDiscovery.etcdClient.getKVValue(etcdServiceDiscovery.toPath(serviceInstance))); + } + + @Test + public void testUnRegistry() { + ServiceInstance serviceInstance = new DefaultServiceInstance("EtcdTest2Service", "127.0.0.1", 8080,applicationModel); + Assertions.assertNull(etcdServiceDiscovery.etcdClient.getKVValue(etcdServiceDiscovery.toPath(serviceInstance))); + etcdServiceDiscovery.doRegister(serviceInstance); + Assertions.assertNotNull(etcdServiceDiscovery.etcdClient.getKVValue(etcdServiceDiscovery.toPath(serviceInstance))); + etcdServiceDiscovery.doUnregister(serviceInstance); + Assertions.assertNull(etcdServiceDiscovery.etcdClient.getKVValue(etcdServiceDiscovery.toPath(serviceInstance))); + } + + @Test + public void testUpdate() { + DefaultServiceInstance serviceInstance = new DefaultServiceInstance( "EtcdTest34Service", "127.0.0.1", 8080,applicationModel); + Assertions.assertNull(etcdServiceDiscovery.etcdClient.getKVValue(etcdServiceDiscovery.toPath(serviceInstance))); + etcdServiceDiscovery.doRegister(serviceInstance); + + Assertions.assertNotNull(etcdServiceDiscovery.etcdClient.getKVValue(etcdServiceDiscovery.toPath(serviceInstance))); + + Assertions.assertEquals(etcdServiceDiscovery.etcdClient.getKVValue(etcdServiceDiscovery.toPath(serviceInstance)), new Gson().toJson(serviceInstance)); + serviceInstance.setPort(9999); + + etcdServiceDiscovery.doRegister(serviceInstance); + Assertions.assertNotNull(etcdServiceDiscovery.etcdClient.getKVValue(etcdServiceDiscovery.toPath(serviceInstance))); + Assertions.assertEquals(etcdServiceDiscovery.etcdClient.getKVValue(etcdServiceDiscovery.toPath(serviceInstance)), new Gson().toJson(serviceInstance)); + } + + @Test + public void testGetInstances() { + String serviceName = "EtcdTest77Service"; + Assertions.assertTrue(etcdServiceDiscovery.getInstances(serviceName).isEmpty()); + etcdServiceDiscovery.doRegister(new DefaultServiceInstance(serviceName, "127.0.0.1", 8080,applicationModel)); + etcdServiceDiscovery.doRegister(new DefaultServiceInstance(serviceName, "127.0.0.1", 9809,applicationModel)); + Assertions.assertFalse(etcdServiceDiscovery.getInstances(serviceName).isEmpty()); + List r = convertToIpPort(etcdServiceDiscovery.getInstances(serviceName)); + Assertions.assertTrue(r.contains("127.0.0.1:8080")); + Assertions.assertTrue(r.contains("127.0.0.1:9809")); + } + + private List convertToIpPort(List serviceInstances) { + List result = new ArrayList<>(); + for (ServiceInstance serviceInstance : serviceInstances) { + result.add(serviceInstance.getHost() + ":" + serviceInstance.getPort()); + } + return result; + } + +} diff --git a/dubbo-registry-extensions/dubbo-registry-redis/pom.xml b/dubbo-registry-extensions/dubbo-registry-redis/pom.xml index 01c091b12..d7095b8a7 100644 --- a/dubbo-registry-extensions/dubbo-registry-redis/pom.xml +++ b/dubbo-registry-extensions/dubbo-registry-redis/pom.xml @@ -35,6 +35,7 @@ org.apache.dubbo dubbo-registry-api + ${dubbo3.version} true diff --git a/dubbo-registry-extensions/pom.xml b/dubbo-registry-extensions/pom.xml index 64eb9c448..82c68b059 100644 --- a/dubbo-registry-extensions/pom.xml +++ b/dubbo-registry-extensions/pom.xml @@ -27,6 +27,14 @@ dubbo-registry-extensions ${revision} pom + + + UTF-8 + 1.8 + 1.8 + 3.2.9 + + dubbo-registry-dns dubbo-registry-consul @@ -36,4 +44,5 @@ dubbo-registry-nameservice dubbo-registry-polaris + diff --git a/dubbo-remoting-extensions/dubbo-remoting-etcd3/pom.xml b/dubbo-remoting-extensions/dubbo-remoting-etcd3/pom.xml index bca7da7b8..ea6007993 100644 --- a/dubbo-remoting-extensions/dubbo-remoting-etcd3/pom.xml +++ b/dubbo-remoting-extensions/dubbo-remoting-etcd3/pom.xml @@ -42,11 +42,37 @@ org.apache.dubbo dubbo-remoting-api + 3.2.0 + + + dubbo-common + org.apache.dubbo + + + dubbo-serialization-api + org.apache.dubbo + + true + + + org.apache.dubbo + dubbo-serialization-api + 3.2.0 + + + dubbo-common + org.apache.dubbo + + + true + + org.apache.dubbo dubbo-common + 3.2.0 true @@ -93,17 +119,19 @@ + org.apache.maven.plugins maven-surefire-plugin - + ${skipIntegrationTests} - + + diff --git a/dubbo-remoting-extensions/dubbo-remoting-etcd3/src/main/java/org/apache/dubbo/remoting/etcd/Constants.java b/dubbo-remoting-extensions/dubbo-remoting-etcd3/src/main/java/org/apache/dubbo/remoting/etcd/Constants.java index 3450bb5b3..8445fd214 100644 --- a/dubbo-remoting-extensions/dubbo-remoting-etcd3/src/main/java/org/apache/dubbo/remoting/etcd/Constants.java +++ b/dubbo-remoting-extensions/dubbo-remoting-etcd3/src/main/java/org/apache/dubbo/remoting/etcd/Constants.java @@ -17,12 +17,10 @@ package org.apache.dubbo.remoting.etcd; -import static org.apache.dubbo.remoting.Constants.DEFAULT_IO_THREADS; - public interface Constants { String ETCD3_NOTIFY_MAXTHREADS_KEYS = "etcd3.notify.maxthreads"; - int DEFAULT_ETCD3_NOTIFY_THREADS = DEFAULT_IO_THREADS; + int DEFAULT_ETCD3_NOTIFY_THREADS = Math.min(Runtime.getRuntime().availableProcessors() + 1, 32); String DEFAULT_ETCD3_NOTIFY_QUEUES_KEY = "etcd3.notify.queues"; diff --git a/dubbo-remoting-extensions/dubbo-remoting-etcd3/src/main/java/org/apache/dubbo/remoting/etcd/jetcd/JEtcdClientWrapper.java b/dubbo-remoting-extensions/dubbo-remoting-etcd3/src/main/java/org/apache/dubbo/remoting/etcd/jetcd/JEtcdClientWrapper.java index 286586f31..08796c3e9 100644 --- a/dubbo-remoting-extensions/dubbo-remoting-etcd3/src/main/java/org/apache/dubbo/remoting/etcd/jetcd/JEtcdClientWrapper.java +++ b/dubbo-remoting-extensions/dubbo-remoting-etcd3/src/main/java/org/apache/dubbo/remoting/etcd/jetcd/JEtcdClientWrapper.java @@ -522,6 +522,7 @@ public void start() { } try { + this.future = reconnectNotify.scheduleWithFixedDelay(() -> { boolean connected = isConnected(); if (connectState != connected) { diff --git a/dubbo-remoting-extensions/dubbo-remoting-etcd3/src/test/java/org/apache/dubbo/remoting/etcd/jetcd/JEtcdClientTest.java b/dubbo-remoting-extensions/dubbo-remoting-etcd3/src/test/java/org/apache/dubbo/remoting/etcd/jetcd/JEtcdClientTest.java index 15c16343f..0344f9003 100644 --- a/dubbo-remoting-extensions/dubbo-remoting-etcd3/src/test/java/org/apache/dubbo/remoting/etcd/jetcd/JEtcdClientTest.java +++ b/dubbo-remoting-extensions/dubbo-remoting-etcd3/src/test/java/org/apache/dubbo/remoting/etcd/jetcd/JEtcdClientTest.java @@ -76,6 +76,7 @@ public class JEtcdClientTest { public void test_watch_when_create_path() throws InterruptedException { String path = "/dubbo/com.alibaba.dubbo.demo.DemoService/providers"; + String child = "/dubbo/com.alibaba.dubbo.demo.DemoService/providers/demoService1"; final CountDownLatch notNotified = new CountDownLatch(1); @@ -89,6 +90,7 @@ public void test_watch_when_create_path() throws InterruptedException { client.addChildListener(path, childListener); client.createEphemeral(child); + Assertions.assertTrue(notNotified.await(10, TimeUnit.SECONDS)); client.removeChildListener(path, childListener); @@ -151,8 +153,8 @@ public void onCompleted() { } }); - WatchCreateRequest.Builder builder = WatchCreateRequest.newBuilder() - .setKey(ByteString.copyFrom(path, UTF_8)); + + WatchCreateRequest.Builder builder = WatchCreateRequest.newBuilder().setKey(ByteString.copyFrom(path, UTF_8)); observer.onNext(WatchRequest.newBuilder().setCreateRequest(builder).build()); @@ -171,6 +173,7 @@ public void testCancelWatchWithGrpc() { CountDownLatch updateLatch = new CountDownLatch(1); CountDownLatch cancelLatch = new CountDownLatch(1); final AtomicLong watchID = new AtomicLong(-1L); + try (Client client = Client.builder().endpoints(endpoint).build()) { ManagedChannel channel = getChannel(client); StreamObserver observer = WatchGrpc.newStub(channel).watch(new StreamObserver() { @@ -232,6 +235,7 @@ public void onCompleted() { public void test_watch_when_create_wrong_path() throws InterruptedException { String path = "/dubbo/com.alibaba.dubbo.demo.DemoService/providers"; + String child = "/dubbo/com.alibaba.dubbo.demo.DemoService/routers/demoService1"; final CountDownLatch notNotified = new CountDownLatch(1); @@ -248,7 +252,9 @@ public void test_watch_when_create_wrong_path() throws InterruptedException { Assertions.assertFalse(notNotified.await(1, TimeUnit.SECONDS)); client.removeChildListener(path, childListener); + client.delete(child); + } @Test @@ -309,6 +315,7 @@ public void test_watch_then_unwatch() throws InterruptedException { public void test_watch_on_unrecoverable_connection() throws InterruptedException { String path = "/dubbo/com.alibaba.dubbo.demo.DemoService/providers"; + JEtcdClient.EtcdWatcher watcher = null; try { ChildListener childListener = (parent, children) -> { @@ -320,7 +327,7 @@ public void test_watch_on_unrecoverable_connection() throws InterruptedException watcher.watchRequest.onNext(watcher.nextRequest()); } catch (Exception e) { - Assertions.assertTrue(e.getMessage().contains("call was cancelled")); + Assertions.assertTrue(e.getMessage().contains("calls are allowed")); } } @@ -387,8 +394,7 @@ public void test_watch_after_client_closed() throws InterruptedException { @BeforeEach public void setUp() { // timeout in 15 seconds. - URL url = URL.valueOf("etcd3://127.0.0.1:2379/com.alibaba.dubbo.registry.RegistryService") - .addParameter(SESSION_TIMEOUT_KEY, 15000); + URL url = URL.valueOf("etcd3://127.0.0.1:2379/com.alibaba.dubbo.registry.RegistryService").addParameter(SESSION_TIMEOUT_KEY, 15000); client = new JEtcdClient(url); } diff --git a/dubbo-remoting-extensions/dubbo-remoting-redis/src/main/java/org/apache/dubbo/remoting/redis/jedis/ClusterRedisClient.java b/dubbo-remoting-extensions/dubbo-remoting-redis/src/main/java/org/apache/dubbo/remoting/redis/jedis/ClusterRedisClient.java index e0e4c14a1..fb5f4ff0c 100644 --- a/dubbo-remoting-extensions/dubbo-remoting-redis/src/main/java/org/apache/dubbo/remoting/redis/jedis/ClusterRedisClient.java +++ b/dubbo-remoting-extensions/dubbo-remoting-redis/src/main/java/org/apache/dubbo/remoting/redis/jedis/ClusterRedisClient.java @@ -45,12 +45,15 @@ public class ClusterRedisClient extends AbstractRedisClient implements RedisClie private static final int DEFAULT_MAX_ATTEMPTS = 5; - private JedisCluster jedisCluster; + private final JedisCluster jedisCluster; private Pattern COLON_SPLIT_PATTERN = Pattern.compile("\\s*[:]+\\s*"); public ClusterRedisClient(URL url) { super(url); Set nodes = getNodes(url); + if (url.hasParameter("db.index")) { + logger.warn("Redis Cluster does not support multiple databases, the SELECT command is not allowed. So the setting of db.index will not be effect"); + } jedisCluster = new JedisCluster(nodes, url.getParameter("connection.timeout", DEFAULT_TIMEOUT), url.getParameter("so.timeout", DEFAULT_SO_TIMEOUT), url.getParameter("max.attempts", DEFAULT_MAX_ATTEMPTS), url.getPassword(), getConfig()); diff --git a/dubbo-remoting-extensions/dubbo-remoting-redis/src/main/java/org/apache/dubbo/remoting/redis/jedis/MonoRedisClient.java b/dubbo-remoting-extensions/dubbo-remoting-redis/src/main/java/org/apache/dubbo/remoting/redis/jedis/MonoRedisClient.java index 864f18e33..07cb9eddf 100644 --- a/dubbo-remoting-extensions/dubbo-remoting-redis/src/main/java/org/apache/dubbo/remoting/redis/jedis/MonoRedisClient.java +++ b/dubbo-remoting-extensions/dubbo-remoting-redis/src/main/java/org/apache/dubbo/remoting/redis/jedis/MonoRedisClient.java @@ -35,14 +35,12 @@ public class MonoRedisClient extends AbstractRedisClient implements RedisClient { private static final Logger logger = LoggerFactory.getLogger(MonoRedisClient.class); - private static final String START_CURSOR = "0"; - - private JedisPool jedisPool; + private final JedisPool jedisPool; public MonoRedisClient(URL url) { super(url); jedisPool = new JedisPool(getConfig(), url.getHost(), url.getPort(), - url.getParameter(TIMEOUT_KEY, DEFAULT_TIMEOUT), url.getPassword()); + url.getParameter(TIMEOUT_KEY, DEFAULT_TIMEOUT), url.getPassword(), url.getParameter("db.index", 0)); } @Override diff --git a/dubbo-remoting-extensions/dubbo-remoting-redis/src/main/java/org/apache/dubbo/remoting/redis/jedis/SentinelRedisClient.java b/dubbo-remoting-extensions/dubbo-remoting-redis/src/main/java/org/apache/dubbo/remoting/redis/jedis/SentinelRedisClient.java index 6a10bf987..137a37914 100644 --- a/dubbo-remoting-extensions/dubbo-remoting-redis/src/main/java/org/apache/dubbo/remoting/redis/jedis/SentinelRedisClient.java +++ b/dubbo-remoting-extensions/dubbo-remoting-redis/src/main/java/org/apache/dubbo/remoting/redis/jedis/SentinelRedisClient.java @@ -32,10 +32,13 @@ import java.util.Map; import java.util.Set; +import static org.apache.dubbo.common.constants.CommonConstants.DEFAULT_TIMEOUT; +import static org.apache.dubbo.common.constants.CommonConstants.TIMEOUT_KEY; + public class SentinelRedisClient extends AbstractRedisClient implements RedisClient { private static final Logger logger = LoggerFactory.getLogger(SentinelRedisClient.class); - private JedisSentinelPool sentinelPool; + private final JedisSentinelPool sentinelPool; public SentinelRedisClient(URL url) { super(url); @@ -47,7 +50,8 @@ public SentinelRedisClient(URL url) { } Set sentinels = new HashSet<>(Arrays.asList(backupAddresses)); sentinels.add(address); - sentinelPool = new JedisSentinelPool(masterName, sentinels, getConfig(), url.getPassword()); + sentinelPool = new JedisSentinelPool(masterName, sentinels, getConfig(), url.getParameter(TIMEOUT_KEY, DEFAULT_TIMEOUT), url.getPassword(), + url.getParameter("db.index", 0)); } @Override diff --git a/dubbo-serialization-extensions/dubbo-serialization-protostuff/src/main/java/org/apache/dubbo/common/serialize/protostuff/delegate/ServiceConfigURLDelegate.java b/dubbo-serialization-extensions/dubbo-serialization-protostuff/src/main/java/org/apache/dubbo/common/serialize/protostuff/delegate/ServiceConfigURLDelegate.java new file mode 100644 index 000000000..6ea8b3b25 --- /dev/null +++ b/dubbo-serialization-extensions/dubbo-serialization-protostuff/src/main/java/org/apache/dubbo/common/serialize/protostuff/delegate/ServiceConfigURLDelegate.java @@ -0,0 +1,57 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.dubbo.common.serialize.protostuff.delegate; + +import io.protostuff.Input; +import io.protostuff.Output; +import io.protostuff.Pipe; +import io.protostuff.WireFormat; +import io.protostuff.runtime.Delegate; + +import java.io.IOException; + +import org.apache.dubbo.common.url.component.ServiceConfigURL; + +/** + * Custom {@link org.apache.dubbo.common.url.component.ServiceConfigURL} delegate + */ +public class ServiceConfigURLDelegate implements Delegate { + @Override + public WireFormat.FieldType getFieldType() { + return WireFormat.FieldType.STRING; + } + + @Override + public ServiceConfigURL readFrom(Input input) throws IOException { + return (ServiceConfigURL) org.apache.dubbo.common.URL.valueOf(input.readString()); + } + + @Override + public void writeTo(Output output, int number, ServiceConfigURL value, boolean repeated) throws IOException { + output.writeString(number, value.toFullString(), repeated); + } + + @Override + public void transfer(Pipe pipe, Input input, Output output, int number, boolean repeated) throws IOException { + output.writeString(number, input.readString(), repeated); + } + + @Override + public Class typeClass() { + return ServiceConfigURL.class; + } +} diff --git a/dubbo-serialization-extensions/dubbo-serialization-protostuff/src/main/java/org/apache/dubbo/common/serialize/protostuff/utils/WrapperUtils.java b/dubbo-serialization-extensions/dubbo-serialization-protostuff/src/main/java/org/apache/dubbo/common/serialize/protostuff/utils/WrapperUtils.java index 321ed0dcb..4cd3809bc 100644 --- a/dubbo-serialization-extensions/dubbo-serialization-protostuff/src/main/java/org/apache/dubbo/common/serialize/protostuff/utils/WrapperUtils.java +++ b/dubbo-serialization-extensions/dubbo-serialization-protostuff/src/main/java/org/apache/dubbo/common/serialize/protostuff/utils/WrapperUtils.java @@ -21,6 +21,7 @@ import org.apache.dubbo.common.serialize.protostuff.delegate.SqlDateDelegate; import org.apache.dubbo.common.serialize.protostuff.delegate.TimeDelegate; import org.apache.dubbo.common.serialize.protostuff.delegate.TimestampDelegate; +import org.apache.dubbo.common.serialize.protostuff.delegate.ServiceConfigURLDelegate; import io.protostuff.runtime.DefaultIdStrategy; import io.protostuff.runtime.RuntimeEnv; @@ -56,6 +57,7 @@ public class WrapperUtils { if (RuntimeEnv.ID_STRATEGY instanceof DefaultIdStrategy) { ((DefaultIdStrategy) RuntimeEnv.ID_STRATEGY).registerDelegate(new TimeDelegate()); ((DefaultIdStrategy) RuntimeEnv.ID_STRATEGY).registerDelegate(new TimestampDelegate()); + ((DefaultIdStrategy) RuntimeEnv.ID_STRATEGY).registerDelegate(new ServiceConfigURLDelegate()); ((DefaultIdStrategy) RuntimeEnv.ID_STRATEGY).registerDelegate(new SqlDateDelegate()); } @@ -87,6 +89,7 @@ public class WrapperUtils { WRAPPER_SET.add(Time.class); WRAPPER_SET.add(Timestamp.class); WRAPPER_SET.add(java.sql.Date.class); + WRAPPER_SET.add(org.apache.dubbo.common.url.component.ServiceConfigURL.class); WRAPPER_SET.add(Wrapper.class); diff --git a/dubbo-serialization-extensions/dubbo-serialization-test/src/test/java/org/apache/dubbo/common/serialize/base/AbstractSerializationTest.java b/dubbo-serialization-extensions/dubbo-serialization-test/src/test/java/org/apache/dubbo/common/serialize/base/AbstractSerializationTest.java index fabe08e0d..02afaea73 100644 --- a/dubbo-serialization-extensions/dubbo-serialization-test/src/test/java/org/apache/dubbo/common/serialize/base/AbstractSerializationTest.java +++ b/dubbo-serialization-extensions/dubbo-serialization-test/src/test/java/org/apache/dubbo/common/serialize/base/AbstractSerializationTest.java @@ -68,7 +68,7 @@ public abstract class AbstractSerializationTest { protected URL url = new URL("protocol", "1.1.1.1", 1234); protected ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); - // ================ Primitive Type ================ + // ================ Primitive Type ================ protected BigPerson bigPerson; protected MediaContent mediaContent; @@ -383,7 +383,7 @@ public void test_BytesRange() throws Exception { } } - // ================ Array Type ================ + // ================ Array Type ================ void assertObjectArray(T[] data, Class clazz) throws Exception { ObjectOutput objectOutput = serialization.serialize(url, byteArrayOutputStream); @@ -762,7 +762,7 @@ public void test_StringArray_withType() throws Exception { assertObjectArrayWithType(new String[]{"1", "b"}, String[].class); } - // ================ Simple Type ================ + // ================ Simple Type ================ @Test public void test_IntegerArray() throws Exception { @@ -979,7 +979,7 @@ public void test_LinkedHashMap() throws Exception { } } - // ================ Complex Collection Type ================ + // ================ Complex Collection Type ================ @Test public void test_SPersonList() throws Exception { @@ -1111,7 +1111,7 @@ public void test_MultiObject_WithType() throws Exception { } - // abnormal case + // abnormal case @Test public void test_MediaContent_badStream() throws Exception { @@ -1188,7 +1188,6 @@ public void test_LoopReference() throws Exception { // ================ final field test ================ @Test - @Disabled public void test_URL_mutable_withType() throws Exception { URL data = URL.valueOf("dubbo://admin:hello1234@10.20.130.230:20880/context/path?version=1.0.0&application=morgan&noValue"); diff --git a/dubbo-xds/pom.xml b/dubbo-xds/pom.xml new file mode 100644 index 000000000..def5fbb45 --- /dev/null +++ b/dubbo-xds/pom.xml @@ -0,0 +1,141 @@ + + + + 4.0.0 + + org.apache.dubbo.extensions + extensions-parent + ${revision} + ../pom.xml + + + dubbo-xds + ${project.artifactId} + The xDS Integration + + false + 3.25.1 + 1.54.0 + 1.7.1 + 0.6.1 + + + + + + org.apache.dubbo + dubbo-bom + 3.2.9 + pom + import + + + + + + + org.apache.dubbo + dubbo-registry-api + true + + + + org.apache.dubbo + dubbo-common + true + + + + io.grpc + grpc-protobuf + + + + io.grpc + grpc-stub + + + + io.grpc + grpc-netty-shaded + + + + io.envoyproxy.controlplane + api + + + + com.google.protobuf + protobuf-java + + + + com.google.protobuf + protobuf-java-util + + + + org.bouncycastle + bcprov-jdk15on + + + org.bouncycastle + bcpkix-jdk15on + + + org.bouncycastle + bcprov-ext-jdk15on + + + + + + + + org.xolstice.maven.plugins + protobuf-maven-plugin + ${maven_protobuf_plugin_version} + + com.google.protobuf:protoc:${protobuf-java_version}:exe:${os.detected.classifier} + + grpc-java + io.grpc:protoc-gen-grpc-java:${grpc_version}:exe:${os.detected.classifier} + + + + + + compile + compile-custom + + + + + + + + kr.motd.maven + os-maven-plugin + ${maven_os_plugin_version} + + + + + diff --git a/dubbo-xds/src/main/java/org/apache/dubbo/registry/xds/XdsCertificateSigner.java b/dubbo-xds/src/main/java/org/apache/dubbo/registry/xds/XdsCertificateSigner.java new file mode 100644 index 000000000..1ef1a8b31 --- /dev/null +++ b/dubbo-xds/src/main/java/org/apache/dubbo/registry/xds/XdsCertificateSigner.java @@ -0,0 +1,58 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.dubbo.registry.xds; + +import org.apache.dubbo.common.URL; +import org.apache.dubbo.common.extension.Adaptive; +import org.apache.dubbo.common.extension.SPI; + +@SPI +public interface XdsCertificateSigner { + + @Adaptive(value = "signer") + CertPair GenerateCert(URL url); + + class CertPair { + private final String privateKey; + private final String publicKey; + private final long createTime; + private final long expireTime; + + public CertPair(String privateKey, String publicKey, long createTime, long expireTime) { + this.privateKey = privateKey; + this.publicKey = publicKey; + this.createTime = createTime; + this.expireTime = expireTime; + } + + public String getPrivateKey() { + return privateKey; + } + + public String getPublicKey() { + return publicKey; + } + + public long getCreateTime() { + return createTime; + } + + public boolean isExpire() { + return System.currentTimeMillis() < expireTime; + } + } +} diff --git a/dubbo-xds/src/main/java/org/apache/dubbo/registry/xds/XdsEnv.java b/dubbo-xds/src/main/java/org/apache/dubbo/registry/xds/XdsEnv.java new file mode 100644 index 000000000..c09ea8087 --- /dev/null +++ b/dubbo-xds/src/main/java/org/apache/dubbo/registry/xds/XdsEnv.java @@ -0,0 +1,22 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.dubbo.registry.xds; + +public interface XdsEnv { + + String getCluster(); +} diff --git a/dubbo-xds/src/main/java/org/apache/dubbo/registry/xds/XdsInitializationException.java b/dubbo-xds/src/main/java/org/apache/dubbo/registry/xds/XdsInitializationException.java new file mode 100644 index 000000000..9a9b9a35c --- /dev/null +++ b/dubbo-xds/src/main/java/org/apache/dubbo/registry/xds/XdsInitializationException.java @@ -0,0 +1,28 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.dubbo.registry.xds; + +public final class XdsInitializationException extends Exception { + + public XdsInitializationException(String message) { + super(message); + } + + public XdsInitializationException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/dubbo-xds/src/main/java/org/apache/dubbo/registry/xds/XdsRegistry.java b/dubbo-xds/src/main/java/org/apache/dubbo/registry/xds/XdsRegistry.java new file mode 100644 index 000000000..30e005ffe --- /dev/null +++ b/dubbo-xds/src/main/java/org/apache/dubbo/registry/xds/XdsRegistry.java @@ -0,0 +1,50 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.dubbo.registry.xds; + +import org.apache.dubbo.common.URL; +import org.apache.dubbo.registry.NotifyListener; +import org.apache.dubbo.registry.support.FailbackRegistry; + +/** + * Empty implements for xDS
+ * xDS only support `Service Discovery` mode register
+ * Used to compat past version like 2.6.x, 2.7.x with interface level register
+ * {@link XdsServiceDiscovery} is the real implementation of xDS + */ +public class XdsRegistry extends FailbackRegistry { + public XdsRegistry(URL url) { + super(url); + } + + @Override + public boolean isAvailable() { + return true; + } + + @Override + public void doRegister(URL url) {} + + @Override + public void doUnregister(URL url) {} + + @Override + public void doSubscribe(URL url, NotifyListener listener) {} + + @Override + public void doUnsubscribe(URL url, NotifyListener listener) {} +} diff --git a/dubbo-xds/src/main/java/org/apache/dubbo/registry/xds/XdsRegistryFactory.java b/dubbo-xds/src/main/java/org/apache/dubbo/registry/xds/XdsRegistryFactory.java new file mode 100644 index 000000000..8fa130b19 --- /dev/null +++ b/dubbo-xds/src/main/java/org/apache/dubbo/registry/xds/XdsRegistryFactory.java @@ -0,0 +1,34 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.dubbo.registry.xds; + +import org.apache.dubbo.common.URL; +import org.apache.dubbo.registry.Registry; +import org.apache.dubbo.registry.support.AbstractRegistryFactory; + +public class XdsRegistryFactory extends AbstractRegistryFactory { + + @Override + protected String createRegistryCacheKey(URL url) { + return url.toFullString(); + } + + @Override + protected Registry createRegistry(URL url) { + return new XdsRegistry(url); + } +} diff --git a/dubbo-xds/src/main/java/org/apache/dubbo/registry/xds/XdsServiceDiscovery.java b/dubbo-xds/src/main/java/org/apache/dubbo/registry/xds/XdsServiceDiscovery.java new file mode 100644 index 000000000..3385ff97a --- /dev/null +++ b/dubbo-xds/src/main/java/org/apache/dubbo/registry/xds/XdsServiceDiscovery.java @@ -0,0 +1,117 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.dubbo.registry.xds; + +import org.apache.dubbo.common.URL; +import org.apache.dubbo.common.logger.ErrorTypeAwareLogger; +import org.apache.dubbo.common.logger.LoggerFactory; +import org.apache.dubbo.registry.client.DefaultServiceInstance; +import org.apache.dubbo.registry.client.ReflectionBasedServiceDiscovery; +import org.apache.dubbo.registry.client.ServiceInstance; +import org.apache.dubbo.registry.client.event.listener.ServiceInstancesChangedListener; +import org.apache.dubbo.registry.xds.util.PilotExchanger; +import org.apache.dubbo.registry.xds.util.protocol.message.Endpoint; +import org.apache.dubbo.rpc.model.ApplicationModel; +import org.apache.dubbo.rpc.model.ScopeModelUtil; + +import java.util.Collection; +import java.util.Comparator; +import java.util.LinkedList; +import java.util.List; +import java.util.Set; + +import static org.apache.dubbo.common.constants.LoggerCodeConstants.REGISTRY_ERROR_INITIALIZE_XDS; +import static org.apache.dubbo.common.constants.LoggerCodeConstants.REGISTRY_ERROR_PARSING_XDS; + +public class XdsServiceDiscovery extends ReflectionBasedServiceDiscovery { + + private static final ErrorTypeAwareLogger logger = LoggerFactory.getErrorTypeAwareLogger(XdsServiceDiscovery.class); + + private PilotExchanger exchanger; + + public XdsServiceDiscovery(ApplicationModel applicationModel, URL registryURL) { + super(applicationModel, registryURL); + } + + @Override + public void doInitialize(URL registryURL) { + try { + exchanger = PilotExchanger.initialize(registryURL); + } catch (Throwable t) { + logger.error(REGISTRY_ERROR_INITIALIZE_XDS, "", "", t.getMessage(), t); + } + } + + @Override + public void doDestroy() { + try { + if (exchanger == null) { + return; + } + exchanger.destroy(); + } catch (Throwable t) { + logger.error(REGISTRY_ERROR_INITIALIZE_XDS, "", "", t.getMessage(), t); + } + } + + @Override + public Set getServices() { + return exchanger.getServices(); + } + + @Override + public List getInstances(String serviceName) throws NullPointerException { + Set endpoints = exchanger.getEndpoints(serviceName); + return changedToInstances(serviceName, endpoints); + } + + @Override + public void addServiceInstancesChangedListener(ServiceInstancesChangedListener listener) + throws NullPointerException, IllegalArgumentException { + listener.getServiceNames() + .forEach(serviceName -> exchanger.observeEndpoints( + serviceName, + (endpoints -> + notifyListener(serviceName, listener, changedToInstances(serviceName, endpoints))))); + } + + private List changedToInstances(String serviceName, Collection endpoints) { + List instances = new LinkedList<>(); + endpoints.forEach(endpoint -> { + try { + DefaultServiceInstance serviceInstance = new DefaultServiceInstance( + serviceName, + endpoint.getAddress(), + endpoint.getPortValue(), + ScopeModelUtil.getApplicationModel(getUrl().getScopeModel())); + // fill metadata by SelfHostMetaServiceDiscovery, will be fetched by RPC request + serviceInstance.putExtendParam("clusterName", endpoint.getClusterName()); + fillServiceInstance(serviceInstance); + instances.add(serviceInstance); + } catch (Throwable t) { + logger.error( + REGISTRY_ERROR_PARSING_XDS, + "", + "", + "Error occurred when parsing endpoints. Endpoints List:" + endpoints, + t); + } + }); + instances.sort(Comparator.comparingInt(ServiceInstance::hashCode)); + return instances; + } +} diff --git a/dubbo-xds/src/main/java/org/apache/dubbo/registry/xds/XdsServiceDiscoveryFactory.java b/dubbo-xds/src/main/java/org/apache/dubbo/registry/xds/XdsServiceDiscoveryFactory.java new file mode 100644 index 000000000..0f763c59c --- /dev/null +++ b/dubbo-xds/src/main/java/org/apache/dubbo/registry/xds/XdsServiceDiscoveryFactory.java @@ -0,0 +1,48 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.dubbo.registry.xds; + +import org.apache.dubbo.common.URL; +import org.apache.dubbo.common.logger.ErrorTypeAwareLogger; +import org.apache.dubbo.common.logger.LoggerFactory; +import org.apache.dubbo.registry.client.AbstractServiceDiscoveryFactory; +import org.apache.dubbo.registry.client.ServiceDiscovery; +import org.apache.dubbo.rpc.model.ApplicationModel; + +import static org.apache.dubbo.common.constants.LoggerCodeConstants.REGISTRY_ERROR_INITIALIZE_XDS; + +public class XdsServiceDiscoveryFactory extends AbstractServiceDiscoveryFactory { + + private static final ErrorTypeAwareLogger logger = + LoggerFactory.getErrorTypeAwareLogger(XdsServiceDiscoveryFactory.class); + + @Override + protected ServiceDiscovery createDiscovery(URL registryURL) { + XdsServiceDiscovery xdsServiceDiscovery = new XdsServiceDiscovery(ApplicationModel.defaultModel(), registryURL); + try { + xdsServiceDiscovery.doInitialize(registryURL); + } catch (Exception e) { + logger.error( + REGISTRY_ERROR_INITIALIZE_XDS, + "", + "", + "Error occurred when initialize xDS service discovery impl.", + e); + } + return xdsServiceDiscovery; + } +} diff --git a/dubbo-xds/src/main/java/org/apache/dubbo/registry/xds/istio/IstioCitadelCertificateSigner.java b/dubbo-xds/src/main/java/org/apache/dubbo/registry/xds/istio/IstioCitadelCertificateSigner.java new file mode 100644 index 000000000..95b653a05 --- /dev/null +++ b/dubbo-xds/src/main/java/org/apache/dubbo/registry/xds/istio/IstioCitadelCertificateSigner.java @@ -0,0 +1,294 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.dubbo.registry.xds.istio; + +import org.apache.dubbo.common.URL; +import org.apache.dubbo.common.logger.ErrorTypeAwareLogger; +import org.apache.dubbo.common.logger.LoggerFactory; +import org.apache.dubbo.common.utils.StringUtils; +import org.apache.dubbo.registry.xds.XdsCertificateSigner; +import org.apache.dubbo.rpc.RpcException; + +import io.grpc.ManagedChannel; +import io.grpc.Metadata; +import io.grpc.netty.shaded.io.grpc.netty.GrpcSslContexts; +import io.grpc.netty.shaded.io.grpc.netty.NettyChannelBuilder; +import io.grpc.netty.shaded.io.netty.handler.ssl.util.InsecureTrustManagerFactory; +import io.grpc.stub.MetadataUtils; +import io.grpc.stub.StreamObserver; +import istio.v1.auth.IstioCertificateRequest; +import istio.v1.auth.IstioCertificateResponse; +import istio.v1.auth.IstioCertificateServiceGrpc; +import org.bouncycastle.asn1.pkcs.PKCSObjectIdentifiers; +import org.bouncycastle.asn1.x500.X500Name; +import org.bouncycastle.asn1.x509.Extension; +import org.bouncycastle.asn1.x509.ExtensionsGenerator; +import org.bouncycastle.asn1.x509.GeneralName; +import org.bouncycastle.asn1.x509.GeneralNames; +import org.bouncycastle.openssl.jcajce.JcaPEMWriter; +import org.bouncycastle.operator.ContentSigner; +import org.bouncycastle.operator.OperatorCreationException; +import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; +import org.bouncycastle.pkcs.PKCS10CertificationRequest; +import org.bouncycastle.pkcs.jcajce.JcaPKCS10CertificationRequestBuilder; +import org.bouncycastle.util.io.pem.PemObject; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.StringWriter; +import java.nio.charset.StandardCharsets; +import java.security.InvalidAlgorithmParameterException; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.SecureRandom; +import java.security.spec.ECGenParameterSpec; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; + +import static org.apache.dubbo.common.constants.LoggerCodeConstants.REGISTRY_FAILED_GENERATE_CERT_ISTIO; +import static org.apache.dubbo.common.constants.LoggerCodeConstants.REGISTRY_FAILED_GENERATE_KEY_ISTIO; +import static org.apache.dubbo.common.constants.LoggerCodeConstants.REGISTRY_RECEIVE_ERROR_MSG_ISTIO; + +public class IstioCitadelCertificateSigner implements XdsCertificateSigner { + + private static final ErrorTypeAwareLogger logger = + LoggerFactory.getErrorTypeAwareLogger(IstioCitadelCertificateSigner.class); + + private final org.apache.dubbo.registry.xds.istio.IstioEnv istioEnv; + + private CertPair certPair; + + public IstioCitadelCertificateSigner() { + // watch cert, Refresh every 30s + ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(1); + scheduledThreadPool.scheduleAtFixedRate(new GenerateCertTask(), 0, 30, TimeUnit.SECONDS); + istioEnv = IstioEnv.getInstance(); + } + + @Override + public CertPair GenerateCert(URL url) { + + if (certPair != null && !certPair.isExpire()) { + return certPair; + } + return doGenerateCert(); + } + + private class GenerateCertTask implements Runnable { + @Override + public void run() { + doGenerateCert(); + } + } + + private CertPair doGenerateCert() { + synchronized (this) { + if (certPair == null || certPair.isExpire()) { + try { + certPair = createCert(); + } catch (IOException e) { + logger.error(REGISTRY_FAILED_GENERATE_CERT_ISTIO, "", "", "Generate Cert from Istio failed.", e); + throw new RpcException("Generate Cert from Istio failed.", e); + } + } + } + return certPair; + } + + public CertPair createCert() throws IOException { + PublicKey publicKey = null; + PrivateKey privateKey = null; + ContentSigner signer = null; + + if (istioEnv.isECCFirst()) { + try { + ECGenParameterSpec ecSpec = new ECGenParameterSpec("secp256r1"); + KeyPairGenerator g = KeyPairGenerator.getInstance("EC"); + g.initialize(ecSpec, new SecureRandom()); + KeyPair keypair = g.generateKeyPair(); + publicKey = keypair.getPublic(); + privateKey = keypair.getPrivate(); + signer = new JcaContentSignerBuilder("SHA256withECDSA").build(keypair.getPrivate()); + } catch (NoSuchAlgorithmException | InvalidAlgorithmParameterException | OperatorCreationException e) { + logger.error( + REGISTRY_FAILED_GENERATE_KEY_ISTIO, + "", + "", + "Generate Key with secp256r1 algorithm failed. Please check if your system support. " + + "Will attempt to generate with RSA2048.", + e); + } + } + + if (publicKey == null) { + try { + KeyPairGenerator kpGenerator = KeyPairGenerator.getInstance("RSA"); + kpGenerator.initialize(istioEnv.getRasKeySize()); + KeyPair keypair = kpGenerator.generateKeyPair(); + publicKey = keypair.getPublic(); + privateKey = keypair.getPrivate(); + signer = new JcaContentSignerBuilder("SHA256WithRSA").build(keypair.getPrivate()); + } catch (NoSuchAlgorithmException | OperatorCreationException e) { + logger.error( + REGISTRY_FAILED_GENERATE_KEY_ISTIO, + "", + "", + "Generate Key with SHA256WithRSA algorithm failed. Please check if your system support.", + e); + throw new RpcException(e); + } + } + + String csr = generateCsr(publicKey, signer); + String caCert = istioEnv.getCaCert(); + ManagedChannel channel; + if (StringUtils.isNotEmpty(caCert)) { + channel = NettyChannelBuilder.forTarget(istioEnv.getCaAddr()) + .sslContext(GrpcSslContexts.forClient() + .trustManager(new ByteArrayInputStream(caCert.getBytes(StandardCharsets.UTF_8))) + .build()) + .build(); + } else { + channel = NettyChannelBuilder.forTarget(istioEnv.getCaAddr()) + .sslContext(GrpcSslContexts.forClient() + .trustManager(InsecureTrustManagerFactory.INSTANCE) + .build()) + .build(); + } + + Metadata header = new Metadata(); + Metadata.Key key = Metadata.Key.of("authorization", Metadata.ASCII_STRING_MARSHALLER); + header.put(key, "Bearer " + istioEnv.getServiceAccount()); + + key = Metadata.Key.of("ClusterID", Metadata.ASCII_STRING_MARSHALLER); + header.put(key, istioEnv.getIstioMetaClusterId()); + + IstioCertificateServiceGrpc.IstioCertificateServiceStub stub = IstioCertificateServiceGrpc.newStub(channel); + + stub = stub.withInterceptors(MetadataUtils.newAttachHeadersInterceptor(header)); + + CountDownLatch countDownLatch = new CountDownLatch(1); + StringBuffer publicKeyBuilder = new StringBuffer(); + AtomicBoolean failed = new AtomicBoolean(false); + stub.createCertificate( + generateRequest(csr), generateResponseObserver(countDownLatch, publicKeyBuilder, failed)); + + long expireTime = + System.currentTimeMillis() + (long) (istioEnv.getSecretTTL() * istioEnv.getSecretGracePeriodRatio()); + + try { + countDownLatch.await(); + } catch (InterruptedException e) { + throw new RpcException("Generate Cert Failed. Wait for cert failed.", e); + } + + if (failed.get()) { + throw new RpcException("Generate Cert Failed. Send csr request failed. Please check log above."); + } + + String privateKeyPem = generatePrivatePemKey(privateKey); + CertPair certPair = + new CertPair(privateKeyPem, publicKeyBuilder.toString(), System.currentTimeMillis(), expireTime); + + channel.shutdown(); + return certPair; + } + + private IstioCertificateRequest generateRequest(String csr) { + return IstioCertificateRequest.newBuilder() + .setCsr(csr) + .setValidityDuration(istioEnv.getSecretTTL()) + .build(); + } + + private StreamObserver generateResponseObserver( + CountDownLatch countDownLatch, StringBuffer publicKeyBuilder, AtomicBoolean failed) { + return new StreamObserver() { + @Override + public void onNext(IstioCertificateResponse istioCertificateResponse) { + for (int i = 0; i < istioCertificateResponse.getCertChainCount(); i++) { + publicKeyBuilder.append( + istioCertificateResponse.getCertChainBytes(i).toStringUtf8()); + } + if (logger.isDebugEnabled()) { + logger.debug("Receive Cert chain from Istio Citadel. \n" + publicKeyBuilder); + } + countDownLatch.countDown(); + } + + @Override + public void onError(Throwable throwable) { + failed.set(true); + logger.error( + REGISTRY_RECEIVE_ERROR_MSG_ISTIO, + "", + "", + "Receive error message from Istio Citadel grpc stub.", + throwable); + countDownLatch.countDown(); + } + + @Override + public void onCompleted() { + countDownLatch.countDown(); + } + }; + } + + private String generatePrivatePemKey(PrivateKey privateKey) throws IOException { + String key = generatePemKey("RSA PRIVATE KEY", privateKey.getEncoded()); + if (logger.isDebugEnabled()) { + logger.debug("Generated Private Key. \n" + key); + } + return key; + } + + private String generatePemKey(String type, byte[] content) throws IOException { + PemObject pemObject = new PemObject(type, content); + StringWriter str = new StringWriter(); + JcaPEMWriter jcaPEMWriter = new JcaPEMWriter(str); + jcaPEMWriter.writeObject(pemObject); + jcaPEMWriter.close(); + str.close(); + return str.toString(); + } + + private String generateCsr(PublicKey publicKey, ContentSigner signer) throws IOException { + GeneralNames subjectAltNames = new GeneralNames(new GeneralName[] {new GeneralName(6, istioEnv.getCsrHost())}); + + ExtensionsGenerator extGen = new ExtensionsGenerator(); + extGen.addExtension(Extension.subjectAlternativeName, true, subjectAltNames); + + PKCS10CertificationRequest request = new JcaPKCS10CertificationRequestBuilder( + new X500Name("O=" + istioEnv.getTrustDomain()), publicKey) + .addAttribute(PKCSObjectIdentifiers.pkcs_9_at_extensionRequest, extGen.generate()) + .build(signer); + + String csr = generatePemKey("CERTIFICATE REQUEST", request.getEncoded()); + + if (logger.isDebugEnabled()) { + logger.debug("CSR Request to Istio Citadel. \n" + csr); + } + return csr; + } +} diff --git a/dubbo-xds/src/main/java/org/apache/dubbo/registry/xds/istio/IstioConstant.java b/dubbo-xds/src/main/java/org/apache/dubbo/registry/xds/istio/IstioConstant.java new file mode 100644 index 000000000..018fa3a0f --- /dev/null +++ b/dubbo-xds/src/main/java/org/apache/dubbo/registry/xds/istio/IstioConstant.java @@ -0,0 +1,109 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.dubbo.registry.xds.istio; + +public class IstioConstant { + /** + * Address of the spiffe certificate provider. Defaults to discoveryAddress + */ + public static final String CA_ADDR_KEY = "CA_ADDR"; + + /** + * CA and xDS services + */ + public static final String DEFAULT_CA_ADDR = "istiod.istio-system.svc:15012"; + + /** + * The trust domain for spiffe certificates + */ + public static final String TRUST_DOMAIN_KEY = "TRUST_DOMAIN"; + + /** + * The trust domain for spiffe certificates default value + */ + public static final String DEFAULT_TRUST_DOMAIN = "cluster.local"; + + public static final String WORKLOAD_NAMESPACE_KEY = "WORKLOAD_NAMESPACE"; + + public static final String DEFAULT_WORKLOAD_NAMESPACE = "default"; + + /** + * k8s jwt token + */ + public static final String KUBERNETES_SA_PATH = "/var/run/secrets/kubernetes.io/serviceaccount/token"; + + public static final String KUBERNETES_CA_PATH = "/var/run/secrets/kubernetes.io/serviceaccount/ca.crt"; + + public static final String ISTIO_SA_PATH = "/var/run/secrets/tokens/istio-token"; + + public static final String ISTIO_CA_PATH = "/var/run/secrets/istio/root-cert.pem"; + + public static final String KUBERNETES_NAMESPACE_PATH = "/var/run/secrets/kubernetes.io/serviceaccount/namespace"; + + public static final String RSA_KEY_SIZE_KEY = "RSA_KEY_SIZE"; + + public static final String DEFAULT_RSA_KEY_SIZE = "2048"; + + /** + * The type of ECC signature algorithm to use when generating private keys + */ + public static final String ECC_SIG_ALG_KEY = "ECC_SIGNATURE_ALGORITHM"; + + public static final String DEFAULT_ECC_SIG_ALG = "ECDSA"; + + /** + * The cert lifetime requested by istio agent + */ + public static final String SECRET_TTL_KEY = "SECRET_TTL"; + + /** + * The cert lifetime default value 24h0m0s + */ + public static final String DEFAULT_SECRET_TTL = "86400"; // 24 * 60 * 60 + + /** + * The grace period ratio for the cert rotation + */ + public static final String SECRET_GRACE_PERIOD_RATIO_KEY = "SECRET_GRACE_PERIOD_RATIO"; + + /** + * The grace period ratio for the cert rotation, by default 0.5 + */ + public static final String DEFAULT_SECRET_GRACE_PERIOD_RATIO = "0.5"; + + public static final String ISTIO_META_CLUSTER_ID_KEY = "ISTIO_META_CLUSTER_ID"; + + public static final String PILOT_CERT_PROVIDER_KEY = "PILOT_CERT_PROVIDER"; + + public static final String ISTIO_PILOT_CERT_PROVIDER = "istiod"; + + public static final String DEFAULT_ISTIO_META_CLUSTER_ID = "Kubernetes"; + + public static final String SPIFFE = "spiffe://"; + + public static final String NS = "/ns/"; + + public static final String SA = "/sa/"; + + public static final String JWT_POLICY = "JWT_POLICY"; + + public static final String DEFAULT_JWT_POLICY = "first-party-jwt"; + + public static final String FIRST_PARTY_JWT = "first-party-jwt"; + + public static final String THIRD_PARTY_JWT = "third-party-jwt"; +} diff --git a/dubbo-xds/src/main/java/org/apache/dubbo/registry/xds/istio/IstioEnv.java b/dubbo-xds/src/main/java/org/apache/dubbo/registry/xds/istio/IstioEnv.java new file mode 100644 index 000000000..43b454bf6 --- /dev/null +++ b/dubbo-xds/src/main/java/org/apache/dubbo/registry/xds/istio/IstioEnv.java @@ -0,0 +1,195 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.dubbo.registry.xds.istio; + +import org.apache.dubbo.common.constants.LoggerCodeConstants; +import org.apache.dubbo.common.logger.ErrorTypeAwareLogger; +import org.apache.dubbo.common.logger.LoggerFactory; +import org.apache.dubbo.registry.xds.XdsEnv; + +import org.apache.commons.io.FileUtils; + +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.Optional; + +import static org.apache.dubbo.common.constants.LoggerCodeConstants.REGISTRY_ERROR_READ_FILE_ISTIO; +import static org.apache.dubbo.registry.xds.istio.IstioConstant.NS; +import static org.apache.dubbo.registry.xds.istio.IstioConstant.SA; +import static org.apache.dubbo.registry.xds.istio.IstioConstant.SPIFFE; + +public class IstioEnv implements XdsEnv { + private static final ErrorTypeAwareLogger logger = LoggerFactory.getErrorTypeAwareLogger(IstioEnv.class); + + private static final IstioEnv INSTANCE = new IstioEnv(); + + private String podName; + + private String caAddr; + + private String jwtPolicy; + + private String trustDomain; + + private String workloadNameSpace; + + private int rasKeySize; + + private String eccSigAlg; + + private int secretTTL; + + private float secretGracePeriodRatio; + + private String istioMetaClusterId; + + private String pilotCertProvider; + + private IstioEnv() { + jwtPolicy = + Optional.ofNullable(System.getenv(IstioConstant.JWT_POLICY)).orElse(IstioConstant.DEFAULT_JWT_POLICY); + podName = Optional.ofNullable(System.getenv("POD_NAME")).orElse(System.getenv("HOSTNAME")); + trustDomain = Optional.ofNullable(System.getenv(IstioConstant.TRUST_DOMAIN_KEY)) + .orElse(IstioConstant.DEFAULT_TRUST_DOMAIN); + workloadNameSpace = Optional.ofNullable(System.getenv(IstioConstant.WORKLOAD_NAMESPACE_KEY)) + .orElseGet(() -> { + File namespaceFile = new File(IstioConstant.KUBERNETES_NAMESPACE_PATH); + if (namespaceFile.canRead()) { + try { + return FileUtils.readFileToString(namespaceFile, StandardCharsets.UTF_8); + } catch (IOException e) { + logger.error(REGISTRY_ERROR_READ_FILE_ISTIO, "", "", "read namespace file error", e); + } + } + return IstioConstant.DEFAULT_WORKLOAD_NAMESPACE; + }); + caAddr = Optional.ofNullable(System.getenv(IstioConstant.CA_ADDR_KEY)).orElse(IstioConstant.DEFAULT_CA_ADDR); + rasKeySize = Integer.parseInt(Optional.ofNullable(System.getenv(IstioConstant.RSA_KEY_SIZE_KEY)) + .orElse(IstioConstant.DEFAULT_RSA_KEY_SIZE)); + eccSigAlg = Optional.ofNullable(System.getenv(IstioConstant.ECC_SIG_ALG_KEY)) + .orElse(IstioConstant.DEFAULT_ECC_SIG_ALG); + secretTTL = Integer.parseInt(Optional.ofNullable(System.getenv(IstioConstant.SECRET_TTL_KEY)) + .orElse(IstioConstant.DEFAULT_SECRET_TTL)); + secretGracePeriodRatio = + Float.parseFloat(Optional.ofNullable(System.getenv(IstioConstant.SECRET_GRACE_PERIOD_RATIO_KEY)) + .orElse(IstioConstant.DEFAULT_SECRET_GRACE_PERIOD_RATIO)); + istioMetaClusterId = Optional.ofNullable(System.getenv(IstioConstant.ISTIO_META_CLUSTER_ID_KEY)) + .orElse(IstioConstant.DEFAULT_ISTIO_META_CLUSTER_ID); + pilotCertProvider = Optional.ofNullable(System.getenv(IstioConstant.PILOT_CERT_PROVIDER_KEY)) + .orElse(""); + + if (getServiceAccount() == null) { + throw new UnsupportedOperationException("Unable to found kubernetes service account token file. " + + "Please check if work in Kubernetes and mount service account token file correctly."); + } + } + + public static IstioEnv getInstance() { + return INSTANCE; + } + + public String getPodName() { + return podName; + } + + public String getCaAddr() { + return caAddr; + } + + public String getServiceAccount() { + File saFile; + switch (jwtPolicy) { + case IstioConstant.FIRST_PARTY_JWT: + saFile = new File(IstioConstant.KUBERNETES_SA_PATH); + break; + case IstioConstant.THIRD_PARTY_JWT: + default: + saFile = new File(IstioConstant.ISTIO_SA_PATH); + } + if (saFile.canRead()) { + try { + return FileUtils.readFileToString(saFile, StandardCharsets.UTF_8); + } catch (IOException e) { + logger.error( + LoggerCodeConstants.REGISTRY_ISTIO_EXCEPTION, + "File Read Failed", + "", + "Unable to read token file.", + e); + } + } + + return null; + } + + public String getCsrHost() { + // spiffe:///ns//sa/ + return SPIFFE + trustDomain + NS + workloadNameSpace + SA + getServiceAccount(); + } + + public String getTrustDomain() { + return trustDomain; + } + + public String getWorkloadNameSpace() { + return workloadNameSpace; + } + + @Override + public String getCluster() { + return null; + } + + public int getRasKeySize() { + return rasKeySize; + } + + public boolean isECCFirst() { + return IstioConstant.DEFAULT_ECC_SIG_ALG.equals(eccSigAlg); + } + + public int getSecretTTL() { + return secretTTL; + } + + public float getSecretGracePeriodRatio() { + return secretGracePeriodRatio; + } + + public String getIstioMetaClusterId() { + return istioMetaClusterId; + } + + public String getCaCert() { + File caFile; + if (IstioConstant.ISTIO_PILOT_CERT_PROVIDER.equals(pilotCertProvider)) { + caFile = new File(IstioConstant.ISTIO_CA_PATH); + } else { + return null; + } + if (caFile.canRead()) { + try { + return FileUtils.readFileToString(caFile, StandardCharsets.UTF_8); + } catch (IOException e) { + logger.error( + LoggerCodeConstants.REGISTRY_ISTIO_EXCEPTION, "File Read Failed", "", "read ca file error", e); + } + } + return null; + } +} diff --git a/dubbo-xds/src/main/java/org/apache/dubbo/registry/xds/util/AdsObserver.java b/dubbo-xds/src/main/java/org/apache/dubbo/registry/xds/util/AdsObserver.java new file mode 100644 index 000000000..39eee6660 --- /dev/null +++ b/dubbo-xds/src/main/java/org/apache/dubbo/registry/xds/util/AdsObserver.java @@ -0,0 +1,140 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.dubbo.registry.xds.util; + +import org.apache.dubbo.common.URL; +import org.apache.dubbo.common.logger.ErrorTypeAwareLogger; +import org.apache.dubbo.common.logger.LoggerFactory; +import org.apache.dubbo.common.threadpool.manager.FrameworkExecutorRepository; +import org.apache.dubbo.registry.xds.util.protocol.AbstractProtocol; +import org.apache.dubbo.registry.xds.util.protocol.DeltaResource; +import org.apache.dubbo.rpc.model.ApplicationModel; + +import io.envoyproxy.envoy.config.core.v3.Node; +import io.envoyproxy.envoy.service.discovery.v3.DiscoveryRequest; +import io.envoyproxy.envoy.service.discovery.v3.DiscoveryResponse; +import io.grpc.stub.StreamObserver; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +import static org.apache.dubbo.common.constants.LoggerCodeConstants.REGISTRY_ERROR_REQUEST_XDS; + +public class AdsObserver { + private static final ErrorTypeAwareLogger logger = LoggerFactory.getErrorTypeAwareLogger(AdsObserver.class); + private final ApplicationModel applicationModel; + private final URL url; + private final Node node; + private volatile XdsChannel xdsChannel; + + private final Map listeners = new ConcurrentHashMap<>(); + + protected StreamObserver requestObserver; + + private final Map observedResources = new ConcurrentHashMap<>(); + + public AdsObserver(URL url, Node node) { + this.url = url; + this.node = node; + this.xdsChannel = new XdsChannel(url); + this.applicationModel = url.getOrDefaultApplicationModel(); + } + + public > void addListener(AbstractProtocol protocol) { + listeners.put(protocol.getTypeUrl(), protocol); + } + + public void request(DiscoveryRequest discoveryRequest) { + if (requestObserver == null) { + requestObserver = xdsChannel.createDeltaDiscoveryRequest(new ResponseObserver(this)); + } + requestObserver.onNext(discoveryRequest); + observedResources.put(discoveryRequest.getTypeUrl(), discoveryRequest); + } + + private static class ResponseObserver implements StreamObserver { + private AdsObserver adsObserver; + + public ResponseObserver(AdsObserver adsObserver) { + this.adsObserver = adsObserver; + } + + @Override + public void onNext(DiscoveryResponse discoveryResponse) { + XdsListener xdsListener = adsObserver.listeners.get(discoveryResponse.getTypeUrl()); + xdsListener.process(discoveryResponse); + adsObserver.requestObserver.onNext(buildAck(discoveryResponse)); + } + + protected DiscoveryRequest buildAck(DiscoveryResponse response) { + // for ACK + return DiscoveryRequest.newBuilder() + .setNode(adsObserver.node) + .setTypeUrl(response.getTypeUrl()) + .setVersionInfo(response.getVersionInfo()) + .setResponseNonce(response.getNonce()) + .addAllResourceNames(adsObserver + .observedResources + .get(response.getTypeUrl()) + .getResourceNamesList()) + .build(); + } + + @Override + public void onError(Throwable throwable) { + logger.error(REGISTRY_ERROR_REQUEST_XDS, "", "", "xDS Client received error message! detail:", throwable); + adsObserver.triggerReConnectTask(); + } + + @Override + public void onCompleted() { + logger.info("xDS Client completed"); + adsObserver.triggerReConnectTask(); + } + } + + private void triggerReConnectTask() { + ScheduledExecutorService scheduledFuture = applicationModel + .getFrameworkModel() + .getBeanFactory() + .getBean(FrameworkExecutorRepository.class) + .getSharedScheduledExecutor(); + scheduledFuture.schedule(this::recover, 3, TimeUnit.SECONDS); + } + + private void recover() { + try { + xdsChannel = new XdsChannel(url); + if (xdsChannel.getChannel() != null) { + requestObserver = xdsChannel.createDeltaDiscoveryRequest(new ResponseObserver(this)); + observedResources.values().forEach(requestObserver::onNext); + return; + } else { + logger.error( + REGISTRY_ERROR_REQUEST_XDS, + "", + "", + "Recover failed for xDS connection. Will retry. Create channel failed."); + } + } catch (Exception e) { + logger.error(REGISTRY_ERROR_REQUEST_XDS, "", "", "Recover failed for xDS connection. Will retry.", e); + } + triggerReConnectTask(); + } +} diff --git a/dubbo-xds/src/main/java/org/apache/dubbo/registry/xds/util/NodeBuilder.java b/dubbo-xds/src/main/java/org/apache/dubbo/registry/xds/util/NodeBuilder.java new file mode 100644 index 000000000..eaa88f390 --- /dev/null +++ b/dubbo-xds/src/main/java/org/apache/dubbo/registry/xds/util/NodeBuilder.java @@ -0,0 +1,43 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.dubbo.registry.xds.util; + +import org.apache.dubbo.common.utils.NetUtils; +import org.apache.dubbo.registry.xds.istio.IstioEnv; + +import io.envoyproxy.envoy.config.core.v3.Node; + +public class NodeBuilder { + + private static final String SVC_CLUSTER_LOCAL = ".svc.cluster.local"; + + public static Node build() { + // String podName = System.getenv("metadata.name"); + // String podNamespace = System.getenv("metadata.namespace"); + + String podName = IstioEnv.getInstance().getPodName(); + String podNamespace = IstioEnv.getInstance().getWorkloadNameSpace(); + String svcName = IstioEnv.getInstance().getIstioMetaClusterId(); + + // id -> sidecar~ip~{POD_NAME}~{NAMESPACE_NAME}.svc.cluster.local + // cluster -> {SVC_NAME} + return Node.newBuilder() + .setId("sidecar~" + NetUtils.getLocalHost() + "~" + podName + "~" + podNamespace + SVC_CLUSTER_LOCAL) + .setCluster(svcName) + .build(); + } +} diff --git a/dubbo-xds/src/main/java/org/apache/dubbo/registry/xds/util/PilotExchanger.java b/dubbo-xds/src/main/java/org/apache/dubbo/registry/xds/util/PilotExchanger.java new file mode 100644 index 000000000..87fa81083 --- /dev/null +++ b/dubbo-xds/src/main/java/org/apache/dubbo/registry/xds/util/PilotExchanger.java @@ -0,0 +1,250 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.dubbo.registry.xds.util; + +import org.apache.dubbo.common.URL; +import org.apache.dubbo.common.threadpool.manager.FrameworkExecutorRepository; +import org.apache.dubbo.common.utils.CollectionUtils; +import org.apache.dubbo.common.utils.ConcurrentHashSet; +import org.apache.dubbo.registry.xds.util.protocol.AbstractProtocol; +import org.apache.dubbo.registry.xds.util.protocol.impl.EdsProtocol; +import org.apache.dubbo.registry.xds.util.protocol.impl.LdsProtocol; +import org.apache.dubbo.registry.xds.util.protocol.impl.RdsProtocol; +import org.apache.dubbo.registry.xds.util.protocol.message.Endpoint; +import org.apache.dubbo.registry.xds.util.protocol.message.EndpointResult; +import org.apache.dubbo.registry.xds.util.protocol.message.ListenerResult; +import org.apache.dubbo.registry.xds.util.protocol.message.RouteResult; +import org.apache.dubbo.rpc.cluster.router.xds.RdsVirtualHostListener; +import org.apache.dubbo.rpc.model.ApplicationModel; + +import java.util.Collections; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Consumer; +import java.util.stream.Collectors; + +public class PilotExchanger { + + protected final XdsChannel xdsChannel; + + protected final LdsProtocol ldsProtocol; + + protected final RdsProtocol rdsProtocol; + + protected final EdsProtocol edsProtocol; + + protected Map listenerResult; + + protected Map routeResult; + + private final AtomicBoolean isRdsObserve = new AtomicBoolean(false); + private final Set domainObserveRequest = new ConcurrentHashSet(); + + private final Map>>> domainObserveConsumer = new ConcurrentHashMap<>(); + + private final Map> rdsObserveConsumer = new ConcurrentHashMap<>(); + + private static PilotExchanger GLOBAL_PILOT_EXCHANGER = null; + + private final ApplicationModel applicationModel; + + protected PilotExchanger(URL url) { + xdsChannel = new XdsChannel(url); + int pollingTimeout = url.getParameter("pollingTimeout", 10); + this.applicationModel = url.getOrDefaultApplicationModel(); + AdsObserver adsObserver = new AdsObserver(url, NodeBuilder.build()); + this.ldsProtocol = new LdsProtocol(adsObserver, NodeBuilder.build(), pollingTimeout); + this.rdsProtocol = new RdsProtocol(adsObserver, NodeBuilder.build(), pollingTimeout); + this.edsProtocol = new EdsProtocol(adsObserver, NodeBuilder.build(), pollingTimeout); + + this.listenerResult = ldsProtocol.getListeners(); + this.routeResult = rdsProtocol.getResource( + listenerResult.values().iterator().next().getRouteConfigNames()); + Set ldsResourcesName = new HashSet<>(); + ldsResourcesName.add(AbstractProtocol.emptyResourceName); + // Observer RDS update + if (CollectionUtils.isNotEmpty(listenerResult.values().iterator().next().getRouteConfigNames())) { + createRouteObserve(); + isRdsObserve.set(true); + } + // Observe LDS updated + ldsProtocol.observeResource( + ldsResourcesName, + (newListener) -> { + // update local cache + if (!newListener.equals(listenerResult)) { + this.listenerResult = newListener; + // update RDS observation + if (isRdsObserve.get()) { + createRouteObserve(); + } + } + }, + false); + } + + private void createRouteObserve() { + rdsProtocol.observeResource( + listenerResult.values().iterator().next().getRouteConfigNames(), + (newResult) -> { + // check if observed domain update ( will update endpoint observation ) + List domainsToUpdate = new LinkedList<>(); + domainObserveConsumer.forEach((domain, consumer) -> { + newResult.values().forEach(o -> { + Set newRoute = o.searchDomain(domain); + for (Map.Entry entry : routeResult.entrySet()) { + if (!entry.getValue().searchDomain(domain).equals(newRoute)) { + // routers in observed domain has been updated + // Long domainRequest = domainObserveRequest.get(domain); + // router list is empty when observeEndpoints() called and domainRequest has not + // been created yet + // create new observation + domainsToUpdate.add(domain); + // doObserveEndpoints(domain); + } + } + }); + }); + routeResult = newResult; + ExecutorService executorService = applicationModel + .getFrameworkModel() + .getBeanFactory() + .getBean(FrameworkExecutorRepository.class) + .getSharedExecutor(); + executorService.submit(() -> domainsToUpdate.forEach(this::doObserveEndpoints)); + }, + false); + } + + public static PilotExchanger initialize(URL url) { + synchronized (PilotExchanger.class) { + if (GLOBAL_PILOT_EXCHANGER != null) { + return GLOBAL_PILOT_EXCHANGER; + } + return (GLOBAL_PILOT_EXCHANGER = new PilotExchanger(url)); + } + } + + public static PilotExchanger getInstance() { + synchronized (PilotExchanger.class) { + return GLOBAL_PILOT_EXCHANGER; + } + } + + public static boolean isEnabled() { + return GLOBAL_PILOT_EXCHANGER != null; + } + + public void destroy() { + xdsChannel.destroy(); + } + + public Set getServices() { + Set domains = new HashSet<>(); + for (Map.Entry entry : routeResult.entrySet()) { + domains.addAll(entry.getValue().getDomains()); + } + return domains; + } + + public Set getEndpoints(String domain) { + Set endpoints = new HashSet<>(); + for (Map.Entry entry : routeResult.entrySet()) { + Set cluster = entry.getValue().searchDomain(domain); + if (CollectionUtils.isNotEmpty(cluster)) { + Map endpointResultList = edsProtocol.getResource(cluster); + endpointResultList.forEach((k, v) -> endpoints.addAll(v.getEndpoints())); + } else { + return Collections.emptySet(); + } + } + return endpoints; + } + + public void observeEndpoints(String domain, Consumer> consumer) { + // store Consumer + domainObserveConsumer.compute(domain, (k, v) -> { + if (v == null) { + v = new ConcurrentHashSet<>(); + } + // support multi-consumer + v.add(consumer); + return v; + }); + if (!domainObserveRequest.contains(domain)) { + doObserveEndpoints(domain); + } + } + + private void doObserveEndpoints(String domain) { + for (Map.Entry entry : routeResult.entrySet()) { + Set router = entry.getValue().searchDomain(domain); + // if router is empty, do nothing + // observation will be created when RDS updates + if (CollectionUtils.isNotEmpty(router)) { + edsProtocol.observeResource( + router, + (endpointResultMap) -> { + Set endpoints = endpointResultMap.values().stream() + .map(EndpointResult::getEndpoints) + .flatMap(Set::stream) + .collect(Collectors.toSet()); + for (Consumer> consumer : domainObserveConsumer.get(domain)) { + consumer.accept(endpoints); + } + }, + false); + domainObserveRequest.add(domain); + } + } + } + + public void unObserveEndpoints(String domain, Consumer> consumer) { + domainObserveConsumer.get(domain).remove(consumer); + domainObserveRequest.remove(domain); + } + + public void observeEds(Set clusterNames, Consumer> consumer) { + edsProtocol.observeResource(clusterNames, consumer, false); + } + + public void unObserveEds(Set clusterNames, Consumer> consumer) { + edsProtocol.unobserveResource(clusterNames, consumer); + } + + public void observeRds(Set clusterNames, Consumer> consumer) { + rdsProtocol.observeResource(clusterNames, consumer, false); + } + + public void unObserveRds(Set clusterNames, Consumer> consumer) { + rdsProtocol.unobserveResource(clusterNames, consumer); + } + + public void observeLds(Consumer> consumer) { + ldsProtocol.observeResource(Collections.singleton(AbstractProtocol.emptyResourceName), consumer, false); + } + + public void unObserveLds(Consumer> consumer) { + ldsProtocol.unobserveResource(Collections.singleton(AbstractProtocol.emptyResourceName), consumer); + } +} diff --git a/dubbo-xds/src/main/java/org/apache/dubbo/registry/xds/util/XdsChannel.java b/dubbo-xds/src/main/java/org/apache/dubbo/registry/xds/util/XdsChannel.java new file mode 100644 index 000000000..94ba125b9 --- /dev/null +++ b/dubbo-xds/src/main/java/org/apache/dubbo/registry/xds/util/XdsChannel.java @@ -0,0 +1,142 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.dubbo.registry.xds.util; + +import org.apache.dubbo.common.URL; +import org.apache.dubbo.common.logger.ErrorTypeAwareLogger; +import org.apache.dubbo.common.logger.LoggerFactory; +import org.apache.dubbo.common.url.component.URLAddress; +import org.apache.dubbo.registry.xds.XdsCertificateSigner; +import org.apache.dubbo.registry.xds.util.bootstrap.Bootstrapper; +import org.apache.dubbo.registry.xds.util.bootstrap.BootstrapperImpl; + +import io.envoyproxy.envoy.service.discovery.v3.AggregatedDiscoveryServiceGrpc; +import io.envoyproxy.envoy.service.discovery.v3.DeltaDiscoveryRequest; +import io.envoyproxy.envoy.service.discovery.v3.DeltaDiscoveryResponse; +import io.envoyproxy.envoy.service.discovery.v3.DiscoveryRequest; +import io.envoyproxy.envoy.service.discovery.v3.DiscoveryResponse; +import io.grpc.ManagedChannel; +import io.grpc.netty.shaded.io.grpc.netty.GrpcSslContexts; +import io.grpc.netty.shaded.io.grpc.netty.NettyChannelBuilder; +import io.grpc.netty.shaded.io.netty.channel.epoll.EpollDomainSocketChannel; +import io.grpc.netty.shaded.io.netty.channel.epoll.EpollEventLoopGroup; +import io.grpc.netty.shaded.io.netty.channel.unix.DomainSocketAddress; +import io.grpc.netty.shaded.io.netty.handler.ssl.SslContext; +import io.grpc.netty.shaded.io.netty.handler.ssl.util.InsecureTrustManagerFactory; +import io.grpc.stub.StreamObserver; + +import java.io.ByteArrayInputStream; +import java.nio.charset.StandardCharsets; + +import static org.apache.dubbo.common.constants.LoggerCodeConstants.REGISTRY_ERROR_CREATE_CHANNEL_XDS; + +public class XdsChannel { + + private static final ErrorTypeAwareLogger logger = LoggerFactory.getErrorTypeAwareLogger(XdsChannel.class); + + private static final String USE_AGENT = "use-agent"; + + private URL url; + + private static final String SECURE = "secure"; + + private static final String PLAINTEXT = "plaintext"; + + private final ManagedChannel channel; + + public URL getUrl() { + return url; + } + + public ManagedChannel getChannel() { + return channel; + } + + public XdsChannel(URL url) { + ManagedChannel managedChannel = null; + this.url = url; + try { + if (!url.getParameter(USE_AGENT, false)) { + if (PLAINTEXT.equals(url.getParameter(SECURE))) { + managedChannel = NettyChannelBuilder.forAddress(url.getHost(), url.getPort()) + .usePlaintext() + .build(); + } else { + XdsCertificateSigner signer = url.getOrDefaultApplicationModel() + .getExtensionLoader(XdsCertificateSigner.class) + .getExtension(url.getParameter("signer", "istio")); + XdsCertificateSigner.CertPair certPair = signer.GenerateCert(url); + SslContext context = GrpcSslContexts.forClient() + .trustManager(InsecureTrustManagerFactory.INSTANCE) + .keyManager( + new ByteArrayInputStream( + certPair.getPublicKey().getBytes(StandardCharsets.UTF_8)), + new ByteArrayInputStream( + certPair.getPrivateKey().getBytes(StandardCharsets.UTF_8))) + .build(); + managedChannel = NettyChannelBuilder.forAddress(url.getHost(), url.getPort()) + .sslContext(context) + .build(); + } + } else { + BootstrapperImpl bootstrapper = new BootstrapperImpl(); + Bootstrapper.BootstrapInfo bootstrapInfo = bootstrapper.bootstrap(); + URLAddress address = + URLAddress.parse(bootstrapInfo.servers().get(0).target(), null, false); + EpollEventLoopGroup elg = new EpollEventLoopGroup(); + managedChannel = NettyChannelBuilder.forAddress(new DomainSocketAddress("/" + address.getPath())) + .eventLoopGroup(elg) + .channelType(EpollDomainSocketChannel.class) + .usePlaintext() + .build(); + } + } catch (Exception e) { + logger.error( + REGISTRY_ERROR_CREATE_CHANNEL_XDS, + "", + "", + "Error occurred when creating gRPC channel to control panel.", + e); + } + channel = managedChannel; + } + + public StreamObserver observeDeltaDiscoveryRequest( + StreamObserver observer) { + return AggregatedDiscoveryServiceGrpc.newStub(channel).deltaAggregatedResources(observer); + } + + public StreamObserver createDeltaDiscoveryRequest(StreamObserver observer) { + return AggregatedDiscoveryServiceGrpc.newStub(channel).streamAggregatedResources(observer); + } + + public StreamObserver observeDeltaDiscoveryRequestV2( + StreamObserver observer) { + return io.envoyproxy.envoy.service.discovery.v2.AggregatedDiscoveryServiceGrpc.newStub(channel) + .deltaAggregatedResources(observer); + } + + public StreamObserver createDeltaDiscoveryRequestV2( + StreamObserver observer) { + return io.envoyproxy.envoy.service.discovery.v2.AggregatedDiscoveryServiceGrpc.newStub(channel) + .streamAggregatedResources(observer); + } + + public void destroy() { + channel.shutdown(); + } +} diff --git a/dubbo-xds/src/main/java/org/apache/dubbo/registry/xds/util/XdsListener.java b/dubbo-xds/src/main/java/org/apache/dubbo/registry/xds/util/XdsListener.java new file mode 100644 index 000000000..233d92198 --- /dev/null +++ b/dubbo-xds/src/main/java/org/apache/dubbo/registry/xds/util/XdsListener.java @@ -0,0 +1,23 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.dubbo.registry.xds.util; + +import io.envoyproxy.envoy.service.discovery.v3.DiscoveryResponse; + +public interface XdsListener { + void process(DiscoveryResponse discoveryResponse); +} diff --git a/dubbo-xds/src/main/java/org/apache/dubbo/registry/xds/util/bootstrap/BootstrapInfoImpl.java b/dubbo-xds/src/main/java/org/apache/dubbo/registry/xds/util/bootstrap/BootstrapInfoImpl.java new file mode 100644 index 000000000..dad3b6d70 --- /dev/null +++ b/dubbo-xds/src/main/java/org/apache/dubbo/registry/xds/util/bootstrap/BootstrapInfoImpl.java @@ -0,0 +1,130 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.dubbo.registry.xds.util.bootstrap; + +import io.envoyproxy.envoy.config.core.v3.Node; + +import javax.annotation.Nullable; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; + +public final class BootstrapInfoImpl extends Bootstrapper.BootstrapInfo { + + private final List servers; + + private final String serverListenerResourceNameTemplate; + + private final Map certProviders; + + private final Node node; + + BootstrapInfoImpl( + List servers, + String serverListenerResourceNameTemplate, + Map certProviders, + Node node) { + this.servers = servers; + this.serverListenerResourceNameTemplate = serverListenerResourceNameTemplate; + this.certProviders = certProviders; + this.node = node; + } + + @Override + public List servers() { + return servers; + } + + public Map certProviders() { + return certProviders; + } + + @Override + public Node node() { + return node; + } + + @Override + public String serverListenerResourceNameTemplate() { + return serverListenerResourceNameTemplate; + } + + @Override + public String toString() { + return "BootstrapInfo{" + + "servers=" + servers + ", " + + "serverListenerResourceNameTemplate=" + serverListenerResourceNameTemplate + ", " + + "node=" + node + ", " + + "}"; + } + + public static final class Builder extends Bootstrapper.BootstrapInfo.Builder { + private List servers; + private Node node; + + private Map certProviders; + + private String serverListenerResourceNameTemplate; + + Builder() {} + + @Override + Bootstrapper.BootstrapInfo.Builder servers(List servers) { + this.servers = new LinkedList<>(servers); + return this; + } + + @Override + Bootstrapper.BootstrapInfo.Builder node(Node node) { + if (node == null) { + throw new NullPointerException("Null node"); + } + this.node = node; + return this; + } + + @Override + Bootstrapper.BootstrapInfo.Builder certProviders( + @Nullable Map certProviders) { + this.certProviders = certProviders; + return this; + } + + @Override + Bootstrapper.BootstrapInfo.Builder serverListenerResourceNameTemplate( + @Nullable String serverListenerResourceNameTemplate) { + this.serverListenerResourceNameTemplate = serverListenerResourceNameTemplate; + return this; + } + + @Override + Bootstrapper.BootstrapInfo build() { + if (this.servers == null || this.node == null) { + StringBuilder missing = new StringBuilder(); + if (this.servers == null) { + missing.append(" servers"); + } + if (this.node == null) { + missing.append(" node"); + } + throw new IllegalStateException("Missing required properties:" + missing); + } + return new BootstrapInfoImpl( + this.servers, this.serverListenerResourceNameTemplate, this.certProviders, this.node); + } + } +} diff --git a/dubbo-xds/src/main/java/org/apache/dubbo/registry/xds/util/bootstrap/Bootstrapper.java b/dubbo-xds/src/main/java/org/apache/dubbo/registry/xds/util/bootstrap/Bootstrapper.java new file mode 100644 index 000000000..b8ef44be6 --- /dev/null +++ b/dubbo-xds/src/main/java/org/apache/dubbo/registry/xds/util/bootstrap/Bootstrapper.java @@ -0,0 +1,74 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.dubbo.registry.xds.util.bootstrap; + +import org.apache.dubbo.registry.xds.XdsInitializationException; + +import io.envoyproxy.envoy.config.core.v3.Node; +import io.grpc.ChannelCredentials; + +import javax.annotation.Nullable; +import java.util.List; +import java.util.Map; + +public abstract class Bootstrapper { + + public abstract BootstrapInfo bootstrap() throws XdsInitializationException; + + BootstrapInfo bootstrap(Map rawData) throws XdsInitializationException { + throw new UnsupportedOperationException(); + } + + public abstract static class ServerInfo { + public abstract String target(); + + abstract ChannelCredentials channelCredentials(); + + abstract boolean useProtocolV3(); + + abstract boolean ignoreResourceDeletion(); + } + + public abstract static class CertificateProviderInfo { + public abstract String pluginName(); + + public abstract Map config(); + } + + public abstract static class BootstrapInfo { + public abstract List servers(); + + public abstract Map certProviders(); + + public abstract Node node(); + + public abstract String serverListenerResourceNameTemplate(); + + abstract static class Builder { + + abstract Builder servers(List servers); + + abstract Builder node(Node node); + + abstract Builder certProviders(@Nullable Map certProviders); + + abstract Builder serverListenerResourceNameTemplate(@Nullable String serverListenerResourceNameTemplate); + + abstract BootstrapInfo build(); + } + } +} diff --git a/dubbo-xds/src/main/java/org/apache/dubbo/registry/xds/util/bootstrap/BootstrapperImpl.java b/dubbo-xds/src/main/java/org/apache/dubbo/registry/xds/util/bootstrap/BootstrapperImpl.java new file mode 100644 index 000000000..b4d913d73 --- /dev/null +++ b/dubbo-xds/src/main/java/org/apache/dubbo/registry/xds/util/bootstrap/BootstrapperImpl.java @@ -0,0 +1,178 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.dubbo.registry.xds.util.bootstrap; + +import org.apache.dubbo.common.logger.Logger; +import org.apache.dubbo.common.logger.LoggerFactory; +import org.apache.dubbo.registry.xds.XdsInitializationException; + +import io.envoyproxy.envoy.config.core.v3.Node; +import io.grpc.ChannelCredentials; +import io.grpc.internal.JsonParser; +import io.grpc.internal.JsonUtil; + +import javax.annotation.Nullable; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; + +public class BootstrapperImpl extends Bootstrapper { + + static final String BOOTSTRAP_PATH_SYS_ENV_VAR = "GRPC_XDS_BOOTSTRAP"; + static String bootstrapPathFromEnvVar = System.getenv(BOOTSTRAP_PATH_SYS_ENV_VAR); + + private static final Logger logger = LoggerFactory.getLogger(BootstrapperImpl.class); + private FileReader reader = LocalFileReader.INSTANCE; + + private static final String SERVER_FEATURE_XDS_V3 = "xds_v3"; + private static final String SERVER_FEATURE_IGNORE_RESOURCE_DELETION = "ignore_resource_deletion"; + + public BootstrapInfo bootstrap() throws XdsInitializationException { + String filePath = bootstrapPathFromEnvVar; + String fileContent = null; + if (filePath != null) { + try { + fileContent = reader.readFile(filePath); + } catch (IOException e) { + throw new XdsInitializationException("Fail to read bootstrap file", e); + } + } + if (fileContent == null) throw new XdsInitializationException("Cannot find bootstrap configuration"); + + Map rawBootstrap; + try { + rawBootstrap = (Map) JsonParser.parse(fileContent); + } catch (IOException e) { + throw new XdsInitializationException("Failed to parse JSON", e); + } + return bootstrap(rawBootstrap); + } + + @Override + BootstrapInfo bootstrap(Map rawData) throws XdsInitializationException { + BootstrapInfo.Builder builder = new BootstrapInfoImpl.Builder(); + + List rawServerConfigs = JsonUtil.getList(rawData, "xds_servers"); + if (rawServerConfigs == null) { + throw new XdsInitializationException("Invalid bootstrap: 'xds_servers' does not exist."); + } + List servers = parseServerInfos(rawServerConfigs); + builder.servers(servers); + + Node.Builder nodeBuilder = Node.newBuilder(); + Map rawNode = JsonUtil.getObject(rawData, "node"); + if (rawNode != null) { + String id = JsonUtil.getString(rawNode, "id"); + if (id != null) { + nodeBuilder.setId(id); + } + String cluster = JsonUtil.getString(rawNode, "cluster"); + if (cluster != null) { + nodeBuilder.setCluster(cluster); + } + Map metadata = JsonUtil.getObject(rawNode, "metadata"); + Map rawLocality = JsonUtil.getObject(rawNode, "locality"); + } + builder.node(nodeBuilder.build()); + + Map certProvidersBlob = JsonUtil.getObject(rawData, "certificate_providers"); + if (certProvidersBlob != null) { + Map certProviders = new HashMap<>(certProvidersBlob.size()); + for (String name : certProvidersBlob.keySet()) { + Map valueMap = JsonUtil.getObject(certProvidersBlob, name); + String pluginName = checkForNull(JsonUtil.getString(valueMap, "plugin_name"), "plugin_name"); + Map config = checkForNull(JsonUtil.getObject(valueMap, "config"), "config"); + CertificateProviderInfoImpl certificateProviderInfo = + new CertificateProviderInfoImpl(pluginName, config); + certProviders.put(name, certificateProviderInfo); + } + builder.certProviders(certProviders); + } + + return builder.build(); + } + + private static List parseServerInfos(List rawServerConfigs) throws XdsInitializationException { + List servers = new LinkedList<>(); + List> serverConfigList = JsonUtil.checkObjectList(rawServerConfigs); + for (Map serverConfig : serverConfigList) { + String serverUri = JsonUtil.getString(serverConfig, "server_uri"); + if (serverUri == null) { + throw new XdsInitializationException("Invalid bootstrap: missing 'server_uri'"); + } + List rawChannelCredsList = JsonUtil.getList(serverConfig, "channel_creds"); + if (rawChannelCredsList == null || rawChannelCredsList.isEmpty()) { + throw new XdsInitializationException( + "Invalid bootstrap: server " + serverUri + " 'channel_creds' required"); + } + ChannelCredentials channelCredentials = + parseChannelCredentials(JsonUtil.checkObjectList(rawChannelCredsList), serverUri); + // if (channelCredentials == null) { + // throw new XdsInitializationException( + // "Server " + serverUri + ": no supported channel credentials found"); + // } + + boolean useProtocolV3 = false; + boolean ignoreResourceDeletion = false; + List serverFeatures = JsonUtil.getListOfStrings(serverConfig, "server_features"); + if (serverFeatures != null) { + useProtocolV3 = serverFeatures.contains(SERVER_FEATURE_XDS_V3); + ignoreResourceDeletion = serverFeatures.contains(SERVER_FEATURE_IGNORE_RESOURCE_DELETION); + } + servers.add(new ServerInfoImpl(serverUri, channelCredentials, useProtocolV3, ignoreResourceDeletion)); + } + return servers; + } + + void setFileReader(FileReader reader) { + this.reader = reader; + } + + /** + * Reads the content of the file with the given path in the file system. + */ + interface FileReader { + String readFile(String path) throws IOException; + } + + private enum LocalFileReader implements FileReader { + INSTANCE; + + @Override + public String readFile(String path) throws IOException { + return new String(Files.readAllBytes(Paths.get(path)), StandardCharsets.UTF_8); + } + } + + private static T checkForNull(T value, String fieldName) throws XdsInitializationException { + if (value == null) { + throw new XdsInitializationException("Invalid bootstrap: '" + fieldName + "' does not exist."); + } + return value; + } + + @Nullable + private static ChannelCredentials parseChannelCredentials(List> jsonList, String serverUri) + throws XdsInitializationException { + return null; + } +} diff --git a/dubbo-xds/src/main/java/org/apache/dubbo/registry/xds/util/bootstrap/CertificateProviderInfoImpl.java b/dubbo-xds/src/main/java/org/apache/dubbo/registry/xds/util/bootstrap/CertificateProviderInfoImpl.java new file mode 100644 index 000000000..980b6e1fd --- /dev/null +++ b/dubbo-xds/src/main/java/org/apache/dubbo/registry/xds/util/bootstrap/CertificateProviderInfoImpl.java @@ -0,0 +1,45 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.dubbo.registry.xds.util.bootstrap; + +import java.util.Map; + +final class CertificateProviderInfoImpl extends Bootstrapper.CertificateProviderInfo { + + private final String pluginName; + private final Map config; + + CertificateProviderInfoImpl(String pluginName, Map config) { + this.pluginName = pluginName; + this.config = config; + } + + @Override + public String pluginName() { + return pluginName; + } + + @Override + public Map config() { + return config; + } + + @Override + public String toString() { + return "CertificateProviderInfo{" + "pluginName=" + pluginName + ", " + "config=" + config + "}"; + } +} diff --git a/dubbo-xds/src/main/java/org/apache/dubbo/registry/xds/util/bootstrap/ServerInfoImpl.java b/dubbo-xds/src/main/java/org/apache/dubbo/registry/xds/util/bootstrap/ServerInfoImpl.java new file mode 100644 index 000000000..c15f4f28b --- /dev/null +++ b/dubbo-xds/src/main/java/org/apache/dubbo/registry/xds/util/bootstrap/ServerInfoImpl.java @@ -0,0 +1,71 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.dubbo.registry.xds.util.bootstrap; + +import io.grpc.ChannelCredentials; + +final class ServerInfoImpl extends Bootstrapper.ServerInfo { + + private final String target; + + private final ChannelCredentials channelCredentials; + + private final boolean useProtocolV3; + + private final boolean ignoreResourceDeletion; + + ServerInfoImpl( + String target, + ChannelCredentials channelCredentials, + boolean useProtocolV3, + boolean ignoreResourceDeletion) { + this.target = target; + this.channelCredentials = channelCredentials; + this.useProtocolV3 = useProtocolV3; + this.ignoreResourceDeletion = ignoreResourceDeletion; + } + + @Override + public String target() { + return target; + } + + @Override + ChannelCredentials channelCredentials() { + return channelCredentials; + } + + @Override + boolean useProtocolV3() { + return useProtocolV3; + } + + @Override + boolean ignoreResourceDeletion() { + return ignoreResourceDeletion; + } + + @Override + public String toString() { + return "ServerInfo{" + + "target=" + target + ", " + + "channelCredentials=" + channelCredentials + ", " + + "useProtocolV3=" + useProtocolV3 + ", " + + "ignoreResourceDeletion=" + ignoreResourceDeletion + + "}"; + } +} diff --git a/dubbo-xds/src/main/java/org/apache/dubbo/registry/xds/util/protocol/AbstractProtocol.java b/dubbo-xds/src/main/java/org/apache/dubbo/registry/xds/util/protocol/AbstractProtocol.java new file mode 100644 index 000000000..792f20262 --- /dev/null +++ b/dubbo-xds/src/main/java/org/apache/dubbo/registry/xds/util/protocol/AbstractProtocol.java @@ -0,0 +1,269 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.dubbo.registry.xds.util.protocol; + +import org.apache.dubbo.common.logger.ErrorTypeAwareLogger; +import org.apache.dubbo.common.logger.LoggerFactory; +import org.apache.dubbo.common.utils.ConcurrentHashMapUtils; +import org.apache.dubbo.common.utils.StringUtils; +import org.apache.dubbo.registry.xds.util.AdsObserver; +import org.apache.dubbo.registry.xds.util.XdsListener; + +import io.envoyproxy.envoy.config.core.v3.Node; +import io.envoyproxy.envoy.service.discovery.v3.DiscoveryRequest; +import io.envoyproxy.envoy.service.discovery.v3.DiscoveryResponse; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.locks.ReentrantLock; +import java.util.concurrent.locks.ReentrantReadWriteLock; +import java.util.function.Consumer; +import java.util.stream.Collectors; + +import static org.apache.dubbo.common.constants.LoggerCodeConstants.INTERNAL_INTERRUPTED; +import static org.apache.dubbo.common.constants.LoggerCodeConstants.PROTOCOL_FAILED_REQUEST; + +public abstract class AbstractProtocol> implements XdsProtocol, XdsListener { + + private static final ErrorTypeAwareLogger logger = LoggerFactory.getErrorTypeAwareLogger(AbstractProtocol.class); + + protected AdsObserver adsObserver; + + protected final Node node; + + private final int checkInterval; + + protected final ReentrantReadWriteLock lock = new ReentrantReadWriteLock(); + + protected final ReentrantReadWriteLock.ReadLock readLock = lock.readLock(); + + protected final ReentrantReadWriteLock.WriteLock writeLock = lock.writeLock(); + + protected Set observeResourcesName; + + public static final String emptyResourceName = "emptyResourcesName"; + private final ReentrantLock resourceLock = new ReentrantLock(); + + protected Map, List>>> consumerObserveMap = new ConcurrentHashMap<>(); + + public Map, List>>> getConsumerObserveMap() { + return consumerObserveMap; + } + + protected Map resourcesMap = new ConcurrentHashMap<>(); + + public AbstractProtocol(AdsObserver adsObserver, Node node, int checkInterval) { + this.adsObserver = adsObserver; + this.node = node; + this.checkInterval = checkInterval; + adsObserver.addListener(this); + } + + /** + * Abstract method to obtain Type-URL from sub-class + * + * @return Type-URL of xDS + */ + public abstract String getTypeUrl(); + + public boolean isCacheExistResource(Set resourceNames) { + for (String resourceName : resourceNames) { + if ("".equals(resourceName)) { + continue; + } + if (!resourcesMap.containsKey(resourceName)) { + return false; + } + } + return true; + } + + public T getCacheResource(String resourceName) { + if (resourceName == null || resourceName.length() == 0) { + return null; + } + return resourcesMap.get(resourceName); + } + + @Override + public Map getResource(Set resourceNames) { + resourceNames = resourceNames == null ? Collections.emptySet() : resourceNames; + + if (!resourceNames.isEmpty() && isCacheExistResource(resourceNames)) { + return getResourceFromCache(resourceNames); + } else { + return getResourceFromRemote(resourceNames); + } + } + + private Map getResourceFromCache(Set resourceNames) { + return resourceNames.stream() + .filter(o -> !StringUtils.isEmpty(o)) + .collect(Collectors.toMap(k -> k, this::getCacheResource)); + } + + public Map getResourceFromRemote(Set resourceNames) { + try { + resourceLock.lock(); + CompletableFuture> future = new CompletableFuture<>(); + observeResourcesName = resourceNames; + Set consumerObserveResourceNames = new HashSet<>(); + if (resourceNames.isEmpty()) { + consumerObserveResourceNames.add(emptyResourceName); + } else { + consumerObserveResourceNames = resourceNames; + } + + Consumer> futureConsumer = future::complete; + try { + writeLock.lock(); + ConcurrentHashMapUtils.computeIfAbsent( + (ConcurrentHashMap, List>>>) consumerObserveMap, + consumerObserveResourceNames, + key -> new ArrayList<>()) + .add(futureConsumer); + } finally { + writeLock.unlock(); + } + + Set resourceNamesToObserve = new HashSet<>(resourceNames); + resourceNamesToObserve.addAll(resourcesMap.keySet()); + adsObserver.request(buildDiscoveryRequest(resourceNamesToObserve)); + logger.info("Send xDS Observe request to remote. Resource count: " + resourceNamesToObserve.size() + + ". Resource Type: " + getTypeUrl()); + + try { + Map result = future.get(); + + try { + writeLock.lock(); + consumerObserveMap.get(consumerObserveResourceNames).removeIf(o -> o.equals(futureConsumer)); + } finally { + writeLock.unlock(); + } + + return result; + } catch (InterruptedException e) { + logger.error( + INTERNAL_INTERRUPTED, + "", + "", + "InterruptedException occur when request control panel. error=", + e); + Thread.currentThread().interrupt(); + } catch (Exception e) { + logger.error(PROTOCOL_FAILED_REQUEST, "", "", "Error occur when request control panel. error=", e); + } + } finally { + resourceLock.unlock(); + } + return Collections.emptyMap(); + } + + public void observeResource(Set resourceNames, Consumer> consumer, boolean isReConnect) { + // call once for full data + if (!isReConnect) { + consumer.accept(getResource(resourceNames)); + try { + writeLock.lock(); + consumerObserveMap.compute(resourceNames, (k, v) -> { + if (v == null) { + v = new ArrayList<>(); + } + // support multi-consumer + v.add(consumer); + return v; + }); + } finally { + writeLock.unlock(); + } + } + try { + writeLock.lock(); + this.observeResourcesName = + consumerObserveMap.keySet().stream().flatMap(Set::stream).collect(Collectors.toSet()); + } finally { + writeLock.unlock(); + } + } + + public void unobserveResource(Set resourceNames, Consumer> consumer) { + // TODO + } + + protected DiscoveryRequest buildDiscoveryRequest(Set resourceNames) { + return DiscoveryRequest.newBuilder() + .setNode(node) + .setTypeUrl(getTypeUrl()) + .addAllResourceNames(resourceNames) + .build(); + } + + protected abstract Map decodeDiscoveryResponse(DiscoveryResponse response); + + @Override + public final void process(DiscoveryResponse discoveryResponse) { + Map newResult = decodeDiscoveryResponse(discoveryResponse); + Map oldResource = resourcesMap; + discoveryResponseListener(oldResource, newResult); + resourcesMap = newResult; + } + + private void discoveryResponseListener(Map oldResult, Map newResult) { + Set changedResourceNames = new HashSet<>(); + oldResult.forEach((key, origin) -> { + if (!Objects.equals(origin, newResult.get(key))) { + changedResourceNames.add(key); + } + }); + newResult.forEach((key, origin) -> { + if (!Objects.equals(origin, oldResult.get(key))) { + changedResourceNames.add(key); + } + }); + if (changedResourceNames.isEmpty()) { + return; + } + + logger.info("Receive resource update notification from xds server. Change resource count: " + + changedResourceNames.stream() + ". Type: " + getTypeUrl()); + + // call once for full data + try { + readLock.lock(); + for (Map.Entry, List>>> entry : consumerObserveMap.entrySet()) { + if (entry.getKey().stream().noneMatch(changedResourceNames::contains)) { + // none update + continue; + } + + Map dsResultMap = + entry.getKey().stream().collect(Collectors.toMap(k -> k, v -> newResult.get(v))); + entry.getValue().forEach(o -> o.accept(dsResultMap)); + } + } finally { + readLock.unlock(); + } + } +} diff --git a/dubbo-xds/src/main/java/org/apache/dubbo/registry/xds/util/protocol/DeltaResource.java b/dubbo-xds/src/main/java/org/apache/dubbo/registry/xds/util/protocol/DeltaResource.java new file mode 100644 index 000000000..bcae39cee --- /dev/null +++ b/dubbo-xds/src/main/java/org/apache/dubbo/registry/xds/util/protocol/DeltaResource.java @@ -0,0 +1,32 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.dubbo.registry.xds.util.protocol; + +/** + * A interface for resources in xDS, which can be updated by ADS delta stream + *
+ * This interface is design to unify the way of fetching data in delta stream + * in {@link org.apache.dubbo.registry.xds.util.PilotExchanger} + */ +public interface DeltaResource { + /** + * Get resource from delta stream + * + * @return the newest resource from stream + */ + T getResource(); +} diff --git a/dubbo-xds/src/main/java/org/apache/dubbo/registry/xds/util/protocol/XdsProtocol.java b/dubbo-xds/src/main/java/org/apache/dubbo/registry/xds/util/protocol/XdsProtocol.java new file mode 100644 index 000000000..ce1a08814 --- /dev/null +++ b/dubbo-xds/src/main/java/org/apache/dubbo/registry/xds/util/protocol/XdsProtocol.java @@ -0,0 +1,41 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.dubbo.registry.xds.util.protocol; + +import java.util.Map; +import java.util.Set; +import java.util.function.Consumer; + +public interface XdsProtocol { + /** + * Gets all {@link T resource} by the specified resource name. + * For LDS, the {@param resourceNames} is ignored + * + * @param resourceNames specified resource name + * @return resources, null if request failed + */ + Map getResource(Set resourceNames); + + /** + * Add a observer resource with {@link Consumer} + * + * @param resourceNames specified resource name + * @param consumer resource notifier, will be called when resource updated + * @return requestId, used when resourceNames update with {@link XdsProtocol#updateObserve(long, Set)} + */ + void observeResource(Set resourceNames, Consumer> consumer, boolean isReConnect); +} diff --git a/dubbo-xds/src/main/java/org/apache/dubbo/registry/xds/util/protocol/delta/DeltaEndpoint.java b/dubbo-xds/src/main/java/org/apache/dubbo/registry/xds/util/protocol/delta/DeltaEndpoint.java new file mode 100644 index 000000000..46988f38e --- /dev/null +++ b/dubbo-xds/src/main/java/org/apache/dubbo/registry/xds/util/protocol/delta/DeltaEndpoint.java @@ -0,0 +1,48 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.dubbo.registry.xds.util.protocol.delta; + +import org.apache.dubbo.common.utils.CollectionUtils; +import org.apache.dubbo.registry.xds.util.protocol.DeltaResource; +import org.apache.dubbo.registry.xds.util.protocol.message.Endpoint; +import org.apache.dubbo.registry.xds.util.protocol.message.EndpointResult; + +import java.util.Collection; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Collectors; + +public class DeltaEndpoint implements DeltaResource { + private final Map> data = new ConcurrentHashMap<>(); + + public void addResource(String resourceName, Set endpoints) { + data.put(resourceName, endpoints); + } + + public void removeResource(Collection resourceName) { + if (CollectionUtils.isNotEmpty(resourceName)) { + resourceName.forEach(data::remove); + } + } + + @Override + public EndpointResult getResource() { + Set set = data.values().stream().flatMap(Set::stream).collect(Collectors.toSet()); + return new EndpointResult(set); + } +} diff --git a/dubbo-xds/src/main/java/org/apache/dubbo/registry/xds/util/protocol/delta/DeltaListener.java b/dubbo-xds/src/main/java/org/apache/dubbo/registry/xds/util/protocol/delta/DeltaListener.java new file mode 100644 index 000000000..bca3024ed --- /dev/null +++ b/dubbo-xds/src/main/java/org/apache/dubbo/registry/xds/util/protocol/delta/DeltaListener.java @@ -0,0 +1,47 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.dubbo.registry.xds.util.protocol.delta; + +import org.apache.dubbo.common.utils.CollectionUtils; +import org.apache.dubbo.registry.xds.util.protocol.DeltaResource; +import org.apache.dubbo.registry.xds.util.protocol.message.ListenerResult; + +import java.util.Collection; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Collectors; + +public class DeltaListener implements DeltaResource { + private final Map> data = new ConcurrentHashMap<>(); + + public void addResource(String resourceName, Set listeners) { + data.put(resourceName, listeners); + } + + public void removeResource(Collection resourceName) { + if (CollectionUtils.isNotEmpty(resourceName)) { + resourceName.forEach(data::remove); + } + } + + @Override + public ListenerResult getResource() { + Set set = data.values().stream().flatMap(Set::stream).collect(Collectors.toSet()); + return new ListenerResult(set); + } +} diff --git a/dubbo-xds/src/main/java/org/apache/dubbo/registry/xds/util/protocol/delta/DeltaRoute.java b/dubbo-xds/src/main/java/org/apache/dubbo/registry/xds/util/protocol/delta/DeltaRoute.java new file mode 100644 index 000000000..71fdb479b --- /dev/null +++ b/dubbo-xds/src/main/java/org/apache/dubbo/registry/xds/util/protocol/delta/DeltaRoute.java @@ -0,0 +1,47 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.dubbo.registry.xds.util.protocol.delta; + +import org.apache.dubbo.common.utils.CollectionUtils; +import org.apache.dubbo.registry.xds.util.protocol.DeltaResource; +import org.apache.dubbo.registry.xds.util.protocol.message.RouteResult; + +import java.util.Collection; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +public class DeltaRoute implements DeltaResource { + private final Map>> data = new ConcurrentHashMap<>(); + + public void addResource(String resourceName, Map> route) { + data.put(resourceName, route); + } + + public void removeResource(Collection resourceName) { + if (CollectionUtils.isNotEmpty(resourceName)) { + resourceName.forEach(data::remove); + } + } + + @Override + public RouteResult getResource() { + Map> result = new ConcurrentHashMap<>(); + data.values().forEach(result::putAll); + return new RouteResult(result); + } +} diff --git a/dubbo-xds/src/main/java/org/apache/dubbo/registry/xds/util/protocol/impl/EdsProtocol.java b/dubbo-xds/src/main/java/org/apache/dubbo/registry/xds/util/protocol/impl/EdsProtocol.java new file mode 100644 index 000000000..987be418a --- /dev/null +++ b/dubbo-xds/src/main/java/org/apache/dubbo/registry/xds/util/protocol/impl/EdsProtocol.java @@ -0,0 +1,97 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.dubbo.registry.xds.util.protocol.impl; + +import org.apache.dubbo.common.logger.ErrorTypeAwareLogger; +import org.apache.dubbo.common.logger.LoggerFactory; +import org.apache.dubbo.registry.xds.util.AdsObserver; +import org.apache.dubbo.registry.xds.util.protocol.AbstractProtocol; +import org.apache.dubbo.registry.xds.util.protocol.delta.DeltaEndpoint; +import org.apache.dubbo.registry.xds.util.protocol.message.Endpoint; +import org.apache.dubbo.registry.xds.util.protocol.message.EndpointResult; + +import com.google.protobuf.Any; +import com.google.protobuf.InvalidProtocolBufferException; +import io.envoyproxy.envoy.config.core.v3.HealthStatus; +import io.envoyproxy.envoy.config.core.v3.Node; +import io.envoyproxy.envoy.config.core.v3.SocketAddress; +import io.envoyproxy.envoy.config.endpoint.v3.ClusterLoadAssignment; +import io.envoyproxy.envoy.config.endpoint.v3.LbEndpoint; +import io.envoyproxy.envoy.service.discovery.v3.DiscoveryResponse; + +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Collectors; + +import static org.apache.dubbo.common.constants.LoggerCodeConstants.REGISTRY_ERROR_RESPONSE_XDS; + +public class EdsProtocol extends AbstractProtocol { + + private static final ErrorTypeAwareLogger logger = LoggerFactory.getErrorTypeAwareLogger(EdsProtocol.class); + + public EdsProtocol(AdsObserver adsObserver, Node node, int checkInterval) { + super(adsObserver, node, checkInterval); + } + + @Override + public String getTypeUrl() { + return "type.googleapis.com/envoy.config.endpoint.v3.ClusterLoadAssignment"; + } + + @Override + protected Map decodeDiscoveryResponse(DiscoveryResponse response) { + if (getTypeUrl().equals(response.getTypeUrl())) { + return response.getResourcesList().stream() + .map(EdsProtocol::unpackClusterLoadAssignment) + .filter(Objects::nonNull) + .collect(Collectors.toConcurrentMap( + ClusterLoadAssignment::getClusterName, this::decodeResourceToEndpoint)); + } + return new HashMap<>(); + } + + private EndpointResult decodeResourceToEndpoint(ClusterLoadAssignment resource) { + Set endpoints = resource.getEndpointsList().stream() + .flatMap(e -> e.getLbEndpointsList().stream()) + .map(e -> decodeLbEndpointToEndpoint(resource.getClusterName(), e)) + .collect(Collectors.toSet()); + return new EndpointResult(endpoints); + } + + private static Endpoint decodeLbEndpointToEndpoint(String clusterName, LbEndpoint lbEndpoint) { + Endpoint endpoint = new Endpoint(); + SocketAddress address = lbEndpoint.getEndpoint().getAddress().getSocketAddress(); + endpoint.setAddress(address.getAddress()); + endpoint.setPortValue(address.getPortValue()); + boolean healthy = HealthStatus.HEALTHY.equals(lbEndpoint.getHealthStatus()) + || HealthStatus.UNKNOWN.equals(lbEndpoint.getHealthStatus()); + endpoint.setHealthy(healthy); + endpoint.setWeight(lbEndpoint.getLoadBalancingWeight().getValue()); + return endpoint; + } + + private static ClusterLoadAssignment unpackClusterLoadAssignment(Any any) { + try { + return any.unpack(ClusterLoadAssignment.class); + } catch (InvalidProtocolBufferException e) { + logger.error(REGISTRY_ERROR_RESPONSE_XDS, "", "", "Error occur when decode xDS response.", e); + return null; + } + } +} diff --git a/dubbo-xds/src/main/java/org/apache/dubbo/registry/xds/util/protocol/impl/LdsProtocol.java b/dubbo-xds/src/main/java/org/apache/dubbo/registry/xds/util/protocol/impl/LdsProtocol.java new file mode 100644 index 000000000..e4494e284 --- /dev/null +++ b/dubbo-xds/src/main/java/org/apache/dubbo/registry/xds/util/protocol/impl/LdsProtocol.java @@ -0,0 +1,106 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.dubbo.registry.xds.util.protocol.impl; + +import org.apache.dubbo.common.logger.ErrorTypeAwareLogger; +import org.apache.dubbo.common.logger.LoggerFactory; +import org.apache.dubbo.registry.xds.util.AdsObserver; +import org.apache.dubbo.registry.xds.util.protocol.AbstractProtocol; +import org.apache.dubbo.registry.xds.util.protocol.delta.DeltaListener; +import org.apache.dubbo.registry.xds.util.protocol.message.ListenerResult; + +import com.google.protobuf.Any; +import com.google.protobuf.InvalidProtocolBufferException; +import io.envoyproxy.envoy.config.core.v3.Node; +import io.envoyproxy.envoy.config.listener.v3.Filter; +import io.envoyproxy.envoy.config.listener.v3.Listener; +import io.envoyproxy.envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager; +import io.envoyproxy.envoy.extensions.filters.network.http_connection_manager.v3.Rds; +import io.envoyproxy.envoy.service.discovery.v3.DiscoveryResponse; + +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Collectors; + +import static org.apache.dubbo.common.constants.LoggerCodeConstants.REGISTRY_ERROR_RESPONSE_XDS; + +public class LdsProtocol extends AbstractProtocol { + private static final ErrorTypeAwareLogger logger = LoggerFactory.getErrorTypeAwareLogger(LdsProtocol.class); + + public LdsProtocol(AdsObserver adsObserver, Node node, int checkInterval) { + super(adsObserver, node, checkInterval); + } + + @Override + public String getTypeUrl() { + return "type.googleapis.com/envoy.config.listener.v3.Listener"; + } + + public Map getListeners() { + return getResource(null); + } + + @Override + protected Map decodeDiscoveryResponse(DiscoveryResponse response) { + if (getTypeUrl().equals(response.getTypeUrl())) { + Set set = response.getResourcesList().stream() + .map(LdsProtocol::unpackListener) + .filter(Objects::nonNull) + .flatMap(e -> decodeResourceToListener(e).stream()) + .collect(Collectors.toSet()); + Map listenerDecodeResult = new ConcurrentHashMap<>(); + listenerDecodeResult.put(emptyResourceName, new ListenerResult(set)); + return listenerDecodeResult; + } + return new HashMap<>(); + } + + private Set decodeResourceToListener(Listener resource) { + return resource.getFilterChainsList().stream() + .flatMap(e -> e.getFiltersList().stream()) + .map(Filter::getTypedConfig) + .map(LdsProtocol::unpackHttpConnectionManager) + .filter(Objects::nonNull) + .map(HttpConnectionManager::getRds) + .map(Rds::getRouteConfigName) + .collect(Collectors.toSet()); + } + + private static Listener unpackListener(Any any) { + try { + return any.unpack(Listener.class); + } catch (InvalidProtocolBufferException e) { + logger.error(REGISTRY_ERROR_RESPONSE_XDS, "", "", "Error occur when decode xDS response.", e); + return null; + } + } + + private static HttpConnectionManager unpackHttpConnectionManager(Any any) { + try { + if (!any.is(HttpConnectionManager.class)) { + return null; + } + return any.unpack(HttpConnectionManager.class); + } catch (InvalidProtocolBufferException e) { + logger.error(REGISTRY_ERROR_RESPONSE_XDS, "", "", "Error occur when decode xDS response.", e); + return null; + } + } +} diff --git a/dubbo-xds/src/main/java/org/apache/dubbo/registry/xds/util/protocol/impl/RdsProtocol.java b/dubbo-xds/src/main/java/org/apache/dubbo/registry/xds/util/protocol/impl/RdsProtocol.java new file mode 100644 index 000000000..6d7ddade3 --- /dev/null +++ b/dubbo-xds/src/main/java/org/apache/dubbo/registry/xds/util/protocol/impl/RdsProtocol.java @@ -0,0 +1,92 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.dubbo.registry.xds.util.protocol.impl; + +import org.apache.dubbo.common.logger.ErrorTypeAwareLogger; +import org.apache.dubbo.common.logger.LoggerFactory; +import org.apache.dubbo.registry.xds.util.AdsObserver; +import org.apache.dubbo.registry.xds.util.protocol.AbstractProtocol; +import org.apache.dubbo.registry.xds.util.protocol.delta.DeltaRoute; +import org.apache.dubbo.registry.xds.util.protocol.message.RouteResult; + +import com.google.protobuf.Any; +import com.google.protobuf.InvalidProtocolBufferException; +import io.envoyproxy.envoy.config.core.v3.Node; +import io.envoyproxy.envoy.config.route.v3.Route; +import io.envoyproxy.envoy.config.route.v3.RouteAction; +import io.envoyproxy.envoy.config.route.v3.RouteConfiguration; +import io.envoyproxy.envoy.config.route.v3.VirtualHost; +import io.envoyproxy.envoy.service.discovery.v3.DiscoveryResponse; + +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Collectors; + +import static org.apache.dubbo.common.constants.LoggerCodeConstants.REGISTRY_ERROR_RESPONSE_XDS; + +public class RdsProtocol extends AbstractProtocol { + + private static final ErrorTypeAwareLogger logger = LoggerFactory.getErrorTypeAwareLogger(RdsProtocol.class); + + public RdsProtocol(AdsObserver adsObserver, Node node, int checkInterval) { + super(adsObserver, node, checkInterval); + } + + @Override + public String getTypeUrl() { + return "type.googleapis.com/envoy.config.route.v3.RouteConfiguration"; + } + + @Override + protected Map decodeDiscoveryResponse(DiscoveryResponse response) { + if (getTypeUrl().equals(response.getTypeUrl())) { + return response.getResourcesList().stream() + .map(RdsProtocol::unpackRouteConfiguration) + .filter(Objects::nonNull) + .collect(Collectors.toConcurrentMap(RouteConfiguration::getName, this::decodeResourceToListener)); + } + return new HashMap<>(); + } + + private RouteResult decodeResourceToListener(RouteConfiguration resource) { + Map> map = new HashMap<>(); + Map rdsVirtualhostMap = new ConcurrentHashMap<>(); + resource.getVirtualHostsList().forEach(virtualHost -> { + Set cluster = virtualHost.getRoutesList().stream() + .map(Route::getRoute) + .map(RouteAction::getCluster) + .collect(Collectors.toSet()); + for (String domain : virtualHost.getDomainsList()) { + map.put(domain, cluster); + rdsVirtualhostMap.put(domain, virtualHost); + } + }); + return new RouteResult(map, rdsVirtualhostMap); + } + + private static RouteConfiguration unpackRouteConfiguration(Any any) { + try { + return any.unpack(RouteConfiguration.class); + } catch (InvalidProtocolBufferException e) { + logger.error(REGISTRY_ERROR_RESPONSE_XDS, "", "", "Error occur when decode xDS response.", e); + return null; + } + } +} diff --git a/dubbo-xds/src/main/java/org/apache/dubbo/registry/xds/util/protocol/message/Endpoint.java b/dubbo-xds/src/main/java/org/apache/dubbo/registry/xds/util/protocol/message/Endpoint.java new file mode 100644 index 000000000..ceed16327 --- /dev/null +++ b/dubbo-xds/src/main/java/org/apache/dubbo/registry/xds/util/protocol/message/Endpoint.java @@ -0,0 +1,96 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.dubbo.registry.xds.util.protocol.message; + +import java.util.Objects; + +public class Endpoint { + private String clusterName; + private String address; + private int portValue; + private boolean healthy; + private int weight; + + public String getClusterName() { + return clusterName; + } + + public void setClusterName(String clusterName) { + this.clusterName = clusterName; + } + + public String getAddress() { + return address; + } + + public void setAddress(String address) { + this.address = address; + } + + public int getPortValue() { + return portValue; + } + + public void setPortValue(int portValue) { + this.portValue = portValue; + } + + public boolean isHealthy() { + return healthy; + } + + public void setHealthy(boolean healthy) { + this.healthy = healthy; + } + + public int getWeight() { + return weight; + } + + public void setWeight(int weight) { + this.weight = weight; + } + + @Override + public String toString() { + return "Endpoint{" + "address='" + + address + '\'' + ", portValue='" + + portValue + '\'' + ", healthy=" + + healthy + ", weight=" + + weight + '}'; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Endpoint endpoint = (Endpoint) o; + return healthy == endpoint.healthy + && weight == endpoint.weight + && Objects.equals(address, endpoint.address) + && Objects.equals(portValue, endpoint.portValue); + } + + @Override + public int hashCode() { + return Objects.hash(address, portValue, healthy, weight); + } +} diff --git a/dubbo-xds/src/main/java/org/apache/dubbo/registry/xds/util/protocol/message/EndpointResult.java b/dubbo-xds/src/main/java/org/apache/dubbo/registry/xds/util/protocol/message/EndpointResult.java new file mode 100644 index 000000000..ead2c4de2 --- /dev/null +++ b/dubbo-xds/src/main/java/org/apache/dubbo/registry/xds/util/protocol/message/EndpointResult.java @@ -0,0 +1,60 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.dubbo.registry.xds.util.protocol.message; + +import org.apache.dubbo.common.utils.ConcurrentHashSet; + +import java.util.Objects; +import java.util.Set; + +public class EndpointResult { + private Set endpoints; + + public EndpointResult() { + this.endpoints = new ConcurrentHashSet<>(); + } + + public EndpointResult(Set endpoints) { + this.endpoints = endpoints; + } + + public Set getEndpoints() { + return endpoints; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + EndpointResult that = (EndpointResult) o; + return Objects.equals(endpoints, that.endpoints); + } + + @Override + public int hashCode() { + return Objects.hash(endpoints); + } + + @Override + public String toString() { + return "EndpointResult{" + "endpoints=" + endpoints + '}'; + } +} diff --git a/dubbo-xds/src/main/java/org/apache/dubbo/registry/xds/util/protocol/message/ListenerResult.java b/dubbo-xds/src/main/java/org/apache/dubbo/registry/xds/util/protocol/message/ListenerResult.java new file mode 100644 index 000000000..7c1670316 --- /dev/null +++ b/dubbo-xds/src/main/java/org/apache/dubbo/registry/xds/util/protocol/message/ListenerResult.java @@ -0,0 +1,68 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.dubbo.registry.xds.util.protocol.message; + +import org.apache.dubbo.common.utils.ConcurrentHashSet; + +import java.util.Objects; +import java.util.Set; + +public class ListenerResult { + private Set routeConfigNames; + + public ListenerResult() { + this.routeConfigNames = new ConcurrentHashSet<>(); + } + + public ListenerResult(Set routeConfigNames) { + this.routeConfigNames = routeConfigNames; + } + + public Set getRouteConfigNames() { + return routeConfigNames; + } + + public void setRouteConfigNames(Set routeConfigNames) { + this.routeConfigNames = routeConfigNames; + } + + public void mergeRouteConfigNames(Set names) { + this.routeConfigNames.addAll(names); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + ListenerResult listenerResult = (ListenerResult) o; + return Objects.equals(routeConfigNames, listenerResult.routeConfigNames); + } + + @Override + public int hashCode() { + return Objects.hash(routeConfigNames); + } + + @Override + public String toString() { + return "ListenerResult{" + "routeConfigNames=" + routeConfigNames + '}'; + } +} diff --git a/dubbo-xds/src/main/java/org/apache/dubbo/registry/xds/util/protocol/message/RouteResult.java b/dubbo-xds/src/main/java/org/apache/dubbo/registry/xds/util/protocol/message/RouteResult.java new file mode 100644 index 000000000..13029d69a --- /dev/null +++ b/dubbo-xds/src/main/java/org/apache/dubbo/registry/xds/util/protocol/message/RouteResult.java @@ -0,0 +1,94 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.dubbo.registry.xds.util.protocol.message; + +import org.apache.dubbo.common.utils.ConcurrentHashSet; + +import io.envoyproxy.envoy.config.route.v3.VirtualHost; + +import java.util.Collections; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +public class RouteResult { + private final Map> domainMap; + private Map virtualHostMap; + + public RouteResult() { + this.domainMap = new ConcurrentHashMap<>(); + this.virtualHostMap = new ConcurrentHashMap<>(); + } + + public RouteResult(Map> domainMap) { + this.domainMap = domainMap; + this.virtualHostMap = new ConcurrentHashMap<>(); + } + + public RouteResult(Map> domainMap, Map virtualHostMap) { + this.domainMap = domainMap; + this.virtualHostMap = virtualHostMap; + } + + public Map> getDomainMap() { + return domainMap; + } + + public boolean isNotEmpty() { + return !domainMap.isEmpty(); + } + + public Set searchDomain(String domain) { + return domainMap.getOrDefault(domain, new ConcurrentHashSet<>()); + } + + public Set getDomains() { + return Collections.unmodifiableSet(domainMap.keySet()); + } + + @Override + public boolean equals(Object o) { + + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + RouteResult that = (RouteResult) o; + return Objects.equals(domainMap, that.domainMap) && Objects.equals(virtualHostMap, that.virtualHostMap); + } + + @Override + public int hashCode() { + return Objects.hash(domainMap, virtualHostMap); + } + + public VirtualHost searchVirtualHost(String domain) { + return virtualHostMap.get(domain); + } + + public void removeVirtualHost(String domain) { + virtualHostMap.remove(domain); + } + + @Override + public String toString() { + return "RouteResult{" + "domainMap=" + domainMap + ", virtualHostMap=" + virtualHostMap + '}'; + } +} diff --git a/dubbo-xds/src/main/java/org/apache/dubbo/rpc/cluster/router/xds/EdsEndpointListener.java b/dubbo-xds/src/main/java/org/apache/dubbo/rpc/cluster/router/xds/EdsEndpointListener.java new file mode 100644 index 000000000..58aaa8623 --- /dev/null +++ b/dubbo-xds/src/main/java/org/apache/dubbo/rpc/cluster/router/xds/EdsEndpointListener.java @@ -0,0 +1,26 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.dubbo.rpc.cluster.router.xds; + +import org.apache.dubbo.registry.xds.util.protocol.message.Endpoint; + +import java.util.Set; + +public interface EdsEndpointListener { + + void onEndPointChange(String cluster, Set endpoints); +} diff --git a/dubbo-xds/src/main/java/org/apache/dubbo/rpc/cluster/router/xds/EdsEndpointManager.java b/dubbo-xds/src/main/java/org/apache/dubbo/rpc/cluster/router/xds/EdsEndpointManager.java new file mode 100644 index 000000000..a54d74c7b --- /dev/null +++ b/dubbo-xds/src/main/java/org/apache/dubbo/rpc/cluster/router/xds/EdsEndpointManager.java @@ -0,0 +1,127 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.dubbo.rpc.cluster.router.xds; + +import org.apache.dubbo.common.threadpool.manager.FrameworkExecutorRepository; +import org.apache.dubbo.common.utils.CollectionUtils; +import org.apache.dubbo.common.utils.ConcurrentHashMapUtils; +import org.apache.dubbo.common.utils.ConcurrentHashSet; +import org.apache.dubbo.registry.xds.util.PilotExchanger; +import org.apache.dubbo.registry.xds.util.protocol.message.Endpoint; +import org.apache.dubbo.registry.xds.util.protocol.message.EndpointResult; +import org.apache.dubbo.rpc.model.FrameworkModel; + +import java.util.Collections; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Consumer; +import java.util.stream.Collectors; + +public class EdsEndpointManager { + + private static final ConcurrentHashMap> ENDPOINT_LISTENERS = + new ConcurrentHashMap<>(); + + private static final ConcurrentHashMap> ENDPOINT_DATA_CACHE = new ConcurrentHashMap<>(); + + private static final ConcurrentHashMap>> EDS_LISTENERS = + new ConcurrentHashMap<>(); + + public EdsEndpointManager() {} + + public synchronized void subscribeEds(String cluster, EdsEndpointListener listener) { + + Set listeners = + ConcurrentHashMapUtils.computeIfAbsent(ENDPOINT_LISTENERS, cluster, key -> new ConcurrentHashSet<>()); + if (CollectionUtils.isEmpty(listeners)) { + doSubscribeEds(cluster); + } + listeners.add(listener); + + if (ENDPOINT_DATA_CACHE.containsKey(cluster)) { + listener.onEndPointChange(cluster, ENDPOINT_DATA_CACHE.get(cluster)); + } + } + + private void doSubscribeEds(String cluster) { + ConcurrentHashMapUtils.computeIfAbsent(EDS_LISTENERS, cluster, key -> endpoints -> { + Set result = endpoints.values().stream() + .map(EndpointResult::getEndpoints) + .flatMap(Set::stream) + .collect(Collectors.toSet()); + notifyEndpointChange(cluster, result); + }); + Consumer> consumer = EDS_LISTENERS.get(cluster); + if (PilotExchanger.isEnabled()) { + FrameworkModel.defaultModel() + .getBeanFactory() + .getBean(FrameworkExecutorRepository.class) + .getSharedExecutor() + .submit(() -> PilotExchanger.getInstance().observeEds(Collections.singleton(cluster), consumer)); + } + } + + public synchronized void unSubscribeEds(String cluster, EdsEndpointListener listener) { + Set listeners = ENDPOINT_LISTENERS.get(cluster); + if (CollectionUtils.isEmpty(listeners)) { + return; + } + listeners.remove(listener); + if (listeners.isEmpty()) { + ENDPOINT_LISTENERS.remove(cluster); + doUnsubscribeEds(cluster); + } + } + + private void doUnsubscribeEds(String cluster) { + Consumer> consumer = EDS_LISTENERS.remove(cluster); + + if (consumer != null && PilotExchanger.isEnabled()) { + PilotExchanger.getInstance().unObserveEds(Collections.singleton(cluster), consumer); + } + ENDPOINT_DATA_CACHE.remove(cluster); + } + + public void notifyEndpointChange(String cluster, Set endpoints) { + + ENDPOINT_DATA_CACHE.put(cluster, endpoints); + + Set listeners = ENDPOINT_LISTENERS.get(cluster); + if (CollectionUtils.isEmpty(listeners)) { + return; + } + for (EdsEndpointListener listener : listeners) { + listener.onEndPointChange(cluster, endpoints); + } + } + + // for test + static ConcurrentHashMap> getEndpointListeners() { + return ENDPOINT_LISTENERS; + } + + // for test + static ConcurrentHashMap> getEndpointDataCache() { + return ENDPOINT_DATA_CACHE; + } + + // for test + static ConcurrentHashMap>> getEdsListeners() { + return EDS_LISTENERS; + } +} diff --git a/dubbo-xds/src/main/java/org/apache/dubbo/rpc/cluster/router/xds/RdsRouteRuleManager.java b/dubbo-xds/src/main/java/org/apache/dubbo/rpc/cluster/router/xds/RdsRouteRuleManager.java new file mode 100644 index 000000000..9d267c9d9 --- /dev/null +++ b/dubbo-xds/src/main/java/org/apache/dubbo/rpc/cluster/router/xds/RdsRouteRuleManager.java @@ -0,0 +1,162 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.dubbo.rpc.cluster.router.xds; + +import org.apache.dubbo.common.utils.CollectionUtils; +import org.apache.dubbo.common.utils.ConcurrentHashMapUtils; +import org.apache.dubbo.common.utils.ConcurrentHashSet; +import org.apache.dubbo.registry.xds.util.PilotExchanger; +import org.apache.dubbo.registry.xds.util.protocol.message.ListenerResult; +import org.apache.dubbo.registry.xds.util.protocol.message.RouteResult; +import org.apache.dubbo.rpc.cluster.router.xds.rule.XdsRouteRule; + +import io.envoyproxy.envoy.config.route.v3.VirtualHost; + +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.function.Consumer; + +public class RdsRouteRuleManager { + + private static final ConcurrentHashMap> RULE_LISTENERS = + new ConcurrentHashMap<>(); + + private static final ConcurrentHashMap> ROUTE_DATA_CACHE = new ConcurrentHashMap<>(); + + private static final ConcurrentMap RDS_LISTENERS = new ConcurrentHashMap<>(); + + private static volatile Consumer> LDS_LISTENER; + + private static volatile Consumer> RDS_LISTENER; + + private static Map RDS_RESULT; + + public RdsRouteRuleManager() {} + + public synchronized void subscribeRds(String domain, XdsRouteRuleListener listener) { + + Set listeners = + ConcurrentHashMapUtils.computeIfAbsent(RULE_LISTENERS, domain, key -> new ConcurrentHashSet<>()); + if (CollectionUtils.isEmpty(listeners)) { + doSubscribeRds(domain); + } + listeners.add(listener); + + if (ROUTE_DATA_CACHE.containsKey(domain)) { + listener.onRuleChange(domain, ROUTE_DATA_CACHE.get(domain)); + } + } + + private void doSubscribeRds(String domain) { + synchronized (RdsRouteRuleManager.class) { + if (RDS_LISTENER == null) { + RDS_LISTENER = rds -> { + if (rds == null) { + return; + } + for (RouteResult routeResult : rds.values()) { + for (String domainToNotify : RDS_LISTENERS.keySet()) { + VirtualHost virtualHost = routeResult.searchVirtualHost(domainToNotify); + if (virtualHost != null) { + RDS_LISTENERS.get(domainToNotify).parseVirtualHost(virtualHost); + } + } + } + RDS_RESULT = rds; + }; + } + if (LDS_LISTENER == null) { + LDS_LISTENER = new Consumer>() { + private volatile Set configNames = null; + + @Override + public void accept(Map listenerResults) { + if (listenerResults.size() == 1) { + for (ListenerResult listenerResult : listenerResults.values()) { + Set newConfigNames = listenerResult.getRouteConfigNames(); + if (configNames == null) { + PilotExchanger.getInstance().observeRds(newConfigNames, RDS_LISTENER); + } else if (!configNames.equals(newConfigNames)) { + PilotExchanger.getInstance().unObserveRds(configNames, RDS_LISTENER); + PilotExchanger.getInstance().observeRds(newConfigNames, RDS_LISTENER); + } + configNames = newConfigNames; + } + } + } + }; + if (PilotExchanger.isEnabled()) { + PilotExchanger.getInstance().observeLds(LDS_LISTENER); + } + } + } + ConcurrentHashMapUtils.computeIfAbsent(RDS_LISTENERS, domain, key -> new RdsVirtualHostListener(domain, this)); + RDS_LISTENER.accept(RDS_RESULT); + } + + public synchronized void unSubscribeRds(String domain, XdsRouteRuleListener listener) { + Set listeners = RULE_LISTENERS.get(domain); + if (CollectionUtils.isEmpty(listeners)) { + return; + } + listeners.remove(listener); + if (listeners.isEmpty()) { + RULE_LISTENERS.remove(domain); + doUnsubscribeRds(domain); + } + } + + private void doUnsubscribeRds(String domain) { + RDS_LISTENERS.remove(domain); + } + + public void notifyRuleChange(String domain, List xdsRouteRules) { + + ROUTE_DATA_CACHE.put(domain, xdsRouteRules); + + Set listeners = RULE_LISTENERS.get(domain); + if (CollectionUtils.isEmpty(listeners)) { + return; + } + boolean empty = CollectionUtils.isEmpty(xdsRouteRules); + for (XdsRouteRuleListener listener : listeners) { + if (empty) { + listener.clearRule(domain); + } else { + listener.onRuleChange(domain, xdsRouteRules); + } + } + } + + // for test + static ConcurrentHashMap> getRuleListeners() { + return RULE_LISTENERS; + } + + // for test + static ConcurrentHashMap> getRouteDataCache() { + return ROUTE_DATA_CACHE; + } + + // for test + static Map getRdsListeners() { + return RDS_LISTENERS; + } +} diff --git a/dubbo-xds/src/main/java/org/apache/dubbo/rpc/cluster/router/xds/RdsVirtualHostListener.java b/dubbo-xds/src/main/java/org/apache/dubbo/rpc/cluster/router/xds/RdsVirtualHostListener.java new file mode 100644 index 000000000..44338cdf4 --- /dev/null +++ b/dubbo-xds/src/main/java/org/apache/dubbo/rpc/cluster/router/xds/RdsVirtualHostListener.java @@ -0,0 +1,184 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.dubbo.rpc.cluster.router.xds; + +import org.apache.dubbo.common.constants.LoggerCodeConstants; +import org.apache.dubbo.common.logger.ErrorTypeAwareLogger; +import org.apache.dubbo.common.logger.LoggerFactory; +import org.apache.dubbo.common.utils.CollectionUtils; +import org.apache.dubbo.rpc.cluster.router.xds.rule.ClusterWeight; +import org.apache.dubbo.rpc.cluster.router.xds.rule.HTTPRouteDestination; +import org.apache.dubbo.rpc.cluster.router.xds.rule.HeaderMatcher; +import org.apache.dubbo.rpc.cluster.router.xds.rule.HttpRequestMatch; +import org.apache.dubbo.rpc.cluster.router.xds.rule.LongRangeMatch; +import org.apache.dubbo.rpc.cluster.router.xds.rule.PathMatcher; +import org.apache.dubbo.rpc.cluster.router.xds.rule.XdsRouteRule; + +import io.envoyproxy.envoy.config.route.v3.Route; +import io.envoyproxy.envoy.config.route.v3.RouteAction; +import io.envoyproxy.envoy.config.route.v3.RouteMatch; +import io.envoyproxy.envoy.config.route.v3.VirtualHost; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; + +public class RdsVirtualHostListener { + + private static final ErrorTypeAwareLogger LOGGER = + LoggerFactory.getErrorTypeAwareLogger(RdsVirtualHostListener.class); + + private final String domain; + + private final RdsRouteRuleManager routeRuleManager; + + public RdsVirtualHostListener(String domain, RdsRouteRuleManager routeRuleManager) { + this.domain = domain; + this.routeRuleManager = routeRuleManager; + } + + public void parseVirtualHost(VirtualHost virtualHost) { + if (virtualHost == null || CollectionUtils.isEmpty(virtualHost.getRoutesList())) { + // post empty + routeRuleManager.notifyRuleChange(domain, new ArrayList<>()); + return; + } + try { + List xdsRouteRules = virtualHost.getRoutesList().stream() + .map(route -> { + if (route.getMatch().getQueryParametersCount() != 0) { + return null; + } + HttpRequestMatch match = parseMatch(route.getMatch()); + HTTPRouteDestination action = parseAction(route); + return new XdsRouteRule(match, action); + }) + .filter(Objects::nonNull) + .collect(Collectors.toList()); + // post rules + routeRuleManager.notifyRuleChange(domain, xdsRouteRules); + } catch (Exception e) { + LOGGER.error( + LoggerCodeConstants.INTERNAL_ERROR, + "", + "", + "parse domain: " + domain + " xds VirtualHost error", + e); + } + } + + private HttpRequestMatch parseMatch(RouteMatch match) { + PathMatcher pathMatcher = parsePathMatch(match); + List headerMatchers = parseHeadMatch(match); + return new HttpRequestMatch(pathMatcher, headerMatchers); + } + + private PathMatcher parsePathMatch(RouteMatch match) { + boolean caseSensitive = match.getCaseSensitive().getValue(); + PathMatcher pathMatcher = new PathMatcher(); + pathMatcher.setCaseSensitive(caseSensitive); + switch (match.getPathSpecifierCase()) { + case PREFIX: + pathMatcher.setPrefix(match.getPrefix()); + return pathMatcher; + case PATH: + pathMatcher.setPath(match.getPath()); + return pathMatcher; + case SAFE_REGEX: + String regex = match.getSafeRegex().getRegex(); + pathMatcher.setRegex(regex); + return pathMatcher; + case PATHSPECIFIER_NOT_SET: + return null; + default: + throw new IllegalArgumentException("Path specifier is not expect"); + } + } + + private List parseHeadMatch(RouteMatch routeMatch) { + List headerMatchers = new ArrayList<>(); + List headersList = routeMatch.getHeadersList(); + for (io.envoyproxy.envoy.config.route.v3.HeaderMatcher headerMatcher : headersList) { + HeaderMatcher matcher = new HeaderMatcher(); + matcher.setName(headerMatcher.getName()); + matcher.setInverted(headerMatcher.getInvertMatch()); + switch (headerMatcher.getHeaderMatchSpecifierCase()) { + case EXACT_MATCH: + matcher.setExactValue(headerMatcher.getExactMatch()); + headerMatchers.add(matcher); + break; + case SAFE_REGEX_MATCH: + matcher.setRegex(headerMatcher.getSafeRegexMatch().getRegex()); + headerMatchers.add(matcher); + break; + case RANGE_MATCH: + LongRangeMatch rang = new LongRangeMatch(); + rang.setStart(headerMatcher.getRangeMatch().getStart()); + rang.setEnd(headerMatcher.getRangeMatch().getEnd()); + matcher.setRange(rang); + headerMatchers.add(matcher); + break; + case PRESENT_MATCH: + matcher.setPresent(headerMatcher.getPresentMatch()); + headerMatchers.add(matcher); + break; + case PREFIX_MATCH: + matcher.setPrefix(headerMatcher.getPrefixMatch()); + headerMatchers.add(matcher); + break; + case SUFFIX_MATCH: + matcher.setSuffix(headerMatcher.getSuffixMatch()); + headerMatchers.add(matcher); + break; + case HEADERMATCHSPECIFIER_NOT_SET: + default: + throw new IllegalArgumentException("Header specifier is not expect"); + } + } + return headerMatchers; + } + + private HTTPRouteDestination parseAction(Route route) { + switch (route.getActionCase()) { + case ROUTE: + HTTPRouteDestination httpRouteDestination = new HTTPRouteDestination(); + // only support cluster and weight cluster + RouteAction routeAction = route.getRoute(); + RouteAction.ClusterSpecifierCase clusterSpecifierCase = routeAction.getClusterSpecifierCase(); + if (clusterSpecifierCase == RouteAction.ClusterSpecifierCase.CLUSTER) { + httpRouteDestination.setCluster(routeAction.getCluster()); + return httpRouteDestination; + } else if (clusterSpecifierCase == RouteAction.ClusterSpecifierCase.WEIGHTED_CLUSTERS) { + List clusterWeights = routeAction.getWeightedClusters().getClustersList().stream() + .map(c -> + new ClusterWeight(c.getName(), c.getWeight().getValue())) + .sorted(Comparator.comparing(ClusterWeight::getWeight)) + .collect(Collectors.toList()); + httpRouteDestination.setWeightedClusters(clusterWeights); + return httpRouteDestination; + } + case REDIRECT: + case DIRECT_RESPONSE: + case FILTER_ACTION: + case ACTION_NOT_SET: + default: + throw new IllegalArgumentException("Cluster specifier is not expect"); + } + } +} diff --git a/dubbo-xds/src/main/java/org/apache/dubbo/rpc/cluster/router/xds/XdsRouteRuleListener.java b/dubbo-xds/src/main/java/org/apache/dubbo/rpc/cluster/router/xds/XdsRouteRuleListener.java new file mode 100644 index 000000000..4e90fe2d4 --- /dev/null +++ b/dubbo-xds/src/main/java/org/apache/dubbo/rpc/cluster/router/xds/XdsRouteRuleListener.java @@ -0,0 +1,28 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.dubbo.rpc.cluster.router.xds; + +import org.apache.dubbo.rpc.cluster.router.xds.rule.XdsRouteRule; + +import java.util.List; + +public interface XdsRouteRuleListener { + + void onRuleChange(String appName, List xdsRouteRules); + + void clearRule(String appName); +} diff --git a/dubbo-xds/src/main/java/org/apache/dubbo/rpc/cluster/router/xds/XdsRouter.java b/dubbo-xds/src/main/java/org/apache/dubbo/rpc/cluster/router/xds/XdsRouter.java new file mode 100644 index 000000000..e043e2d3d --- /dev/null +++ b/dubbo-xds/src/main/java/org/apache/dubbo/rpc/cluster/router/xds/XdsRouter.java @@ -0,0 +1,391 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.dubbo.rpc.cluster.router.xds; + +import org.apache.dubbo.common.URL; +import org.apache.dubbo.common.utils.CollectionUtils; +import org.apache.dubbo.common.utils.ConcurrentHashSet; +import org.apache.dubbo.common.utils.Holder; +import org.apache.dubbo.common.utils.StringUtils; +import org.apache.dubbo.registry.xds.util.PilotExchanger; +import org.apache.dubbo.registry.xds.util.protocol.message.Endpoint; +import org.apache.dubbo.rpc.Invocation; +import org.apache.dubbo.rpc.Invoker; +import org.apache.dubbo.rpc.RpcException; +import org.apache.dubbo.rpc.cluster.router.RouterSnapshotNode; +import org.apache.dubbo.rpc.cluster.router.state.AbstractStateRouter; +import org.apache.dubbo.rpc.cluster.router.state.BitList; +import org.apache.dubbo.rpc.cluster.router.xds.rule.ClusterWeight; +import org.apache.dubbo.rpc.cluster.router.xds.rule.DestinationSubset; +import org.apache.dubbo.rpc.cluster.router.xds.rule.HTTPRouteDestination; +import org.apache.dubbo.rpc.cluster.router.xds.rule.HeaderMatcher; +import org.apache.dubbo.rpc.cluster.router.xds.rule.HttpRequestMatch; +import org.apache.dubbo.rpc.cluster.router.xds.rule.PathMatcher; +import org.apache.dubbo.rpc.cluster.router.xds.rule.XdsRouteRule; +import org.apache.dubbo.rpc.support.RpcUtils; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ThreadLocalRandom; +import java.util.stream.Collectors; + +public class XdsRouter extends AbstractStateRouter implements XdsRouteRuleListener, EdsEndpointListener { + + private Set subscribeApplications; + + private final ConcurrentHashMap> xdsRouteRuleMap; + + private final ConcurrentHashMap> destinationSubsetMap; + + private final RdsRouteRuleManager rdsRouteRuleManager; + + private final EdsEndpointManager edsEndpointManager; + + private volatile BitList> currentInvokeList; + + private static final String BINARY_HEADER_SUFFIX = "-bin"; + + private final boolean isEnable; + + public XdsRouter(URL url) { + super(url); + isEnable = PilotExchanger.isEnabled(); + rdsRouteRuleManager = + url.getOrDefaultApplicationModel().getBeanFactory().getBean(RdsRouteRuleManager.class); + edsEndpointManager = url.getOrDefaultApplicationModel().getBeanFactory().getBean(EdsEndpointManager.class); + subscribeApplications = new ConcurrentHashSet<>(); + destinationSubsetMap = new ConcurrentHashMap<>(); + xdsRouteRuleMap = new ConcurrentHashMap<>(); + currentInvokeList = new BitList<>(new ArrayList<>()); + } + + /** + * @deprecated only for uts + */ + protected XdsRouter( + URL url, RdsRouteRuleManager rdsRouteRuleManager, EdsEndpointManager edsEndpointManager, boolean isEnable) { + super(url); + this.isEnable = isEnable; + this.rdsRouteRuleManager = rdsRouteRuleManager; + this.edsEndpointManager = edsEndpointManager; + subscribeApplications = new ConcurrentHashSet<>(); + destinationSubsetMap = new ConcurrentHashMap<>(); + xdsRouteRuleMap = new ConcurrentHashMap<>(); + currentInvokeList = new BitList<>(new ArrayList<>()); + } + + @Override + protected BitList> doRoute( + BitList> invokers, + URL url, + Invocation invocation, + boolean needToPrintMessage, + Holder> nodeHolder, + Holder messageHolder) + throws RpcException { + if (!isEnable) { + if (needToPrintMessage) { + messageHolder.set( + "Directly Return. Reason: Pilot exchanger has not been initialized, may not in mesh mode."); + } + return invokers; + } + + if (CollectionUtils.isEmpty(invokers)) { + if (needToPrintMessage) { + messageHolder.set("Directly Return. Reason: Invokers from previous router is empty."); + } + return invokers; + } + + if (CollectionUtils.isEmptyMap(xdsRouteRuleMap)) { + if (needToPrintMessage) { + messageHolder.set("Directly Return. Reason: xds route rule is empty."); + } + return invokers; + } + + StringBuilder stringBuilder = needToPrintMessage ? new StringBuilder() : null; + + // find match cluster + String matchCluster = null; + Set appNames = subscribeApplications; + for (String subscribeApplication : appNames) { + List rules = xdsRouteRuleMap.get(subscribeApplication); + if (CollectionUtils.isEmpty(rules)) { + continue; + } + for (XdsRouteRule rule : rules) { + String cluster = computeMatchCluster(invocation, rule); + if (cluster != null) { + matchCluster = cluster; + break; + } + } + if (matchCluster != null) { + if (stringBuilder != null) { + stringBuilder + .append("Match App: ") + .append(subscribeApplication) + .append(" Cluster: ") + .append(matchCluster) + .append(' '); + } + break; + } + } + // not match request just return + if (matchCluster == null) { + if (needToPrintMessage) { + messageHolder.set("Directly Return. Reason: xds rule not match."); + } + return invokers; + } + DestinationSubset destinationSubset = destinationSubsetMap.get(matchCluster); + // cluster no target provider + if (destinationSubset == null) { + if (needToPrintMessage) { + messageHolder.set(stringBuilder.append("no target subset").toString()); + } + return BitList.emptyList(); + } + if (needToPrintMessage) { + messageHolder.set(stringBuilder.toString()); + } + if (destinationSubset.getInvokers() == null) { + return BitList.emptyList(); + } + + return destinationSubset.getInvokers().and(invokers); + } + + private String computeMatchCluster(Invocation invocation, XdsRouteRule rule) { + // compute request match cluster + HttpRequestMatch requestMatch = rule.getMatch(); + if (requestMatch.getPathMatcher() == null && CollectionUtils.isEmpty(requestMatch.getHeaderMatcherList())) { + return null; + } + PathMatcher pathMatcher = requestMatch.getPathMatcher(); + if (pathMatcher != null) { + String path = "/" + invocation.getInvoker().getUrl().getPath() + "/" + RpcUtils.getMethodName(invocation); + if (!pathMatcher.isMatch(path)) { + return null; + } + } + List headerMatchers = requestMatch.getHeaderMatcherList(); + for (HeaderMatcher headerMatcher : headerMatchers) { + String headerName = headerMatcher.getName(); + // not support byte + if (headerName.endsWith(BINARY_HEADER_SUFFIX)) { + return null; + } + String headValue = invocation.getAttachment(headerName); + if (!headerMatcher.match(headValue)) { + return null; + } + } + HTTPRouteDestination route = rule.getRoute(); + if (route.getCluster() != null) { + return route.getCluster(); + } + return computeWeightCluster(route.getWeightedClusters()); + } + + private String computeWeightCluster(List weightedClusters) { + int totalWeight = Math.max( + weightedClusters.stream().mapToInt(ClusterWeight::getWeight).sum(), 1); + // target must greater than 0 + // if weight is 0, the destination will not receive any traffic. + int target = ThreadLocalRandom.current().nextInt(1, totalWeight + 1); + for (ClusterWeight weightedCluster : weightedClusters) { + int weight = weightedCluster.getWeight(); + target -= weight; + if (target <= 0) { + return weightedCluster.getName(); + } + } + return null; + } + + public void notify(BitList> invokers) { + BitList> invokerList = invokers == null ? BitList.emptyList() : invokers; + currentInvokeList = invokerList.clone(); + + // compute need subscribe/unsubscribe rds application + Set currentApplications = new HashSet<>(); + for (Invoker invoker : invokerList) { + String applicationName = invoker.getUrl().getRemoteApplication(); + if (StringUtils.isNotEmpty(applicationName)) { + currentApplications.add(applicationName); + } + } + + if (!subscribeApplications.equals(currentApplications)) { + synchronized (this) { + for (String currentApplication : currentApplications) { + if (!subscribeApplications.contains(currentApplication)) { + rdsRouteRuleManager.subscribeRds(currentApplication, this); + } + } + for (String preApplication : subscribeApplications) { + if (!currentApplications.contains(preApplication)) { + rdsRouteRuleManager.unSubscribeRds(preApplication, this); + } + } + subscribeApplications = currentApplications; + } + } + + // update subset + synchronized (this) { + BitList> allInvokers = currentInvokeList.clone(); + for (DestinationSubset subset : destinationSubsetMap.values()) { + computeSubset(subset, allInvokers); + } + } + } + + private void computeSubset(DestinationSubset subset, BitList> invokers) { + Set endpoints = subset.getEndpoints(); + List> filterInvokers = invokers.stream() + .filter(inv -> { + String host = inv.getUrl().getHost(); + int port = inv.getUrl().getPort(); + Optional any = endpoints.stream() + .filter(end -> host.equals(end.getAddress()) && port == end.getPortValue()) + .findAny(); + return any.isPresent(); + }) + .collect(Collectors.toList()); + subset.setInvokers(new BitList<>(filterInvokers)); + } + + @Override + public synchronized void onRuleChange(String appName, List xdsRouteRules) { + if (CollectionUtils.isEmpty(xdsRouteRules)) { + clearRule(appName); + return; + } + Set oldCluster = getAllCluster(); + xdsRouteRuleMap.put(appName, xdsRouteRules); + Set newCluster = getAllCluster(); + changeClusterSubscribe(oldCluster, newCluster); + } + + private Set getAllCluster() { + if (CollectionUtils.isEmptyMap(xdsRouteRuleMap)) { + return new HashSet<>(); + } + Set clusters = new HashSet<>(); + xdsRouteRuleMap.forEach((appName, rules) -> { + for (XdsRouteRule rule : rules) { + HTTPRouteDestination action = rule.getRoute(); + if (action.getCluster() != null) { + clusters.add(action.getCluster()); + } else if (CollectionUtils.isNotEmpty(action.getWeightedClusters())) { + for (ClusterWeight weightedCluster : action.getWeightedClusters()) { + clusters.add(weightedCluster.getName()); + } + } + } + }); + return clusters; + } + + private void changeClusterSubscribe(Set oldCluster, Set newCluster) { + Set removeSubscribe = new HashSet<>(oldCluster); + Set addSubscribe = new HashSet<>(newCluster); + + removeSubscribe.removeAll(newCluster); + addSubscribe.removeAll(oldCluster); + // remove subscribe cluster + for (String cluster : removeSubscribe) { + edsEndpointManager.unSubscribeEds(cluster, this); + destinationSubsetMap.remove(cluster); + } + // add subscribe cluster + for (String cluster : addSubscribe) { + destinationSubsetMap.put(cluster, new DestinationSubset<>(cluster)); + edsEndpointManager.subscribeEds(cluster, this); + } + } + + @Override + public synchronized void clearRule(String appName) { + Set oldCluster = getAllCluster(); + List oldRules = xdsRouteRuleMap.remove(appName); + if (CollectionUtils.isEmpty(oldRules)) { + return; + } + Set newCluster = getAllCluster(); + changeClusterSubscribe(oldCluster, newCluster); + } + + @Override + public synchronized void onEndPointChange(String cluster, Set endpoints) { + // find and update subset + DestinationSubset subset = destinationSubsetMap.get(cluster); + if (subset == null) { + return; + } + subset.setEndpoints(endpoints); + computeSubset(subset, currentInvokeList.clone()); + } + + @Override + public void stop() { + for (String app : subscribeApplications) { + rdsRouteRuleManager.unSubscribeRds(app, this); + } + for (String cluster : getAllCluster()) { + edsEndpointManager.unSubscribeEds(cluster, this); + } + } + + @Deprecated + Set getSubscribeApplications() { + return subscribeApplications; + } + + /** + * for ut only + */ + @Deprecated + BitList> getInvokerList() { + return currentInvokeList; + } + + /** + * for ut only + */ + @Deprecated + ConcurrentHashMap> getXdsRouteRuleMap() { + return xdsRouteRuleMap; + } + + /** + * for ut only + */ + @Deprecated + ConcurrentHashMap> getDestinationSubsetMap() { + return destinationSubsetMap; + } +} diff --git a/dubbo-xds/src/main/java/org/apache/dubbo/rpc/cluster/router/xds/XdsRouterFactory.java b/dubbo-xds/src/main/java/org/apache/dubbo/rpc/cluster/router/xds/XdsRouterFactory.java new file mode 100644 index 000000000..0e9a0c575 --- /dev/null +++ b/dubbo-xds/src/main/java/org/apache/dubbo/rpc/cluster/router/xds/XdsRouterFactory.java @@ -0,0 +1,31 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.dubbo.rpc.cluster.router.xds; + +import org.apache.dubbo.common.URL; +import org.apache.dubbo.common.extension.Activate; +import org.apache.dubbo.rpc.cluster.router.state.StateRouter; +import org.apache.dubbo.rpc.cluster.router.state.StateRouterFactory; + +@Activate(order = 100) +public class XdsRouterFactory implements StateRouterFactory { + + @Override + public StateRouter getRouter(Class interfaceClass, URL url) { + return new XdsRouter<>(url); + } +} diff --git a/dubbo-xds/src/main/java/org/apache/dubbo/rpc/cluster/router/xds/XdsScopeModelInitializer.java b/dubbo-xds/src/main/java/org/apache/dubbo/rpc/cluster/router/xds/XdsScopeModelInitializer.java new file mode 100644 index 000000000..b34b86ffc --- /dev/null +++ b/dubbo-xds/src/main/java/org/apache/dubbo/rpc/cluster/router/xds/XdsScopeModelInitializer.java @@ -0,0 +1,39 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.dubbo.rpc.cluster.router.xds; + +import org.apache.dubbo.common.beans.factory.ScopeBeanFactory; +import org.apache.dubbo.rpc.model.ApplicationModel; +import org.apache.dubbo.rpc.model.FrameworkModel; +import org.apache.dubbo.rpc.model.ModuleModel; +import org.apache.dubbo.rpc.model.ScopeModelInitializer; + +public class XdsScopeModelInitializer implements ScopeModelInitializer { + + @Override + public void initializeFrameworkModel(FrameworkModel frameworkModel) {} + + @Override + public void initializeApplicationModel(ApplicationModel applicationModel) { + ScopeBeanFactory beanFactory = applicationModel.getBeanFactory(); + beanFactory.registerBean(RdsRouteRuleManager.class); + beanFactory.registerBean(EdsEndpointManager.class); + } + + @Override + public void initializeModuleModel(ModuleModel moduleModel) {} +} diff --git a/dubbo-xds/src/main/java/org/apache/dubbo/rpc/cluster/router/xds/rule/ClusterWeight.java b/dubbo-xds/src/main/java/org/apache/dubbo/rpc/cluster/router/xds/rule/ClusterWeight.java new file mode 100644 index 000000000..fe1307c8f --- /dev/null +++ b/dubbo-xds/src/main/java/org/apache/dubbo/rpc/cluster/router/xds/rule/ClusterWeight.java @@ -0,0 +1,37 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.dubbo.rpc.cluster.router.xds.rule; + +public class ClusterWeight { + + private final String name; + + private final int weight; + + public ClusterWeight(String name, int weight) { + this.name = name; + this.weight = weight; + } + + public String getName() { + return name; + } + + public int getWeight() { + return weight; + } +} diff --git a/dubbo-xds/src/main/java/org/apache/dubbo/rpc/cluster/router/xds/rule/DestinationSubset.java b/dubbo-xds/src/main/java/org/apache/dubbo/rpc/cluster/router/xds/rule/DestinationSubset.java new file mode 100644 index 000000000..79fa2156f --- /dev/null +++ b/dubbo-xds/src/main/java/org/apache/dubbo/rpc/cluster/router/xds/rule/DestinationSubset.java @@ -0,0 +1,57 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.dubbo.rpc.cluster.router.xds.rule; + +import org.apache.dubbo.registry.xds.util.protocol.message.Endpoint; +import org.apache.dubbo.rpc.Invoker; +import org.apache.dubbo.rpc.cluster.router.state.BitList; + +import java.util.HashSet; +import java.util.Set; + +public class DestinationSubset { + + public DestinationSubset(String clusterName) { + this.clusterName = clusterName; + } + + private final String clusterName; + + private Set endpoints = new HashSet<>(); + + private BitList> invokers; + + public String getClusterName() { + return clusterName; + } + + public Set getEndpoints() { + return endpoints; + } + + public void setEndpoints(Set endpoints) { + this.endpoints = endpoints; + } + + public BitList> getInvokers() { + return invokers; + } + + public void setInvokers(BitList> invokers) { + this.invokers = invokers; + } +} diff --git a/dubbo-xds/src/main/java/org/apache/dubbo/rpc/cluster/router/xds/rule/HTTPRouteDestination.java b/dubbo-xds/src/main/java/org/apache/dubbo/rpc/cluster/router/xds/rule/HTTPRouteDestination.java new file mode 100644 index 000000000..91d55d337 --- /dev/null +++ b/dubbo-xds/src/main/java/org/apache/dubbo/rpc/cluster/router/xds/rule/HTTPRouteDestination.java @@ -0,0 +1,42 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.dubbo.rpc.cluster.router.xds.rule; + +import java.util.List; + +public class HTTPRouteDestination { + + private String cluster; + + private List weightedClusters; + + public String getCluster() { + return cluster; + } + + public void setCluster(String cluster) { + this.cluster = cluster; + } + + public List getWeightedClusters() { + return weightedClusters; + } + + public void setWeightedClusters(List weightedClusters) { + this.weightedClusters = weightedClusters; + } +} diff --git a/dubbo-xds/src/main/java/org/apache/dubbo/rpc/cluster/router/xds/rule/HeaderMatcher.java b/dubbo-xds/src/main/java/org/apache/dubbo/rpc/cluster/router/xds/rule/HeaderMatcher.java new file mode 100644 index 000000000..04b0c45a0 --- /dev/null +++ b/dubbo-xds/src/main/java/org/apache/dubbo/rpc/cluster/router/xds/rule/HeaderMatcher.java @@ -0,0 +1,121 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.dubbo.rpc.cluster.router.xds.rule; + +public class HeaderMatcher { + + public String name; + + public String exactValue; + + private String regex; + + public LongRangeMatch range; + + public Boolean present; + + public String prefix; + + public String suffix; + + public boolean inverted; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getExactValue() { + return exactValue; + } + + public void setExactValue(String exactValue) { + this.exactValue = exactValue; + } + + public String getRegex() { + return regex; + } + + public void setRegex(String regex) { + this.regex = regex; + } + + public LongRangeMatch getRange() { + return range; + } + + public void setRange(LongRangeMatch range) { + this.range = range; + } + + public Boolean getPresent() { + return present; + } + + public void setPresent(Boolean present) { + this.present = present; + } + + public String getPrefix() { + return prefix; + } + + public void setPrefix(String prefix) { + this.prefix = prefix; + } + + public String getSuffix() { + return suffix; + } + + public void setSuffix(String suffix) { + this.suffix = suffix; + } + + public boolean isInverted() { + return inverted; + } + + public void setInverted(boolean inverted) { + this.inverted = inverted; + } + + public boolean match(String input) { + if (getPresent() != null) { + return (input == null) == getPresent().equals(isInverted()); + } + if (input == null) { + return false; + } + if (getExactValue() != null) { + return getExactValue().equals(input) != isInverted(); + } else if (getRegex() != null) { + return input.matches(getRegex()) != isInverted(); + } else if (getRange() != null) { + return getRange().isMatch(input) != isInverted(); + } else if (getPrefix() != null) { + return input.startsWith(getPrefix()) != isInverted(); + } else if (getSuffix() != null) { + return input.endsWith(getSuffix()) != isInverted(); + } + return false; + } +} diff --git a/dubbo-xds/src/main/java/org/apache/dubbo/rpc/cluster/router/xds/rule/HttpRequestMatch.java b/dubbo-xds/src/main/java/org/apache/dubbo/rpc/cluster/router/xds/rule/HttpRequestMatch.java new file mode 100644 index 000000000..fef5aa18e --- /dev/null +++ b/dubbo-xds/src/main/java/org/apache/dubbo/rpc/cluster/router/xds/rule/HttpRequestMatch.java @@ -0,0 +1,39 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.dubbo.rpc.cluster.router.xds.rule; + +import java.util.List; + +public class HttpRequestMatch { + + private final PathMatcher pathMatcher; + + private final List headerMatcherList; + + public HttpRequestMatch(PathMatcher pathMatcher, List headerMatcherList) { + this.pathMatcher = pathMatcher; + this.headerMatcherList = headerMatcherList; + } + + public PathMatcher getPathMatcher() { + return pathMatcher; + } + + public List getHeaderMatcherList() { + return headerMatcherList; + } +} diff --git a/dubbo-xds/src/main/java/org/apache/dubbo/rpc/cluster/router/xds/rule/LongRangeMatch.java b/dubbo-xds/src/main/java/org/apache/dubbo/rpc/cluster/router/xds/rule/LongRangeMatch.java new file mode 100644 index 000000000..df482575b --- /dev/null +++ b/dubbo-xds/src/main/java/org/apache/dubbo/rpc/cluster/router/xds/rule/LongRangeMatch.java @@ -0,0 +1,47 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.dubbo.rpc.cluster.router.xds.rule; + +public class LongRangeMatch { + private long start; + private long end; + + public long getStart() { + return start; + } + + public void setStart(long start) { + this.start = start; + } + + public long getEnd() { + return end; + } + + public void setEnd(long end) { + this.end = end; + } + + public boolean isMatch(String input) { + try { + long num = Long.parseLong(input); + return num >= getStart() && num <= getEnd(); + } catch (NumberFormatException ignore) { + return false; + } + } +} diff --git a/dubbo-xds/src/main/java/org/apache/dubbo/rpc/cluster/router/xds/rule/PathMatcher.java b/dubbo-xds/src/main/java/org/apache/dubbo/rpc/cluster/router/xds/rule/PathMatcher.java new file mode 100644 index 000000000..cbf77e871 --- /dev/null +++ b/dubbo-xds/src/main/java/org/apache/dubbo/rpc/cluster/router/xds/rule/PathMatcher.java @@ -0,0 +1,71 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.dubbo.rpc.cluster.router.xds.rule; + +public class PathMatcher { + + private String path; + + private String prefix; + + private String regex; + + private boolean caseSensitive; + + public String getPath() { + return path; + } + + public void setPath(String path) { + this.path = path; + } + + public String getPrefix() { + return prefix; + } + + public void setPrefix(String prefix) { + this.prefix = prefix; + } + + public String getRegex() { + return regex; + } + + public void setRegex(String regex) { + this.regex = regex; + } + + public boolean isCaseSensitive() { + return caseSensitive; + } + + public void setCaseSensitive(boolean caseSensitive) { + this.caseSensitive = caseSensitive; + } + + public boolean isMatch(String input) { + if (getPath() != null) { + return isCaseSensitive() ? getPath().equals(input) : getPath().equalsIgnoreCase(input); + } else if (getPrefix() != null) { + return isCaseSensitive() + ? input.startsWith(getPrefix()) + : input.toLowerCase().startsWith(getPrefix()); + } + return input.matches(getRegex()); + } +} diff --git a/dubbo-xds/src/main/java/org/apache/dubbo/rpc/cluster/router/xds/rule/XdsRouteRule.java b/dubbo-xds/src/main/java/org/apache/dubbo/rpc/cluster/router/xds/rule/XdsRouteRule.java new file mode 100644 index 000000000..5d2994d2d --- /dev/null +++ b/dubbo-xds/src/main/java/org/apache/dubbo/rpc/cluster/router/xds/rule/XdsRouteRule.java @@ -0,0 +1,37 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.dubbo.rpc.cluster.router.xds.rule; + +public class XdsRouteRule { + + private final HttpRequestMatch match; + + private final HTTPRouteDestination route; + + public XdsRouteRule(HttpRequestMatch match, HTTPRouteDestination route) { + this.match = match; + this.route = route; + } + + public HttpRequestMatch getMatch() { + return match; + } + + public HTTPRouteDestination getRoute() { + return route; + } +} diff --git a/dubbo-xds/src/main/proto/ca.proto b/dubbo-xds/src/main/proto/ca.proto new file mode 100644 index 000000000..41e6addb7 --- /dev/null +++ b/dubbo-xds/src/main/proto/ca.proto @@ -0,0 +1,62 @@ +// Copyright Istio 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. + +// The canonical version of this proto can be found at +// https://github.com/istio/api/blob/9abf4c87205f6ad04311fa021ce60803d8b95f78/security/v1alpha1/ca.proto + +syntax = "proto3"; + +import "google/protobuf/struct.proto"; + +// Keep this package for backward compatibility. +package istio.v1.auth; + +option go_package = "istio.io/api/security/v1alpha1"; +option java_generic_services = true; +option java_multiple_files = true; + +// Certificate request message. The authentication should be based on: +// 1. Bearer tokens carried in the side channel; +// 2. Client-side certificate via Mutual TLS handshake. +// Note: the service implementation is REQUIRED to verify the authenticated caller is authorize to +// all SANs in the CSR. The server side may overwrite any requested certificate field based on its +// policies. +message IstioCertificateRequest { + // PEM-encoded certificate request. + // The public key in the CSR is used to generate the certificate, + // and other fields in the generated certificate may be overwritten by the CA. + string csr = 1; + // Optional: requested certificate validity period, in seconds. + int64 validity_duration = 3; + + // $hide_from_docs + // Optional: Opaque metadata provided by the XDS node to Istio. + // Supported metadata: WorkloadName, WorkloadIP, ClusterID + google.protobuf.Struct metadata = 4; +} + +// Certificate response message. +message IstioCertificateResponse { + // PEM-encoded certificate chain. + // The leaf cert is the first element, and the root cert is the last element. + repeated string cert_chain = 1; +} + +// Service for managing certificates issued by the CA. +service IstioCertificateService { + // Using provided CSR, returns a signed certificate. + rpc CreateCertificate(IstioCertificateRequest) + returns (IstioCertificateResponse) { + } +} diff --git a/dubbo-xds/src/main/resources/META-INF/dubbo/internal/org.apache.dubbo.registry.RegistryFactory b/dubbo-xds/src/main/resources/META-INF/dubbo/internal/org.apache.dubbo.registry.RegistryFactory new file mode 100644 index 000000000..2ee954e95 --- /dev/null +++ b/dubbo-xds/src/main/resources/META-INF/dubbo/internal/org.apache.dubbo.registry.RegistryFactory @@ -0,0 +1 @@ +xds=org.apache.dubbo.registry.xds.XdsRegistryFactory diff --git a/dubbo-xds/src/main/resources/META-INF/dubbo/internal/org.apache.dubbo.registry.client.ServiceDiscoveryFactory b/dubbo-xds/src/main/resources/META-INF/dubbo/internal/org.apache.dubbo.registry.client.ServiceDiscoveryFactory new file mode 100644 index 000000000..e6dfce272 --- /dev/null +++ b/dubbo-xds/src/main/resources/META-INF/dubbo/internal/org.apache.dubbo.registry.client.ServiceDiscoveryFactory @@ -0,0 +1 @@ +xds=org.apache.dubbo.registry.xds.XdsServiceDiscoveryFactory diff --git a/dubbo-xds/src/main/resources/META-INF/dubbo/internal/org.apache.dubbo.registry.xds.XdsCertificateSigner b/dubbo-xds/src/main/resources/META-INF/dubbo/internal/org.apache.dubbo.registry.xds.XdsCertificateSigner new file mode 100644 index 000000000..bbfc0fb9d --- /dev/null +++ b/dubbo-xds/src/main/resources/META-INF/dubbo/internal/org.apache.dubbo.registry.xds.XdsCertificateSigner @@ -0,0 +1 @@ +istio=org.apache.dubbo.registry.xds.istio.IstioCitadelCertificateSigner diff --git a/dubbo-xds/src/main/resources/META-INF/dubbo/internal/org.apache.dubbo.rpc.cluster.router.state.StateRouterFactory b/dubbo-xds/src/main/resources/META-INF/dubbo/internal/org.apache.dubbo.rpc.cluster.router.state.StateRouterFactory new file mode 100644 index 000000000..ca9b94ea8 --- /dev/null +++ b/dubbo-xds/src/main/resources/META-INF/dubbo/internal/org.apache.dubbo.rpc.cluster.router.state.StateRouterFactory @@ -0,0 +1 @@ +xds=org.apache.dubbo.rpc.cluster.router.xds.XdsRouterFactory \ No newline at end of file diff --git a/dubbo-xds/src/main/resources/META-INF/dubbo/internal/org.apache.dubbo.rpc.model.ScopeModelInitializer b/dubbo-xds/src/main/resources/META-INF/dubbo/internal/org.apache.dubbo.rpc.model.ScopeModelInitializer new file mode 100644 index 000000000..3005831c6 --- /dev/null +++ b/dubbo-xds/src/main/resources/META-INF/dubbo/internal/org.apache.dubbo.rpc.model.ScopeModelInitializer @@ -0,0 +1 @@ +xds-route=org.apache.dubbo.rpc.cluster.router.xds.XdsScopeModelInitializer diff --git a/dubbo-xds/src/test/java/org/apache/dubbo/registry/xds/util/bootstrap/BootstrapperTest.java b/dubbo-xds/src/test/java/org/apache/dubbo/registry/xds/util/bootstrap/BootstrapperTest.java new file mode 100644 index 000000000..63188233f --- /dev/null +++ b/dubbo-xds/src/test/java/org/apache/dubbo/registry/xds/util/bootstrap/BootstrapperTest.java @@ -0,0 +1,148 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.dubbo.registry.xds.util.bootstrap; + +import org.apache.dubbo.common.URL; +import org.apache.dubbo.common.url.component.URLAddress; +import org.apache.dubbo.registry.xds.XdsInitializationException; + +import io.grpc.netty.shaded.io.netty.channel.unix.DomainSocketAddress; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import java.util.List; + +class BootstrapperTest { + @Test + void testParse() throws XdsInitializationException { + String rawData = "{\n" + " \"xds_servers\": [\n" + + " {\n" + + " \"server_uri\": \"unix:///etc/istio/proxy/XDS\",\n" + + " \"channel_creds\": [\n" + + " {\n" + + " \"type\": \"insecure\"\n" + + " }\n" + + " ],\n" + + " \"server_features\": [\n" + + " \"xds_v3\"\n" + + " ]\n" + + " }\n" + + " ],\n" + + " \"node\": {\n" + + " \"id\": \"sidecar~172.17.0.4~dubbo-demo-consumer-deployment-grpc-agent-58585cb9cd-gp79p.dubbo-demo~dubbo-demo.svc.cluster.local\",\n" + + " \"metadata\": {\n" + + " \"ANNOTATIONS\": {\n" + + " \"inject.istio.io/templates\": \"grpc-agent\",\n" + + " \"kubernetes.io/config.seen\": \"2022-07-19T12:53:29.742565722Z\",\n" + + " \"kubernetes.io/config.source\": \"api\",\n" + + " \"prometheus.io/path\": \"/stats/prometheus\",\n" + + " \"prometheus.io/port\": \"15020\",\n" + + " \"prometheus.io/scrape\": \"true\",\n" + + " \"proxy.istio.io/config\": \"{\\\"holdApplicationUntilProxyStarts\\\": true}\",\n" + + " \"proxy.istio.io/overrides\": \"{\\\"containers\\\":[{\\\"name\\\":\\\"app\\\",\\\"image\\\":\\\"gcr.io/istio-testing/app:latest\\\",\\\"args\\\":[\\\"--metrics=15014\\\",\\\"--port\\\",\\\"18080\\\",\\\"--tcp\\\",\\\"19090\\\",\\\"--xds-grpc-server=17070\\\",\\\"--grpc\\\",\\\"17070\\\",\\\"--grpc\\\",\\\"17171\\\",\\\"--port\\\",\\\"3333\\\",\\\"--port\\\",\\\"8080\\\",\\\"--version\\\",\\\"v1\\\",\\\"--crt=/cert.crt\\\",\\\"--key=/cert.key\\\"],\\\"ports\\\":[{\\\"containerPort\\\":17070,\\\"protocol\\\":\\\"TCP\\\"},{\\\"containerPort\\\":17171,\\\"protocol\\\":\\\"TCP\\\"},{\\\"containerPort\\\":8080,\\\"protocol\\\":\\\"TCP\\\"},{\\\"name\\\":\\\"tcp-health-port\\\",\\\"containerPort\\\":3333,\\\"protocol\\\":\\\"TCP\\\"}],\\\"env\\\":[{\\\"name\\\":\\\"INSTANCE_IP\\\",\\\"valueFrom\\\":{\\\"fieldRef\\\":{\\\"apiVersion\\\":\\\"v1\\\",\\\"fieldPath\\\":\\\"status.podIP\\\"}}}],\\\"resources\\\":{},\\\"volumeMounts\\\":[{\\\"name\\\":\\\"kube-api-access-2tknx\\\",\\\"readOnly\\\":true,\\\"mountPath\\\":\\\"/var/run/secrets/kubernetes.io/serviceaccount\\\"}],\\\"livenessProbe\\\":{\\\"tcpSocket\\\":{\\\"port\\\":\\\"tcp-health-port\\\"},\\\"initialDelaySeconds\\\":10,\\\"timeoutSeconds\\\":1,\\\"periodSeconds\\\":10,\\\"successThreshold\\\":1,\\\"failureThreshold\\\":10},\\\"readinessProbe\\\":{\\\"httpGet\\\":{\\\"path\\\":\\\"/\\\",\\\"port\\\":8080,\\\"scheme\\\":\\\"HTTP\\\"},\\\"initialDelaySeconds\\\":1,\\\"timeoutSeconds\\\":1,\\\"periodSeconds\\\":2,\\\"successThreshold\\\":1,\\\"failureThreshold\\\":10},\\\"startupProbe\\\":{\\\"tcpSocket\\\":{\\\"port\\\":\\\"tcp-health-port\\\"},\\\"timeoutSeconds\\\":1,\\\"periodSeconds\\\":10,\\\"successThreshold\\\":1,\\\"failureThreshold\\\":10},\\\"terminationMessagePath\\\":\\\"/dev/termination-log\\\",\\\"terminationMessagePolicy\\\":\\\"File\\\",\\\"imagePullPolicy\\\":\\\"Always\\\",\\\"securityContext\\\":{\\\"runAsUser\\\":1338,\\\"runAsGroup\\\":1338}},{\\\"name\\\":\\\"dubbo-demo-consumer\\\",\\\"image\\\":\\\"dockeddocking/dubbo:consumer.v1.0\\\",\\\"command\\\":[\\\"sh\\\",\\\"-c\\\",\\\"java $JAVA_OPTS -jar dubbo-demo-consumer.jar \\\"],\\\"resources\\\":{},\\\"volumeMounts\\\":[{\\\"name\\\":\\\"kube-api-access-2tknx\\\",\\\"readOnly\\\":true,\\\"mountPath\\\":\\\"/var/run/secrets/kubernetes.io/serviceaccount\\\"}],\\\"terminationMessagePath\\\":\\\"/dev/termination-log\\\",\\\"terminationMessagePolicy\\\":\\\"File\\\",\\\"imagePullPolicy\\\":\\\"Always\\\"}]}\",\n" + + " \"sidecar.istio.io/rewriteAppHTTPProbers\": \"false\",\n" + + " \"sidecar.istio.io/status\": \"{\\\"initContainers\\\":null,\\\"containers\\\":[\\\"app\\\",\\\"dubbo-demo-consumer\\\",\\\"istio-proxy\\\"],\\\"volumes\\\":[\\\"workload-socket\\\",\\\"workload-certs\\\",\\\"istio-xds\\\",\\\"istio-data\\\",\\\"istio-podinfo\\\",\\\"istio-token\\\",\\\"istiod-ca-cert\\\"],\\\"imagePullSecrets\\\":null,\\\"revision\\\":\\\"default\\\"}\"\n" + + " },\n" + + " \"APP_CONTAINERS\": \"app,dubbo-demo-consumer\",\n" + + " \"CLUSTER_ID\": \"Kubernetes\",\n" + + " \"ENVOY_PROMETHEUS_PORT\": 15090,\n" + + " \"ENVOY_STATUS_PORT\": 15021,\n" + + " \"GENERATOR\": \"grpc\",\n" + + " \"INSTANCE_IPS\": \"172.17.0.4\",\n" + + " \"INTERCEPTION_MODE\": \"REDIRECT\",\n" + + " \"ISTIO_PROXY_SHA\": \"2b6009118109b480e1d5abf3188fd7d9c0c0acf0\",\n" + + " \"ISTIO_VERSION\": \"1.14.1\",\n" + + " \"LABELS\": {\n" + + " \"app\": \"dubbo-demo-consumer-dev\",\n" + + " \"pod-template-hash\": \"58585cb9cd\",\n" + + " \"service.istio.io/canonical-name\": \"dubbo-demo-consumer-dev\",\n" + + " \"service.istio.io/canonical-revision\": \"v1\",\n" + + " \"version\": \"v1\"\n" + + " },\n" + + " \"MESH_ID\": \"cluster.local\",\n" + + " \"NAME\": \"dubbo-demo-consumer-deployment-grpc-agent-58585cb9cd-gp79p\",\n" + + " \"NAMESPACE\": \"dubbo-demo\",\n" + + " \"OWNER\": \"kubernetes://apis/apps/v1/namespaces/dubbo-demo/deployments/dubbo-demo-consumer-deployment-grpc-agent\",\n" + + " \"PILOT_SAN\": [\n" + + " \"istiod.istio-system.svc\"\n" + + " ],\n" + + " \"POD_PORTS\": \"[{\\\"containerPort\\\":17070,\\\"protocol\\\":\\\"TCP\\\"},{\\\"containerPort\\\":17171,\\\"protocol\\\":\\\"TCP\\\"},{\\\"containerPort\\\":8080,\\\"protocol\\\":\\\"TCP\\\"},{\\\"name\\\":\\\"tcp-health-port\\\",\\\"containerPort\\\":3333,\\\"protocol\\\":\\\"TCP\\\"}]\",\n" + + " \"PROV_CERT\": \"var/run/secrets/istio/root-cert.pem\",\n" + + " \"PROXY_CONFIG\": {\n" + + " \"binaryPath\": \"/usr/local/bin/envoy\",\n" + + " \"concurrency\": 2,\n" + + " \"configPath\": \"./etc/istio/proxy\",\n" + + " \"controlPlaneAuthPolicy\": \"MUTUAL_TLS\",\n" + + " \"discoveryAddress\": \"istiod.istio-system.svc:15012\",\n" + + " \"drainDuration\": \"45s\",\n" + + " \"holdApplicationUntilProxyStarts\": true,\n" + + " \"parentShutdownDuration\": \"60s\",\n" + + " \"proxyAdminPort\": 15000,\n" + + " \"serviceCluster\": \"istio-proxy\",\n" + + " \"statNameLength\": 189,\n" + + " \"statusPort\": 15020,\n" + + " \"terminationDrainDuration\": \"5s\",\n" + + " \"tracing\": {\n" + + " \"zipkin\": {\n" + + " \"address\": \"zipkin.istio-system:9411\"\n" + + " }\n" + + " }\n" + + " },\n" + + " \"SERVICE_ACCOUNT\": \"default\",\n" + + " \"WORKLOAD_NAME\": \"dubbo-demo-consumer-deployment-grpc-agent\"\n" + + " },\n" + + " \"locality\": {},\n" + + " \"UserAgentVersionType\": null\n" + + " },\n" + + " \"certificate_providers\": {\n" + + " \"default\": {\n" + + " \"plugin_name\": \"file_watcher\",\n" + + " \"config\": {\n" + + " \"certificate_file\": \"/var/lib/istio/data/cert-chain.pem\",\n" + + " \"private_key_file\": \"/var/lib/istio/data/key.pem\",\n" + + " \"ca_certificate_file\": \"/var/lib/istio/data/root-cert.pem\",\n" + + " \"refresh_interval\": \"900s\"\n" + + " }\n" + + " }\n" + + " },\n" + + " \"server_listener_resource_name_template\": \"xds.istio.io/grpc/lds/inbound/%s\"\n" + + "}"; + BootstrapperImpl.bootstrapPathFromEnvVar = ""; + BootstrapperImpl bootstrapper = new BootstrapperImpl(); + bootstrapper.setFileReader(createFileReader(rawData)); + Bootstrapper.BootstrapInfo info = bootstrapper.bootstrap(); + List serverInfoList = info.servers(); + Assertions.assertEquals(serverInfoList.get(0).target(), "unix:///etc/istio/proxy/XDS"); + URLAddress address = URLAddress.parse(serverInfoList.get(0).target(), null, false); + Assertions.assertEquals(new DomainSocketAddress(address.getPath()).path(), "etc/istio/proxy/XDS"); + } + + @Test + void testUrl() { + URL url = URL.valueOf("dubbo://127.0.0.1:23456/TestService?useAgent=true"); + Assertions.assertTrue(url.getParameter("useAgent", false)); + } + + private static BootstrapperImpl.FileReader createFileReader(final String rawData) { + return new BootstrapperImpl.FileReader() { + @Override + public String readFile(String path) { + return rawData; + } + }; + } +} diff --git a/dubbo-xds/src/test/java/org/apache/dubbo/registry/xds/util/protocol/impl/EdsProtocolMock.java b/dubbo-xds/src/test/java/org/apache/dubbo/registry/xds/util/protocol/impl/EdsProtocolMock.java new file mode 100644 index 000000000..f6ac1f0f6 --- /dev/null +++ b/dubbo-xds/src/test/java/org/apache/dubbo/registry/xds/util/protocol/impl/EdsProtocolMock.java @@ -0,0 +1,51 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.dubbo.registry.xds.util.protocol.impl; + +import org.apache.dubbo.registry.xds.util.AdsObserver; +import org.apache.dubbo.registry.xds.util.protocol.message.EndpointResult; + +import io.envoyproxy.envoy.config.core.v3.Node; + +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.Consumer; + +public class EdsProtocolMock extends EdsProtocol { + + public EdsProtocolMock(AdsObserver adsObserver, Node node, int checkInterval) { + super(adsObserver, node, checkInterval); + } + + public Map getResourcesMap() { + return resourcesMap; + } + + public void setResourcesMap(Map resourcesMap) { + this.resourcesMap = resourcesMap; + } + + public void setConsumerObserveMap( + Map, List>>> consumerObserveMap) { + this.consumerObserveMap = consumerObserveMap; + } + + public void setObserveResourcesName(Set observeResourcesName) { + this.observeResourcesName = observeResourcesName; + } +} diff --git a/dubbo-xds/src/test/java/org/apache/dubbo/registry/xds/util/protocol/impl/LdsProtocolMock.java b/dubbo-xds/src/test/java/org/apache/dubbo/registry/xds/util/protocol/impl/LdsProtocolMock.java new file mode 100644 index 000000000..0618ffb6c --- /dev/null +++ b/dubbo-xds/src/test/java/org/apache/dubbo/registry/xds/util/protocol/impl/LdsProtocolMock.java @@ -0,0 +1,68 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.dubbo.registry.xds.util.protocol.impl; + +import org.apache.dubbo.registry.xds.util.AdsObserver; +import org.apache.dubbo.registry.xds.util.protocol.message.ListenerResult; + +import io.envoyproxy.envoy.config.core.v3.Node; +import io.envoyproxy.envoy.service.discovery.v3.DiscoveryRequest; + +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.Consumer; + +public class LdsProtocolMock extends LdsProtocol { + + public LdsProtocolMock(AdsObserver adsObserver, Node node, int checkInterval) { + super(adsObserver, node, checkInterval); + } + + public Map getResourcesMap() { + return resourcesMap; + } + + public void setResourcesMap(Map resourcesMap) { + this.resourcesMap = resourcesMap; + } + + protected DiscoveryRequest buildDiscoveryRequest(Set resourceNames) { + return DiscoveryRequest.newBuilder() + .setNode(node) + .setTypeUrl(getTypeUrl()) + .addAllResourceNames(resourceNames) + .build(); + } + + public Set getObserveResourcesName() { + return observeResourcesName; + } + + public void setObserveResourcesName(Set observeResourcesName) { + this.observeResourcesName = observeResourcesName; + } + + public Map, List>>> getConsumerObserveMap() { + return consumerObserveMap; + } + + public void setConsumerObserveMap( + Map, List>>> consumerObserveMap) { + this.consumerObserveMap = consumerObserveMap; + } +} diff --git a/dubbo-xds/src/test/java/org/apache/dubbo/registry/xds/util/protocol/impl/RdsProtocolMock.java b/dubbo-xds/src/test/java/org/apache/dubbo/registry/xds/util/protocol/impl/RdsProtocolMock.java new file mode 100644 index 000000000..544fc89cd --- /dev/null +++ b/dubbo-xds/src/test/java/org/apache/dubbo/registry/xds/util/protocol/impl/RdsProtocolMock.java @@ -0,0 +1,54 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.dubbo.registry.xds.util.protocol.impl; + +import org.apache.dubbo.registry.xds.util.AdsObserver; +import org.apache.dubbo.registry.xds.util.protocol.message.RouteResult; + +import io.envoyproxy.envoy.config.core.v3.Node; + +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.Consumer; + +public class RdsProtocolMock extends RdsProtocol { + + public RdsProtocolMock(AdsObserver adsObserver, Node node, int checkInterval) { + super(adsObserver, node, checkInterval); + } + + public Map getResourcesMap() { + return resourcesMap; + } + + public void setResourcesMap(Map resourcesMap) { + this.resourcesMap = resourcesMap; + } + + public Set getObserveResourcesName() { + return observeResourcesName; + } + + public void setConsumerObserveMap(Map, List>>> consumerObserveMap) { + this.consumerObserveMap = consumerObserveMap; + } + + public void setObserveResourcesName(Set observeResourcesName) { + this.observeResourcesName = observeResourcesName; + } +} diff --git a/dubbo-xds/src/test/java/org/apache/dubbo/rpc/cluster/router/xds/EdsEndpointManagerTest.java b/dubbo-xds/src/test/java/org/apache/dubbo/rpc/cluster/router/xds/EdsEndpointManagerTest.java new file mode 100644 index 000000000..b92fc914d --- /dev/null +++ b/dubbo-xds/src/test/java/org/apache/dubbo/rpc/cluster/router/xds/EdsEndpointManagerTest.java @@ -0,0 +1,108 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.dubbo.rpc.cluster.router.xds; + +import org.apache.dubbo.registry.xds.util.protocol.message.Endpoint; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; + +public class EdsEndpointManagerTest { + + @BeforeEach + public void before() { + EdsEndpointManager.getEdsListeners().clear(); + EdsEndpointManager.getEndpointListeners().clear(); + EdsEndpointManager.getEndpointDataCache().clear(); + } + + @Test + public void subscribeEdsTest() { + EdsEndpointManager manager = new EdsEndpointManager(); + String cluster = "testApp"; + int subscribeNum = 3; + for (int i = 0; i < subscribeNum; i++) { + manager.subscribeEds(cluster, new EdsEndpointListener() { + @Override + public void onEndPointChange(String cluster, Set endpoints) {} + }); + } + assertNotNull(EdsEndpointManager.getEdsListeners().get(cluster)); + assertEquals(EdsEndpointManager.getEndpointListeners().get(cluster).size(), subscribeNum); + } + + @Test + public void unsubscribeRdsTest() { + EdsEndpointManager manager = new EdsEndpointManager(); + String domain = "testApp"; + EdsEndpointListener listener = new EdsEndpointListener() { + @Override + public void onEndPointChange(String cluster, Set endpoints) {} + }; + manager.subscribeEds(domain, listener); + assertNotNull(EdsEndpointManager.getEdsListeners().get(domain)); + assertEquals(EdsEndpointManager.getEndpointListeners().get(domain).size(), 1); + + manager.unSubscribeEds(domain, listener); + assertNull(EdsEndpointManager.getEdsListeners().get(domain)); + assertNull(EdsEndpointManager.getEndpointListeners().get(domain)); + } + + @Test + public void notifyRuleChangeTest() { + + Map> cacheData = new HashMap<>(); + String domain = "testApp"; + Set endpoints = new HashSet<>(); + Endpoint endpoint = new Endpoint(); + endpoints.add(endpoint); + + EdsEndpointListener listener = new EdsEndpointListener() { + @Override + public void onEndPointChange(String cluster, Set endpoints) { + cacheData.put(cluster, endpoints); + } + }; + + EdsEndpointManager manager = new EdsEndpointManager(); + manager.subscribeEds(domain, listener); + manager.notifyEndpointChange(domain, endpoints); + assertEquals(cacheData.get(domain), endpoints); + + Map> cacheData2 = new HashMap<>(); + EdsEndpointListener listener2 = new EdsEndpointListener() { + @Override + public void onEndPointChange(String cluster, Set endpoints) { + cacheData2.put(cluster, endpoints); + } + }; + manager.subscribeEds(domain, listener2); + assertEquals(cacheData2.get(domain), endpoints); + // clear + manager.notifyEndpointChange(domain, new HashSet<>()); + assertEquals(cacheData.get(domain).size(), 0); + } +} diff --git a/dubbo-xds/src/test/java/org/apache/dubbo/rpc/cluster/router/xds/RdsRouteRuleManagerTest.java b/dubbo-xds/src/test/java/org/apache/dubbo/rpc/cluster/router/xds/RdsRouteRuleManagerTest.java new file mode 100644 index 000000000..81ad9f3f3 --- /dev/null +++ b/dubbo-xds/src/test/java/org/apache/dubbo/rpc/cluster/router/xds/RdsRouteRuleManagerTest.java @@ -0,0 +1,126 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.dubbo.rpc.cluster.router.xds; + +import org.apache.dubbo.rpc.cluster.router.xds.rule.HTTPRouteDestination; +import org.apache.dubbo.rpc.cluster.router.xds.rule.HttpRequestMatch; +import org.apache.dubbo.rpc.cluster.router.xds.rule.XdsRouteRule; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; + +public class RdsRouteRuleManagerTest { + + @BeforeEach + public void before() { + RdsRouteRuleManager.getRuleListeners().clear(); + RdsRouteRuleManager.getRouteDataCache().clear(); + RdsRouteRuleManager.getRdsListeners().clear(); + } + + @Test + public void subscribeRdsTest() { + RdsRouteRuleManager manager = new RdsRouteRuleManager(); + String domain = "testApp"; + int subscribeNum = 3; + for (int i = 0; i < subscribeNum; i++) { + manager.subscribeRds(domain, new XdsRouteRuleListener() { + @Override + public void onRuleChange(String appName, List xdsRouteRules) {} + + @Override + public void clearRule(String appName) {} + }); + } + assertNotNull(RdsRouteRuleManager.getRdsListeners().get(domain)); + assertEquals(RdsRouteRuleManager.getRuleListeners().get(domain).size(), subscribeNum); + } + + @Test + public void unsubscribeRdsTest() { + RdsRouteRuleManager manager = new RdsRouteRuleManager(); + String domain = "testApp"; + XdsRouteRuleListener listener = new XdsRouteRuleListener() { + @Override + public void onRuleChange(String appName, List xdsRouteRules) {} + + @Override + public void clearRule(String appName) {} + }; + manager.subscribeRds(domain, listener); + assertNotNull(RdsRouteRuleManager.getRdsListeners().get(domain)); + assertEquals(RdsRouteRuleManager.getRuleListeners().get(domain).size(), 1); + + manager.unSubscribeRds(domain, listener); + assertNull(RdsRouteRuleManager.getRdsListeners().get(domain)); + assertNull(RdsRouteRuleManager.getRuleListeners().get(domain)); + } + + @Test + public void notifyRuleChangeTest() { + + Map> cacheData = new HashMap<>(); + String domain = "testApp"; + List xdsRouteRules = new ArrayList<>(); + XdsRouteRule rule = new XdsRouteRule(new HttpRequestMatch(null, null), new HTTPRouteDestination()); + xdsRouteRules.add(rule); + + XdsRouteRuleListener listener = new XdsRouteRuleListener() { + @Override + public void onRuleChange(String appName, List xdsRouteRules) { + cacheData.put(appName, xdsRouteRules); + } + + @Override + public void clearRule(String appName) { + cacheData.remove(appName); + } + }; + + RdsRouteRuleManager manager = new RdsRouteRuleManager(); + manager.subscribeRds(domain, listener); + manager.notifyRuleChange(domain, xdsRouteRules); + assertEquals(cacheData.get(domain), xdsRouteRules); + + Map> cacheData2 = new HashMap<>(); + XdsRouteRuleListener listener2 = new XdsRouteRuleListener() { + @Override + public void onRuleChange(String appName, List xdsRouteRules) { + cacheData2.put(appName, xdsRouteRules); + } + + @Override + public void clearRule(String appName) { + cacheData2.remove(appName); + } + }; + manager.subscribeRds(domain, listener2); + assertEquals(cacheData2.get(domain), xdsRouteRules); + // clear + manager.notifyRuleChange(domain, new ArrayList<>()); + assertNull(cacheData.get(domain)); + } +} diff --git a/dubbo-xds/src/test/java/org/apache/dubbo/rpc/cluster/router/xds/RdsVirtualHostListenerTest.java b/dubbo-xds/src/test/java/org/apache/dubbo/rpc/cluster/router/xds/RdsVirtualHostListenerTest.java new file mode 100644 index 000000000..e23c8b68c --- /dev/null +++ b/dubbo-xds/src/test/java/org/apache/dubbo/rpc/cluster/router/xds/RdsVirtualHostListenerTest.java @@ -0,0 +1,258 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.dubbo.rpc.cluster.router.xds; + +import org.apache.dubbo.rpc.cluster.router.xds.rule.ClusterWeight; +import org.apache.dubbo.rpc.cluster.router.xds.rule.XdsRouteRule; + +import com.google.protobuf.BoolValue; +import com.google.protobuf.UInt32Value; +import io.envoyproxy.envoy.config.route.v3.HeaderMatcher; +import io.envoyproxy.envoy.config.route.v3.Route; +import io.envoyproxy.envoy.config.route.v3.RouteAction; +import io.envoyproxy.envoy.config.route.v3.RouteMatch; +import io.envoyproxy.envoy.config.route.v3.VirtualHost; +import io.envoyproxy.envoy.config.route.v3.WeightedCluster; +import io.envoyproxy.envoy.type.matcher.v3.RegexMatcher; +import io.envoyproxy.envoy.type.v3.Int64Range; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class RdsVirtualHostListenerTest { + + private final String domain = "testApp"; + + private final Map> dataCache = new HashMap<>(); + + private final XdsRouteRuleListener listener = new XdsRouteRuleListener() { + @Override + public void onRuleChange(String appName, List xdsRouteRules) { + dataCache.put(appName, xdsRouteRules); + } + + @Override + public void clearRule(String appName) { + dataCache.remove(appName); + } + }; + + private final RdsRouteRuleManager manager = new RdsRouteRuleManager(); + + private final RdsVirtualHostListener rdsVirtualHostListener = new RdsVirtualHostListener("testApp", manager); + + @BeforeEach + public void init() { + dataCache.clear(); + manager.subscribeRds(domain, listener); + } + + @Test + public void parsePathPathMatcherTest() { + String path = "/test/name"; + VirtualHost virtualHost = VirtualHost.newBuilder() + .addDomains(domain) + .addRoutes(Route.newBuilder() + .setName("route-test") + .setMatch(RouteMatch.newBuilder() + .setPath(path) + .setCaseSensitive( + BoolValue.newBuilder().setValue(true).build()) + .build()) + .setRoute(RouteAction.newBuilder() + .setCluster("cluster-test") + .build()) + .build()) + .build(); + rdsVirtualHostListener.parseVirtualHost(virtualHost); + List rules = dataCache.get(domain); + assertNotNull(rules); + assertEquals(rules.get(0).getMatch().getPathMatcher().getPath(), path); + assertTrue(rules.get(0).getMatch().getPathMatcher().isCaseSensitive()); + } + + @Test + public void parsePrefixPathMatcherTest() { + String prefix = "/test"; + VirtualHost virtualHost = VirtualHost.newBuilder() + .addDomains(domain) + .addRoutes(Route.newBuilder() + .setName("route-test") + .setMatch(RouteMatch.newBuilder().setPrefix(prefix).build()) + .setRoute(RouteAction.newBuilder() + .setCluster("cluster-test") + .build()) + .build()) + .build(); + rdsVirtualHostListener.parseVirtualHost(virtualHost); + List rules = dataCache.get(domain); + assertNotNull(rules); + assertEquals(rules.get(0).getMatch().getPathMatcher().getPrefix(), prefix); + } + + @Test + public void parseRegexPathMatcherTest() { + String regex = "/test/.*"; + VirtualHost virtualHost = VirtualHost.newBuilder() + .addDomains(domain) + .addRoutes(Route.newBuilder() + .setName("route-test") + .setMatch(RouteMatch.newBuilder() + .setSafeRegex(RegexMatcher.newBuilder() + .setRegex(regex) + .build()) + .build()) + .setRoute(RouteAction.newBuilder() + .setCluster("cluster-test") + .build()) + .build()) + .build(); + rdsVirtualHostListener.parseVirtualHost(virtualHost); + List rules = dataCache.get(domain); + assertNotNull(rules); + assertEquals(rules.get(0).getMatch().getPathMatcher().getRegex(), regex); + } + + @Test + public void parseHeadMatcherTest() { + VirtualHost virtualHost = VirtualHost.newBuilder() + .addDomains(domain) + .addRoutes(Route.newBuilder() + .setName("route-test") + .setMatch(RouteMatch.newBuilder() + .addHeaders(HeaderMatcher.newBuilder() + .setName("head-exactValue") + .setExactMatch("exactValue") + .setInvertMatch(true) + .build()) + .addHeaders(HeaderMatcher.newBuilder() + .setName("head-regex") + .setSafeRegexMatch(RegexMatcher.newBuilder() + .setRegex("regex") + .build()) + .build()) + .addHeaders(HeaderMatcher.newBuilder() + .setName("head-range") + .setRangeMatch(Int64Range.newBuilder() + .setStart(1) + .setEnd(100) + .build()) + .build()) + .addHeaders(HeaderMatcher.newBuilder() + .setName("head-present") + .setPresentMatch(true) + .build()) + .addHeaders(HeaderMatcher.newBuilder() + .setName("head-prefix") + .setPrefixMatch("prefix") + .build()) + .addHeaders(HeaderMatcher.newBuilder() + .setName("head-suffix") + .setSuffixMatch("suffix") + .build()) + .build()) + .setRoute(RouteAction.newBuilder() + .setCluster("cluster-test") + .build()) + .build()) + .build(); + rdsVirtualHostListener.parseVirtualHost(virtualHost); + List rules = dataCache.get(domain); + assertNotNull(rules); + List headerMatcherList = + rules.get(0).getMatch().getHeaderMatcherList(); + for (org.apache.dubbo.rpc.cluster.router.xds.rule.HeaderMatcher matcher : headerMatcherList) { + if (matcher.getName().equals("head-exactValue")) { + assertEquals(matcher.getExactValue(), "exactValue"); + } else if (matcher.getName().equals("head-regex")) { + assertEquals(matcher.getRegex(), "regex"); + } else if (matcher.getName().equals("head-range")) { + assertEquals(matcher.getRange().getStart(), 1); + assertEquals(matcher.getRange().getEnd(), 100); + } else if (matcher.getName().equals("head-present")) { + assertTrue(matcher.getPresent()); + } else if (matcher.getName().equals("head-prefix")) { + assertEquals(matcher.getPrefix(), "prefix"); + } else if (matcher.getName().equals("head-suffix")) { + assertEquals(matcher.getSuffix(), "suffix"); + } + } + } + + @Test + public void parseRouteClusterTest() { + String cluster = "cluster-test"; + VirtualHost virtualHost = VirtualHost.newBuilder() + .addDomains(domain) + .addRoutes(Route.newBuilder() + .setName("route-test") + .setMatch(RouteMatch.newBuilder().setPrefix("/test").build()) + .setRoute(RouteAction.newBuilder().setCluster(cluster).build()) + .build()) + .build(); + rdsVirtualHostListener.parseVirtualHost(virtualHost); + List rules = dataCache.get(domain); + assertNotNull(rules); + assertEquals(rules.get(0).getRoute().getCluster(), cluster); + } + + @Test + public void parseRouteWeightClusterTest() { + VirtualHost virtualHost = VirtualHost.newBuilder() + .addDomains(domain) + .addRoutes(Route.newBuilder() + .setName("route-test") + .setMatch(RouteMatch.newBuilder().setPrefix("/test").build()) + .setRoute(RouteAction.newBuilder() + .setWeightedClusters(WeightedCluster.newBuilder() + .addClusters(WeightedCluster.ClusterWeight.newBuilder() + .setName("cluster-test1") + .setWeight(UInt32Value.newBuilder() + .setValue(40) + .build()) + .build()) + .addClusters(WeightedCluster.ClusterWeight.newBuilder() + .setName("cluster-test2") + .setWeight(UInt32Value.newBuilder() + .setValue(60) + .build()) + .build()) + .build()) + .build()) + .build()) + .build(); + rdsVirtualHostListener.parseVirtualHost(virtualHost); + List rules = dataCache.get(domain); + assertNotNull(rules); + List weightedClusters = rules.get(0).getRoute().getWeightedClusters(); + assertEquals(weightedClusters.size(), 2); + for (ClusterWeight weightedCluster : weightedClusters) { + if (weightedCluster.getName().equals("cluster-test1")) { + assertEquals(weightedCluster.getWeight(), 40); + } else if (weightedCluster.getName().equals("cluster-test2")) { + assertEquals(weightedCluster.getWeight(), 60); + } + } + } +} diff --git a/dubbo-xds/src/test/java/org/apache/dubbo/rpc/cluster/router/xds/XdsRouteTest.java b/dubbo-xds/src/test/java/org/apache/dubbo/rpc/cluster/router/xds/XdsRouteTest.java new file mode 100644 index 000000000..c870c687e --- /dev/null +++ b/dubbo-xds/src/test/java/org/apache/dubbo/rpc/cluster/router/xds/XdsRouteTest.java @@ -0,0 +1,376 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.dubbo.rpc.cluster.router.xds; + +import org.apache.dubbo.common.URL; +import org.apache.dubbo.common.utils.Holder; +import org.apache.dubbo.common.utils.StringUtils; +import org.apache.dubbo.registry.xds.util.protocol.message.Endpoint; +import org.apache.dubbo.rpc.Invocation; +import org.apache.dubbo.rpc.Invoker; +import org.apache.dubbo.rpc.cluster.router.mesh.util.TracingContextProvider; +import org.apache.dubbo.rpc.cluster.router.state.BitList; +import org.apache.dubbo.rpc.cluster.router.xds.rule.DestinationSubset; + +import com.google.protobuf.UInt32Value; +import io.envoyproxy.envoy.config.route.v3.HeaderMatcher; +import io.envoyproxy.envoy.config.route.v3.Route; +import io.envoyproxy.envoy.config.route.v3.RouteAction; +import io.envoyproxy.envoy.config.route.v3.RouteMatch; +import io.envoyproxy.envoy.config.route.v3.VirtualHost; +import io.envoyproxy.envoy.config.route.v3.WeightedCluster; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +public class XdsRouteTest { + + private EdsEndpointManager edsEndpointManager; + + private RdsRouteRuleManager rdsRouteRuleManager; + private Set tracingContextProviders; + private URL url; + + @BeforeEach + public void setup() { + edsEndpointManager = Mockito.spy(EdsEndpointManager.class); + rdsRouteRuleManager = Mockito.spy(RdsRouteRuleManager.class); + tracingContextProviders = new HashSet<>(); + + url = URL.valueOf("test://localhost/DemoInterface"); + } + + private Invoker createInvoker(String app) { + URL url = URL.valueOf( + "dubbo://localhost/DemoInterface?" + (StringUtils.isEmpty(app) ? "" : "remote.application=" + app)); + Invoker invoker = Mockito.mock(Invoker.class); + when(invoker.getUrl()).thenReturn(url); + return invoker; + } + + private Invoker createInvoker(String app, String address) { + URL url = URL.valueOf("dubbo://" + address + "/DemoInterface?" + + (StringUtils.isEmpty(app) ? "" : "remote.application=" + app)); + Invoker invoker = Mockito.mock(Invoker.class); + when(invoker.getUrl()).thenReturn(url); + return invoker; + } + + @Test + public void testNotifyInvoker() { + XdsRouter xdsRouter = new XdsRouter<>(url, rdsRouteRuleManager, edsEndpointManager, true); + xdsRouter.notify(null); + assertEquals(0, xdsRouter.getSubscribeApplications().size()); + + BitList> invokers = new BitList<>(Arrays.asList(createInvoker(""), createInvoker("app1"))); + + xdsRouter.notify(invokers); + + assertEquals(1, xdsRouter.getSubscribeApplications().size()); + assertTrue(xdsRouter.getSubscribeApplications().contains("app1")); + assertEquals(invokers, xdsRouter.getInvokerList()); + + verify(rdsRouteRuleManager, times(1)).subscribeRds("app1", xdsRouter); + + invokers = new BitList<>(Arrays.asList(createInvoker("app2"))); + xdsRouter.notify(invokers); + verify(rdsRouteRuleManager, times(1)).subscribeRds("app2", xdsRouter); + verify(rdsRouteRuleManager, times(1)).unSubscribeRds("app1", xdsRouter); + assertEquals(invokers, xdsRouter.getInvokerList()); + + xdsRouter.stop(); + verify(rdsRouteRuleManager, times(1)).unSubscribeRds("app2", xdsRouter); + } + + @Test + public void testRuleChange() { + XdsRouter xdsRouter = new XdsRouter<>(url, rdsRouteRuleManager, edsEndpointManager, true); + String appName = "app1"; + String cluster1 = "cluster-test1"; + String cluster2 = "cluster-test2"; + BitList> invokers = new BitList<>(Arrays.asList(createInvoker(appName))); + xdsRouter.notify(invokers); + String path = "/DemoInterface/call"; + VirtualHost virtualHost = VirtualHost.newBuilder() + .addDomains(appName) + .addRoutes(Route.newBuilder() + .setName("route-test") + .setMatch(RouteMatch.newBuilder().setPath(path).build()) + .setRoute(RouteAction.newBuilder().setCluster(cluster1).build()) + .build()) + .build(); + RdsVirtualHostListener hostListener = new RdsVirtualHostListener(appName, rdsRouteRuleManager); + hostListener.parseVirtualHost(virtualHost); + assertEquals(xdsRouter.getXdsRouteRuleMap().get(appName).size(), 1); + verify(edsEndpointManager, times(1)).subscribeEds(cluster1, xdsRouter); + + VirtualHost virtualHost2 = VirtualHost.newBuilder() + .addDomains(appName) + .addRoutes(Route.newBuilder() + .setName("route-test") + .setMatch(RouteMatch.newBuilder().setPath(path).build()) + .setRoute(RouteAction.newBuilder() + .setCluster("cluster-test2") + .build()) + .build()) + .build(); + hostListener.parseVirtualHost(virtualHost2); + assertEquals(xdsRouter.getXdsRouteRuleMap().get(appName).size(), 1); + verify(edsEndpointManager, times(1)).subscribeEds(cluster2, xdsRouter); + verify(edsEndpointManager, times(1)).unSubscribeEds(cluster1, xdsRouter); + } + + @Test + public void testEndpointChange() { + XdsRouter xdsRouter = new XdsRouter<>(url, rdsRouteRuleManager, edsEndpointManager, true); + String appName = "app1"; + String cluster1 = "cluster-test1"; + BitList> invokers = new BitList<>( + Arrays.asList(createInvoker(appName, "1.1.1.1:20880"), createInvoker(appName, "2.2.2.2:20880"))); + xdsRouter.notify(invokers); + String path = "/DemoInterface/call"; + VirtualHost virtualHost = VirtualHost.newBuilder() + .addDomains(appName) + .addRoutes(Route.newBuilder() + .setName("route-test") + .setMatch(RouteMatch.newBuilder().setPath(path).build()) + .setRoute(RouteAction.newBuilder().setCluster(cluster1).build()) + .build()) + .build(); + RdsVirtualHostListener hostListener = new RdsVirtualHostListener(appName, rdsRouteRuleManager); + hostListener.parseVirtualHost(virtualHost); + assertEquals(xdsRouter.getXdsRouteRuleMap().get(appName).size(), 1); + verify(edsEndpointManager, times(1)).subscribeEds(cluster1, xdsRouter); + + Set endpoints = new HashSet<>(); + Endpoint endpoint1 = new Endpoint(); + endpoint1.setAddress("1.1.1.1"); + endpoint1.setPortValue(20880); + Endpoint endpoint2 = new Endpoint(); + endpoint2.setAddress("2.2.2.2"); + endpoint2.setPortValue(20880); + endpoints.add(endpoint1); + endpoints.add(endpoint2); + edsEndpointManager.notifyEndpointChange(cluster1, endpoints); + + DestinationSubset objectDestinationSubset = + xdsRouter.getDestinationSubsetMap().get(cluster1); + assertEquals(invokers, objectDestinationSubset.getInvokers()); + } + + @Test + public void testRouteNotMatch() { + XdsRouter xdsRouter = new XdsRouter<>(url, rdsRouteRuleManager, edsEndpointManager, true); + String appName = "app1"; + BitList> invokers = new BitList<>( + Arrays.asList(createInvoker(appName, "1.1.1.1:20880"), createInvoker(appName, "2.2.2.2:20880"))); + assertEquals(invokers, xdsRouter.route(invokers.clone(), null, null, false, null)); + Holder message = new Holder<>(); + xdsRouter.doRoute(invokers.clone(), null, null, true, null, message); + assertEquals("Directly Return. Reason: xds route rule is empty.", message.get()); + } + + @Test + public void testRoutePathMatch() { + XdsRouter xdsRouter = new XdsRouter<>(url, rdsRouteRuleManager, edsEndpointManager, true); + String appName = "app1"; + String cluster1 = "cluster-test1"; + Invoker invoker1 = createInvoker(appName, "1.1.1.1:20880"); + BitList> invokers = + new BitList<>(Arrays.asList(invoker1, createInvoker(appName, "2.2.2.2:20880"))); + xdsRouter.notify(invokers); + String path = "/DemoInterface/call"; + VirtualHost virtualHost = VirtualHost.newBuilder() + .addDomains(appName) + .addRoutes(Route.newBuilder() + .setName("route-test") + .setMatch(RouteMatch.newBuilder().setPath(path).build()) + .setRoute(RouteAction.newBuilder().setCluster(cluster1).build()) + .build()) + .build(); + RdsVirtualHostListener hostListener = new RdsVirtualHostListener(appName, rdsRouteRuleManager); + hostListener.parseVirtualHost(virtualHost); + Invocation invocation = Mockito.mock(Invocation.class); + Invoker invoker = Mockito.mock(Invoker.class); + URL url1 = Mockito.mock(URL.class); + when(invoker.getUrl()).thenReturn(url1); + when(url1.getPath()).thenReturn("DemoInterface"); + when(invocation.getInvoker()).thenReturn(invoker); + when(invocation.getMethodName()).thenReturn("call"); + + Set endpoints = new HashSet<>(); + Endpoint endpoint1 = new Endpoint(); + endpoint1.setAddress("1.1.1.1"); + endpoint1.setPortValue(20880); + endpoints.add(endpoint1); + edsEndpointManager.notifyEndpointChange(cluster1, endpoints); + BitList> routes = xdsRouter.route(invokers.clone(), null, invocation, false, null); + assertEquals(1, routes.size()); + assertEquals(invoker1, routes.get(0)); + } + + @Test + public void testRouteHeadMatch() { + XdsRouter xdsRouter = new XdsRouter<>(url, rdsRouteRuleManager, edsEndpointManager, true); + String appName = "app1"; + String cluster1 = "cluster-test1"; + Invoker invoker1 = createInvoker(appName, "1.1.1.1:20880"); + BitList> invokers = + new BitList<>(Arrays.asList(invoker1, createInvoker(appName, "2.2.2.2:20880"))); + xdsRouter.notify(invokers); + VirtualHost virtualHost = VirtualHost.newBuilder() + .addDomains(appName) + .addRoutes(Route.newBuilder() + .setName("route-test") + .setMatch(RouteMatch.newBuilder() + .addHeaders(HeaderMatcher.newBuilder() + .setName("userId") + .setExactMatch("123") + .build()) + .build()) + .setRoute(RouteAction.newBuilder().setCluster(cluster1).build()) + .build()) + .build(); + RdsVirtualHostListener hostListener = new RdsVirtualHostListener(appName, rdsRouteRuleManager); + hostListener.parseVirtualHost(virtualHost); + Invocation invocation = Mockito.mock(Invocation.class); + when(invocation.getAttachment("userId")).thenReturn("123"); + Set endpoints = new HashSet<>(); + Endpoint endpoint1 = new Endpoint(); + endpoint1.setAddress("1.1.1.1"); + endpoint1.setPortValue(20880); + endpoints.add(endpoint1); + edsEndpointManager.notifyEndpointChange(cluster1, endpoints); + BitList> routes = xdsRouter.route(invokers.clone(), null, invocation, false, null); + assertEquals(1, routes.size()); + assertEquals(invoker1, routes.get(0)); + } + + @Test + public void testRouteWeightCluster() { + XdsRouter xdsRouter = new XdsRouter<>(url, rdsRouteRuleManager, edsEndpointManager, true); + String appName = "app1"; + String cluster1 = "cluster-test1"; + String cluster2 = "cluster-test2"; + Invoker invoker1 = createInvoker(appName, "1.1.1.1:20880"); + BitList> invokers = + new BitList<>(Arrays.asList(invoker1, createInvoker(appName, "2.2.2.2:20880"))); + xdsRouter.notify(invokers); + VirtualHost virtualHost = VirtualHost.newBuilder() + .addDomains(appName) + .addRoutes(Route.newBuilder() + .setName("route-test") + .setMatch(RouteMatch.newBuilder() + .addHeaders(HeaderMatcher.newBuilder() + .setName("userId") + .setExactMatch("123") + .build()) + .build()) + .setRoute(RouteAction.newBuilder() + .setWeightedClusters(WeightedCluster.newBuilder() + .addClusters(WeightedCluster.ClusterWeight.newBuilder() + .setName(cluster1) + .setWeight(UInt32Value.newBuilder() + .setValue(100) + .build()) + .build()) + .addClusters(WeightedCluster.ClusterWeight.newBuilder() + .setName(cluster2) + .setWeight(UInt32Value.newBuilder() + .setValue(0) + .build()) + .build()) + .build()) + .build()) + .build()) + .build(); + RdsVirtualHostListener hostListener = new RdsVirtualHostListener(appName, rdsRouteRuleManager); + hostListener.parseVirtualHost(virtualHost); + Invocation invocation = Mockito.mock(Invocation.class); + when(invocation.getAttachment("userId")).thenReturn("123"); + Set endpoints = new HashSet<>(); + Endpoint endpoint1 = new Endpoint(); + endpoint1.setAddress("1.1.1.1"); + endpoint1.setPortValue(20880); + endpoints.add(endpoint1); + edsEndpointManager.notifyEndpointChange(cluster1, endpoints); + + endpoints = new HashSet<>(); + Endpoint endpoint2 = new Endpoint(); + endpoint2.setAddress("2.2.2.2"); + endpoint2.setPortValue(20880); + endpoints.add(endpoint2); + edsEndpointManager.notifyEndpointChange(cluster2, endpoints); + + for (int i = 0; i < 10; i++) { + BitList> routes = xdsRouter.route(invokers.clone(), null, invocation, false, null); + assertEquals(1, routes.size()); + assertEquals(invoker1, routes.get(0)); + } + } + + @Test + public void testRouteMultiApp() { + XdsRouter xdsRouter = new XdsRouter<>(url, rdsRouteRuleManager, edsEndpointManager, true); + String appName1 = "app1"; + String appName2 = "app2"; + String cluster1 = "cluster-test1"; + Invoker invoker1 = createInvoker(appName2, "1.1.1.1:20880"); + Invoker invoker2 = createInvoker(appName1, "2.2.2.2:20880"); + BitList> invokers = new BitList<>(Arrays.asList(invoker1, invoker2)); + xdsRouter.notify(invokers); + assertEquals(xdsRouter.getSubscribeApplications().size(), 2); + String path = "/DemoInterface/call"; + VirtualHost virtualHost = VirtualHost.newBuilder() + .addDomains(appName2) + .addRoutes(Route.newBuilder() + .setName("route-test") + .setMatch(RouteMatch.newBuilder().setPath(path).build()) + .setRoute(RouteAction.newBuilder().setCluster(cluster1).build()) + .build()) + .build(); + RdsVirtualHostListener hostListener = new RdsVirtualHostListener(appName2, rdsRouteRuleManager); + hostListener.parseVirtualHost(virtualHost); + Invocation invocation = Mockito.mock(Invocation.class); + Invoker invoker = Mockito.mock(Invoker.class); + URL url1 = Mockito.mock(URL.class); + when(invoker.getUrl()).thenReturn(url1); + when(url1.getPath()).thenReturn("DemoInterface"); + when(invocation.getInvoker()).thenReturn(invoker); + when(invocation.getMethodName()).thenReturn("call"); + + Set endpoints = new HashSet<>(); + Endpoint endpoint1 = new Endpoint(); + endpoint1.setAddress("1.1.1.1"); + endpoint1.setPortValue(20880); + endpoints.add(endpoint1); + edsEndpointManager.notifyEndpointChange(cluster1, endpoints); + BitList> routes = xdsRouter.route(invokers.clone(), null, invocation, false, null); + assertEquals(1, routes.size()); + assertEquals(invoker1, routes.get(0)); + } +} diff --git a/dubbo-xds/src/test/java/org/apache/dubbo/rpc/cluster/router/xds/rule/HeaderMatcherTest.java b/dubbo-xds/src/test/java/org/apache/dubbo/rpc/cluster/router/xds/rule/HeaderMatcherTest.java new file mode 100644 index 000000000..597c06e0d --- /dev/null +++ b/dubbo-xds/src/test/java/org/apache/dubbo/rpc/cluster/router/xds/rule/HeaderMatcherTest.java @@ -0,0 +1,89 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.dubbo.rpc.cluster.router.xds.rule; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class HeaderMatcherTest { + + @Test + public void exactValueMatcherTest() { + HeaderMatcher headMatcher = new HeaderMatcher(); + headMatcher.setName("testHead"); + String value = "testValue"; + headMatcher.setExactValue(value); + assertTrue(headMatcher.match(value)); + } + + @Test + public void regexMatcherTest() { + HeaderMatcher headMatcher = new HeaderMatcher(); + headMatcher.setRegex("test.*"); + String value = "testValue"; + headMatcher.setExactValue(value); + assertTrue(headMatcher.match(value)); + } + + @Test + public void rangMatcherTest() { + HeaderMatcher headMatcher = new HeaderMatcher(); + LongRangeMatch range = new LongRangeMatch(); + range.setStart(100); + range.setEnd(500); + headMatcher.setRange(range); + assertTrue(headMatcher.match("300")); + } + + @Test + public void presentMatcherTest() { + HeaderMatcher headMatcher = new HeaderMatcher(); + headMatcher.setName("testHead"); + headMatcher.setPresent(true); + assertTrue(headMatcher.match("value")); + headMatcher.setPresent(false); + assertTrue(headMatcher.match(null)); + } + + @Test + public void prefixMatcherTest() { + HeaderMatcher headMatcher = new HeaderMatcher(); + headMatcher.setName("testHead"); + headMatcher.setPrefix("test"); + assertTrue(headMatcher.match("testValue")); + } + + @Test + public void suffixMatcherTest() { + HeaderMatcher headMatcher = new HeaderMatcher(); + headMatcher.setName("testHead"); + headMatcher.setSuffix("Value"); + assertTrue(headMatcher.match("testValue")); + } + + @Test + public void invertedMatcherTest() { + HeaderMatcher headMatcher = new HeaderMatcher(); + headMatcher.setName("testHead"); + String value = "testValue"; + headMatcher.setExactValue(value); + headMatcher.setInverted(true); + assertFalse(headMatcher.match("testValue")); + } +} diff --git a/dubbo-xds/src/test/java/org/apache/dubbo/rpc/cluster/router/xds/rule/PathMatcherTest.java b/dubbo-xds/src/test/java/org/apache/dubbo/rpc/cluster/router/xds/rule/PathMatcherTest.java new file mode 100644 index 000000000..ce8e82b50 --- /dev/null +++ b/dubbo-xds/src/test/java/org/apache/dubbo/rpc/cluster/router/xds/rule/PathMatcherTest.java @@ -0,0 +1,57 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.dubbo.rpc.cluster.router.xds.rule; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class PathMatcherTest { + + @Test + public void pathMatcherTest() { + PathMatcher pathMatcher = new PathMatcher(); + String path = "/testService/test"; + pathMatcher.setPath(path); + assertTrue(pathMatcher.isMatch(path)); + assertTrue(pathMatcher.isMatch(path.toUpperCase())); + pathMatcher.setCaseSensitive(true); + assertFalse(pathMatcher.isMatch(path.toUpperCase())); + } + + @Test + public void prefixMatcherTest() { + PathMatcher pathMatcher = new PathMatcher(); + String prefix = "/test"; + String path = "/testService/test"; + pathMatcher.setPrefix(prefix); + assertTrue(pathMatcher.isMatch(path)); + assertTrue(pathMatcher.isMatch(path.toUpperCase())); + pathMatcher.setCaseSensitive(true); + assertFalse(pathMatcher.isMatch(path.toUpperCase())); + } + + @Test + public void regexMatcherTest() { + PathMatcher pathMatcher = new PathMatcher(); + String regex = "/testService/.*"; + String path = "/testService/test"; + pathMatcher.setRegex(regex); + assertTrue(pathMatcher.isMatch(path)); + } +} diff --git a/pom.xml b/pom.xml index a31ba28c1..522f1401a 100644 --- a/pom.xml +++ b/pom.xml @@ -98,12 +98,15 @@ dubbo-cross-thread-extensions dubbo-tag-extensions dobbo-doc-auto-gen + dubbo-xds + dubbo-kubernetes 1.0.5-SNAPSHOT 5.6.0 + 4.2.0 3.11.1 2.2 5.2.4.Final @@ -184,6 +187,12 @@ ${hamcrest_version} test + + org.awaitility + awaitility + ${awaitility_version} + test + org.mockito mockito-core @@ -561,9 +570,12 @@ true ${checkstyle.skip} - **/istio/v1/auth/Ca.java, - **/istio/v1/auth/IstioCertificateServiceGrpc.java, + **/istio/v1/auth/**/*, + **/com/google/rpc/*, **/generated/**/*, + **/generated-sources/**/*, + **/grpc/health/**/*, + **/grpc/reflection/**/*, **/target/**/*, diff --git a/test/dubbo-scenario-builder/pom.xml b/test/dubbo-scenario-builder/pom.xml index b0f981bcb..45b7855df 100644 --- a/test/dubbo-scenario-builder/pom.xml +++ b/test/dubbo-scenario-builder/pom.xml @@ -76,7 +76,7 @@ ch.qos.logback logback-classic - 1.2.3 + 1.3.12