You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@shenyu.apache.org by xi...@apache.org on 2023/05/08 10:15:22 UTC
[shenyu] branch master updated: [type: feature] init shenyu ingress controller (#4620)
This is an automated email from the ASF dual-hosted git repository.
xiaoyu pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/shenyu.git
The following commit(s) were added to refs/heads/master by this push:
new e7c3fae19 [type: feature] init shenyu ingress controller (#4620)
e7c3fae19 is described below
commit e7c3fae1936862122368124fc59c663bad243a34
Author: Kunshuai Zhu <jo...@gmail.com>
AuthorDate: Mon May 8 18:15:13 2023 +0800
[type: feature] init shenyu ingress controller (#4620)
* [type: feature] init shenyu ingress controller
* fix: change parent of starter-k8s to starter
* fix: fix shenyu-dist image name
---
pom.xml | 8 +
shenyu-bootstrap/pom.xml | 8 +
.../src/main/resources/application.yml | 13 +
shenyu-common/pom.xml | 10 +
.../common/config}/NettyChannelProperties.java | 2 +-
.../shenyu/common/config}/NettyHttpProperties.java | 121 +++++-
.../common/config/ssl/ShenyuSniAsyncMapping.java | 126 +++++++
.../shenyu/common/config/ssl/SslCrtAndKey.java | 24 ++
.../shenyu/common/config/ssl/SslCrtAndKeyFile.java | 93 +++++
.../common/config/ssl/SslCrtAndKeyStream.java | 68 ++++
.../pom.xml | 33 +-
.../org/apache/shenyu/k8s/cache/IngressCache.java | 85 +++++
.../shenyu/k8s/cache/IngressSecretCache.java | 124 +++++++
.../shenyu/k8s/cache/IngressSelectorCache.java | 123 +++++++
.../apache/shenyu/k8s/cache/K8sResourceCache.java | 53 +++
.../org/apache/shenyu/k8s/cache/SelectorCache.java | 57 +++
.../shenyu/k8s/cache/ServiceIngressCache.java | 101 +++++
.../apache/shenyu/k8s/common/IngressConstants.java | 40 ++
.../shenyu/k8s/common/ShenyuMemoryConfig.java | 97 +++++
.../apache/shenyu/k8s/parser/IngressParser.java | 356 ++++++++++++++++++
.../shenyu/k8s/parser/K8sResourceParser.java | 39 ++
.../shenyu/k8s/reconciler/EndpointsReconciler.java | 171 +++++++++
.../shenyu/k8s/reconciler/IngressReconciler.java | 406 +++++++++++++++++++++
.../k8s/repository/ShenyuCacheRepository.java | 135 +++++++
.../apache/shenyu/plugin/base/trie/ShenyuTrie.java | 4 +-
shenyu-spring-boot-starter/pom.xml | 1 +
.../netty/ShenyuNettyWebServerConfiguration.java | 81 +++-
.../ShenyuNettyWebServerConfigurationTest.java | 1 +
.../{ => shenyu-spring-boot-starter-k8s}/pom.xml | 30 +-
.../k8s/IngressControllerConfiguration.java | 264 ++++++++++++++
.../src/main/resources/META-INF/spring.factories | 3 +
.../src/main/resources/META-INF/spring.provides | 1 +
.../ShenyuThreadPoolConfiguration.java | 5 +-
33 files changed, 2642 insertions(+), 41 deletions(-)
diff --git a/pom.xml b/pom.xml
index 06f6f9b1b..b1f0db302 100644
--- a/pom.xml
+++ b/pom.xml
@@ -45,6 +45,7 @@
<module>shenyu-alert</module>
<module>shenyu-sdk</module>
<module>shenyu-discovery</module>
+ <module>shenyu-kubernetes-controller</module>
</modules>
<licenses>
@@ -138,6 +139,7 @@
<clickhouse-http-client.version>0.3.2-patch11</clickhouse-http-client.version>
<eureka.version>1.10.17</eureka.version>
<javatuples.version>1.2</javatuples.version>
+ <k8s-client.version>17.0.2</k8s-client.version>
<!--maven plugin version-->
<exec-maven-plugin.version>1.6.0</exec-maven-plugin.version>
<jacoco-maven-plugin.version>0.8.7</jacoco-maven-plugin.version>
@@ -504,6 +506,12 @@
<artifactId>eureka-client</artifactId>
<version>${eureka.version}</version>
</dependency>
+
+ <dependency>
+ <groupId>io.kubernetes</groupId>
+ <artifactId>client-java-spring-integration</artifactId>
+ <version>${k8s-client.version}</version>
+ </dependency>
</dependencies>
</dependencyManagement>
diff --git a/shenyu-bootstrap/pom.xml b/shenyu-bootstrap/pom.xml
index c0fce4281..29efa85bc 100644
--- a/shenyu-bootstrap/pom.xml
+++ b/shenyu-bootstrap/pom.xml
@@ -571,6 +571,14 @@
</dependency>
<!-- shenyu brpc plugin end -->
+ <!-- shenyu kubernetes controller starter -->
+<!-- <dependency>-->
+<!-- <groupId>org.apache.shenyu</groupId>-->
+<!-- <artifactId>shenyu-spring-boot-starter-k8s</artifactId>-->
+<!-- <version>${project.version}</version>-->
+<!-- </dependency>-->
+ <!-- shenyu kubernetes controller end -->
+
</dependencies>
<profiles>
<profile>
diff --git a/shenyu-bootstrap/src/main/resources/application.yml b/shenyu-bootstrap/src/main/resources/application.yml
index 87e400a51..e49e6a083 100644
--- a/shenyu-bootstrap/src/main/resources/application.yml
+++ b/shenyu-bootstrap/src/main/resources/application.yml
@@ -116,6 +116,19 @@ shenyu:
allocType: "pooled"
messageSizeEstimator: 8
singleEventExecutorPerGroup: true
+ sni:
+ enabled: false
+ mod: k8s #manul
+ defaultK8sSecretNamespace: shenyu-ingress
+ defaultK8sSecretName: default-cert
+# mod: manual
+# certificates:
+# - domain: 'localhost'
+# keyCertChainFile: '/Users/zhukunshuai/Desktop/cert/example.com+1.pem'
+# keyFile: '/Users/zhukunshuai/Desktop/cert/example.com+1-key.pem'
+# - domain: 'example.com'
+# keyCertChainFile: '/Users/zhukunshuai/Desktop/cert/example.com+1.pem'
+# keyFile: '/Users/zhukunshuai/Desktop/cert/example.com+1-key.pem'
# httpclient:
# strategy: webClient
# connectTimeout: 45000
diff --git a/shenyu-common/pom.xml b/shenyu-common/pom.xml
index d860e682b..051aa35c6 100644
--- a/shenyu-common/pom.xml
+++ b/shenyu-common/pom.xml
@@ -73,5 +73,15 @@
<groupId>org.ow2.asm</groupId>
<artifactId>asm-tree</artifactId>
</dependency>
+
+ <dependency>
+ <groupId>io.projectreactor.netty</groupId>
+ <artifactId>reactor-netty-core</artifactId>
+ </dependency>
+
+ <dependency>
+ <groupId>io.projectreactor.netty</groupId>
+ <artifactId>reactor-netty-http</artifactId>
+ </dependency>
</dependencies>
</project>
diff --git a/shenyu-spring-boot-starter/shenyu-spring-boot-starter-gateway/src/main/java/org/apache/shenyu/springboot/starter/netty/NettyChannelProperties.java b/shenyu-common/src/main/java/org/apache/shenyu/common/config/NettyChannelProperties.java
similarity index 99%
rename from shenyu-spring-boot-starter/shenyu-spring-boot-starter-gateway/src/main/java/org/apache/shenyu/springboot/starter/netty/NettyChannelProperties.java
rename to shenyu-common/src/main/java/org/apache/shenyu/common/config/NettyChannelProperties.java
index 20600f7a7..0a18fbb92 100644
--- a/shenyu-spring-boot-starter/shenyu-spring-boot-starter-gateway/src/main/java/org/apache/shenyu/springboot/starter/netty/NettyChannelProperties.java
+++ b/shenyu-common/src/main/java/org/apache/shenyu/common/config/NettyChannelProperties.java
@@ -15,7 +15,7 @@
* limitations under the License.
*/
-package org.apache.shenyu.springboot.starter.netty;
+package org.apache.shenyu.common.config;
import io.netty.buffer.ByteBufAllocator;
import io.netty.buffer.PooledByteBufAllocator;
diff --git a/shenyu-spring-boot-starter/shenyu-spring-boot-starter-gateway/src/main/java/org/apache/shenyu/springboot/starter/netty/NettyHttpProperties.java b/shenyu-common/src/main/java/org/apache/shenyu/common/config/NettyHttpProperties.java
similarity index 70%
rename from shenyu-spring-boot-starter/shenyu-spring-boot-starter-gateway/src/main/java/org/apache/shenyu/springboot/starter/netty/NettyHttpProperties.java
rename to shenyu-common/src/main/java/org/apache/shenyu/common/config/NettyHttpProperties.java
index b0cb71eac..1c14d796d 100644
--- a/shenyu-spring-boot-starter/shenyu-spring-boot-starter-gateway/src/main/java/org/apache/shenyu/springboot/starter/netty/NettyHttpProperties.java
+++ b/shenyu-common/src/main/java/org/apache/shenyu/common/config/NettyHttpProperties.java
@@ -15,7 +15,11 @@
* limitations under the License.
*/
-package org.apache.shenyu.springboot.starter.netty;
+package org.apache.shenyu.common.config;
+
+import org.apache.shenyu.common.config.ssl.SslCrtAndKeyFile;
+
+import java.util.List;
/**
* The netty tcp properties.
@@ -34,6 +38,8 @@ public class NettyHttpProperties {
private SocketChannelProperties socketChannel = new SocketChannelProperties();
+ private SniProperties sni = new SniProperties();
+
/**
* get webServerFactoryEnabled.
*
@@ -125,6 +131,24 @@ public class NettyHttpProperties {
return socketChannel;
}
+ /**
+ * get sni properties.
+ *
+ * @return sni properties
+ */
+ public SniProperties getSni() {
+ return sni;
+ }
+
+ /**
+ * set sni properties.
+ *
+ * @param sni sni properties
+ */
+ public void setSni(final SniProperties sni) {
+ this.sni = sni;
+ }
+
/**
* get access log state.
*
@@ -287,4 +311,99 @@ public class NettyHttpProperties {
this.allowHalfClosure = allowHalfClosure;
}
}
+
+ public static class SniProperties {
+
+ private Boolean enabled = false;
+
+ private String mod;
+
+ private String defaultK8sSecretName;
+
+ private String defaultK8sSecretNamespace;
+
+ private List<SslCrtAndKeyFile> certificates;
+
+ /**
+ * Get default kubernetes secret namespace.
+ *
+ * @return default kubernetes secret namespace
+ */
+ public String getDefaultK8sSecretNamespace() {
+ return defaultK8sSecretNamespace;
+ }
+
+ /**
+ * Set default kubernetes secret namespace.
+ *
+ * @param defaultK8sSecretNamespace default kubernetes secret namespace
+ */
+ public void setDefaultK8sSecretNamespace(final String defaultK8sSecretNamespace) {
+ this.defaultK8sSecretNamespace = defaultK8sSecretNamespace;
+ }
+
+ /**
+ * get enabled.
+ * @return enabled
+ */
+ public Boolean getEnabled() {
+ return enabled;
+ }
+
+ /**
+ * set enabled.
+ * @param enabled enabled
+ */
+ public void setEnabled(final Boolean enabled) {
+ this.enabled = enabled;
+ }
+
+ /**
+ * get mod.
+ * @return mod
+ */
+ public String getMod() {
+ return mod;
+ }
+
+ /**
+ * set mod.
+ * @param mod mod
+ */
+ public void setMod(final String mod) {
+ this.mod = mod;
+ }
+
+ /**
+ * get defaultK8sSecretName.
+ * @return defaultK8sSecretName
+ */
+ public String getDefaultK8sSecretName() {
+ return defaultK8sSecretName;
+ }
+
+ /**
+ * set defaultK8sSecretName.
+ * @param defaultK8sSecretName defaultK8sSecretName
+ */
+ public void setDefaultK8sSecretName(final String defaultK8sSecretName) {
+ this.defaultK8sSecretName = defaultK8sSecretName;
+ }
+
+ /**
+ * get certificates.
+ * @return certificates
+ */
+ public List<SslCrtAndKeyFile> getCertificates() {
+ return certificates;
+ }
+
+ /**
+ * set certificates.
+ * @param certificates certificates
+ */
+ public void setCertificates(final List<SslCrtAndKeyFile> certificates) {
+ this.certificates = certificates;
+ }
+ }
}
diff --git a/shenyu-common/src/main/java/org/apache/shenyu/common/config/ssl/ShenyuSniAsyncMapping.java b/shenyu-common/src/main/java/org/apache/shenyu/common/config/ssl/ShenyuSniAsyncMapping.java
new file mode 100644
index 000000000..561b58d7a
--- /dev/null
+++ b/shenyu-common/src/main/java/org/apache/shenyu/common/config/ssl/ShenyuSniAsyncMapping.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.shenyu.common.config.ssl;
+
+import io.netty.util.AsyncMapping;
+import io.netty.util.concurrent.Future;
+import io.netty.util.concurrent.Promise;
+import org.apache.shenyu.common.exception.ShenyuException;
+import reactor.netty.http.Http11SslContextSpec;
+import reactor.netty.tcp.SslProvider;
+import reactor.netty.tcp.TcpSslContextSpec;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.List;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.regex.Pattern;
+
+/**
+ * Sni async map, can be used to dynamically configure ssl certificates.
+ */
+public class ShenyuSniAsyncMapping implements AsyncMapping<String, SslProvider> {
+
+ private final ConcurrentHashMap<String, SslProvider> sslProviderMap;
+
+ public ShenyuSniAsyncMapping() {
+ this.sslProviderMap = new ConcurrentHashMap<>();
+ }
+
+ public ShenyuSniAsyncMapping(final List<SslCrtAndKeyFile> sslCrtAndKeys) {
+ if (sslCrtAndKeys == null || sslCrtAndKeys.isEmpty()) {
+ throw new ShenyuException("The sslCrtAndKeys can not be null");
+ }
+ this.sslProviderMap = new ConcurrentHashMap<>();
+ sslCrtAndKeys.forEach(sslCrtAndKey -> {
+ Http11SslContextSpec sslContextSpec = Http11SslContextSpec.forServer(new File(sslCrtAndKey.getKeyCertChainFile()),
+ new File(sslCrtAndKey.getKeyFile()));
+ SslProvider sslProvider = SslProvider.builder().sslContext(sslContextSpec).build();
+ this.sslProviderMap.put(sslCrtAndKey.getDomain(), sslProvider);
+ });
+ }
+
+ /**
+ * Add SslProvider by domain.
+ *
+ * @param domain domain
+ * @param sslProvider SslProvider
+ */
+ public void addSslProvider(final String domain, final SslProvider sslProvider) {
+ sslProviderMap.put(domain, sslProvider);
+ }
+
+ /**
+ * Add ssl config.
+ *
+ * @param sslCrtAndKey sslCrtAndKey
+ * @throws IOException IOException
+ */
+ public void addSslCertificate(final SslCrtAndKey sslCrtAndKey) throws IOException {
+ if (sslCrtAndKey instanceof SslCrtAndKeyFile) {
+ SslCrtAndKeyFile sslCrtAndKeyFile = (SslCrtAndKeyFile) sslCrtAndKey;
+ TcpSslContextSpec sslContextSpec = TcpSslContextSpec.forServer(new File(sslCrtAndKeyFile.getKeyCertChainFile()),
+ new File(sslCrtAndKeyFile.getKeyFile()));
+ SslProvider sslProvider = SslProvider.builder().sslContext(sslContextSpec).build();
+ this.sslProviderMap.put(sslCrtAndKeyFile.getDomain(), sslProvider);
+ } else if (sslCrtAndKey instanceof SslCrtAndKeyStream) {
+ SslCrtAndKeyStream sslCrtAndKeyStream = (SslCrtAndKeyStream) sslCrtAndKey;
+ sslCrtAndKeyStream.getKeyCertChainInputStream().reset();
+ sslCrtAndKeyStream.getKeyInputStream().reset();
+ TcpSslContextSpec sslContextSpec = TcpSslContextSpec.forServer(sslCrtAndKeyStream.getKeyCertChainInputStream(),
+ sslCrtAndKeyStream.getKeyInputStream());
+ SslProvider sslProvider = SslProvider.builder().sslContext(sslContextSpec).build();
+ this.sslProviderMap.put(sslCrtAndKeyStream.getDomain(), sslProvider);
+ }
+ }
+
+ /**
+ * Remove ssl config by domain.
+ *
+ * @param domain domain
+ */
+ public void removeSslCertificate(final String domain) {
+ this.sslProviderMap.remove(domain);
+ }
+
+ /**
+ * Get SslProvider by domain.
+ *
+ * @param domain domain
+ * @param promise the promise of SslProvider
+ * @return SslProvider Future
+ */
+ @Override
+ public Future<SslProvider> map(final String domain, final Promise<SslProvider> promise) {
+ try {
+ for (String key : sslProviderMap.keySet()) {
+ if (matchDomain(domain, key)) {
+ return promise.setSuccess(sslProviderMap.get(key));
+ }
+ }
+ return promise.setFailure(new ShenyuException(
+ String.format("Can not find ssl certificate of domain %s", domain)));
+ } catch (Throwable cause) {
+ return promise.setFailure(cause);
+ }
+ }
+
+ private boolean matchDomain(final String domain, final String pattern) {
+ return Pattern.matches(pattern.replace(".", "\\.").replace("*", ".*"), domain);
+ }
+}
diff --git a/shenyu-common/src/main/java/org/apache/shenyu/common/config/ssl/SslCrtAndKey.java b/shenyu-common/src/main/java/org/apache/shenyu/common/config/ssl/SslCrtAndKey.java
new file mode 100644
index 000000000..87531ba4b
--- /dev/null
+++ b/shenyu-common/src/main/java/org/apache/shenyu/common/config/ssl/SslCrtAndKey.java
@@ -0,0 +1,24 @@
+/*
+ * 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.shenyu.common.config.ssl;
+
+/**
+ * ssl certificate and key.
+ */
+public interface SslCrtAndKey {
+}
diff --git a/shenyu-common/src/main/java/org/apache/shenyu/common/config/ssl/SslCrtAndKeyFile.java b/shenyu-common/src/main/java/org/apache/shenyu/common/config/ssl/SslCrtAndKeyFile.java
new file mode 100644
index 000000000..8a39a2848
--- /dev/null
+++ b/shenyu-common/src/main/java/org/apache/shenyu/common/config/ssl/SslCrtAndKeyFile.java
@@ -0,0 +1,93 @@
+/*
+ * 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.shenyu.common.config.ssl;
+
+/**
+ * ssl certificate and key.
+ */
+public class SslCrtAndKeyFile implements SslCrtAndKey {
+
+ private String domain;
+
+ private String keyCertChainFile;
+
+ private String keyFile;
+
+ public SslCrtAndKeyFile() {
+ }
+
+ public SslCrtAndKeyFile(final String domain, final String keyCertChainFile, final String keyFile) {
+ this.domain = domain;
+ this.keyCertChainFile = keyCertChainFile;
+ this.keyFile = keyFile;
+ }
+
+ /**
+ * set domain.
+ *
+ * @param domain domain
+ */
+ public void setDomain(final String domain) {
+ this.domain = domain;
+ }
+
+ /**
+ * set keyCertChainFile.
+ *
+ * @param keyCertChainFile keyCertChainFile
+ */
+ public void setKeyCertChainFile(final String keyCertChainFile) {
+ this.keyCertChainFile = keyCertChainFile;
+ }
+
+ /**
+ * set keyFile.
+ *
+ * @param keyFile keyFile
+ */
+ public void setKeyFile(final String keyFile) {
+ this.keyFile = keyFile;
+ }
+
+ /**
+ * get domain.
+ *
+ * @return domain
+ */
+ public String getDomain() {
+ return domain;
+ }
+
+ /**
+ * get keyCertChainFile.
+ *
+ * @return keyCertChainFile
+ */
+ public String getKeyCertChainFile() {
+ return keyCertChainFile;
+ }
+
+ /**
+ * get keyFile.
+ *
+ * @return keyFile
+ */
+ public String getKeyFile() {
+ return keyFile;
+ }
+}
diff --git a/shenyu-common/src/main/java/org/apache/shenyu/common/config/ssl/SslCrtAndKeyStream.java b/shenyu-common/src/main/java/org/apache/shenyu/common/config/ssl/SslCrtAndKeyStream.java
new file mode 100644
index 000000000..67728bc5a
--- /dev/null
+++ b/shenyu-common/src/main/java/org/apache/shenyu/common/config/ssl/SslCrtAndKeyStream.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.shenyu.common.config.ssl;
+
+import java.io.InputStream;
+
+/**
+ * ssl certificate and key.
+ */
+public class SslCrtAndKeyStream implements SslCrtAndKey {
+
+ private String domain;
+
+ private InputStream keyCertChainInputStream;
+
+ private InputStream keyInputStream;
+
+ public SslCrtAndKeyStream() {
+ }
+
+ public SslCrtAndKeyStream(final String domain, final InputStream keyCertChainInputStream, final InputStream keyInputStream) {
+ this.domain = domain;
+ this.keyCertChainInputStream = keyCertChainInputStream;
+ this.keyInputStream = keyInputStream;
+ }
+
+ /**
+ * get domain.
+ *
+ * @return domain
+ */
+ public String getDomain() {
+ return domain;
+ }
+
+ /**
+ * get keyCertChainInputStream.
+ *
+ * @return keyCertChainInputStream
+ */
+ public InputStream getKeyCertChainInputStream() {
+ return keyCertChainInputStream;
+ }
+
+ /**
+ * get keyInputStream.
+ *
+ * @return keyInputStream
+ */
+ public InputStream getKeyInputStream() {
+ return keyInputStream;
+ }
+}
diff --git a/shenyu-spring-boot-starter/pom.xml b/shenyu-kubernetes-controller/pom.xml
similarity index 60%
copy from shenyu-spring-boot-starter/pom.xml
copy to shenyu-kubernetes-controller/pom.xml
index 071894ae9..fcf0ce898 100644
--- a/shenyu-spring-boot-starter/pom.xml
+++ b/shenyu-kubernetes-controller/pom.xml
@@ -16,31 +16,32 @@
~ limitations under the License.
-->
-<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
- <groupId>org.apache.shenyu</groupId>
<artifactId>shenyu</artifactId>
+ <groupId>org.apache.shenyu</groupId>
<version>2.6.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
- <artifactId>shenyu-spring-boot-starter</artifactId>
- <packaging>pom</packaging>
-
- <modules>
- <module>shenyu-spring-boot-starter-gateway</module>
- <module>shenyu-spring-boot-starter-plugin</module>
- <module>shenyu-spring-boot-starter-sync-data-center</module>
- <module>shenyu-spring-boot-starter-client</module>
- <module>shenyu-spring-boot-starter-instance</module>
- <module>shenyu-spring-boot-starter-sdk</module>
- </modules>
+ <artifactId>shenyu-kubernetes-controller</artifactId>
<dependencies>
+ <dependency>
+ <groupId>org.apache.shenyu</groupId>
+ <artifactId>shenyu-plugin-base</artifactId>
+ <version>${project.version}</version>
+ </dependency>
+
<dependency>
<groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-configuration-processor</artifactId>
- <optional>true</optional>
+ <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
- </dependencies>
+ <dependency>
+ <groupId>io.kubernetes</groupId>
+ <artifactId>client-java-spring-integration</artifactId>
+ </dependency>
+ </dependencies>
</project>
diff --git a/shenyu-kubernetes-controller/src/main/java/org/apache/shenyu/k8s/cache/IngressCache.java b/shenyu-kubernetes-controller/src/main/java/org/apache/shenyu/k8s/cache/IngressCache.java
new file mode 100644
index 000000000..f16b2c268
--- /dev/null
+++ b/shenyu-kubernetes-controller/src/main/java/org/apache/shenyu/k8s/cache/IngressCache.java
@@ -0,0 +1,85 @@
+/*
+ * 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.shenyu.k8s.cache;
+
+import com.google.common.collect.Maps;
+import io.kubernetes.client.openapi.models.V1Ingress;
+
+import java.util.Map;
+
+/**
+ * The cache for V1Ingress.
+ */
+public final class IngressCache implements K8sResourceCache<V1Ingress> {
+
+ private static final IngressCache INSTANCE = new IngressCache();
+
+ private static final Map<String, V1Ingress> INGRESS_MAP = Maps.newConcurrentMap();
+
+ private IngressCache() {
+ }
+
+ /**
+ * Get singleton of IngressCache.
+ *
+ * @return IngressCache
+ */
+ public static IngressCache getInstance() {
+ return INSTANCE;
+ }
+
+ /**
+ * Put ingress.
+ *
+ * @param namespace namespace
+ * @param name name
+ * @param resource resource
+ */
+ @Override
+ public void put(final String namespace, final String name, final V1Ingress resource) {
+ INGRESS_MAP.put(getKey(namespace, name), resource);
+ }
+
+ /**
+ * Get ingress.
+ *
+ * @param namespace namespace
+ * @param name name
+ * @return V1Ingress
+ */
+ @Override
+ public V1Ingress get(final String namespace, final String name) {
+ return INGRESS_MAP.get(getKey(namespace, name));
+ }
+
+ /**
+ * Remove ingress.
+ *
+ * @param namespace namespace
+ * @param name name
+ * @return V1Ingress
+ */
+ @Override
+ public V1Ingress remove(final String namespace, final String name) {
+ return INGRESS_MAP.remove(getKey(namespace, name));
+ }
+
+ private String getKey(final String namespace, final String name) {
+ return String.format("%s-%s", namespace, name);
+ }
+}
diff --git a/shenyu-kubernetes-controller/src/main/java/org/apache/shenyu/k8s/cache/IngressSecretCache.java b/shenyu-kubernetes-controller/src/main/java/org/apache/shenyu/k8s/cache/IngressSecretCache.java
new file mode 100644
index 000000000..274c5d52d
--- /dev/null
+++ b/shenyu-kubernetes-controller/src/main/java/org/apache/shenyu/k8s/cache/IngressSecretCache.java
@@ -0,0 +1,124 @@
+/*
+ * 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.shenyu.k8s.cache;
+
+import com.google.common.collect.Maps;
+import org.apache.shenyu.common.exception.ShenyuException;
+
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicInteger;
+
+/**
+ * The cache for tls config.
+ */
+public final class IngressSecretCache {
+
+ private static final IngressSecretCache INSTANCE = new IngressSecretCache();
+
+ private static final Map<String, Set<String>> INGRESS_DOMAIN_MAP = Maps.newConcurrentMap();
+
+ private static final Map<String, AtomicInteger> DOMAIN_NUMS_MAP = Maps.newConcurrentMap();
+
+ private IngressSecretCache() {
+ }
+
+ /**
+ * Get singleton of IngressSecretCache.
+ *
+ * @return IngressSecretCache
+ */
+ public static IngressSecretCache getInstance() {
+ return INSTANCE;
+ }
+
+ /**
+ * Put tls domain by ingress.
+ *
+ * @param namespace namespace
+ * @param ingressName name
+ * @param domain tls domain
+ */
+ public void putDomainByIngress(final String namespace, final String ingressName, final String domain) {
+ Set<String> set = INGRESS_DOMAIN_MAP.computeIfAbsent(getKey(namespace, ingressName), k -> new HashSet<>());
+ set.add(domain);
+ }
+
+ /**
+ * Put tls domain set by ingress.
+ *
+ * @param namespace namespace
+ * @param ingressName name
+ * @param domainSet domain set
+ */
+ public void putDomainByIngress(final String namespace, final String ingressName, final Set<String> domainSet) {
+ INGRESS_DOMAIN_MAP.put(getKey(namespace, ingressName), domainSet);
+ }
+
+ /**
+ * Get domain set by ingress.
+ *
+ * @param namespace namespace
+ * @param ingressName ingressName
+ * @return domain set
+ */
+ public Set<String> getDomainByIngress(final String namespace, final String ingressName) {
+ return INGRESS_DOMAIN_MAP.get(getKey(namespace, ingressName));
+ }
+
+ /**
+ * Remove domain set by ingress.
+ *
+ * @param namespace namespace
+ * @param ingressName ingressName
+ * @return domain set
+ */
+ public Set<String> removeDomainByIngress(final String namespace, final String ingressName) {
+ return INGRESS_DOMAIN_MAP.remove(getKey(namespace, ingressName));
+ }
+
+ /**
+ * Get and increment the number of the ingress that enables the domain name to take effect.
+ *
+ * @param domain tls domain
+ * @return the previous number of the ingress that enables the domain name to take effect
+ */
+ public Integer getAndIncrementDomainNums(final String domain) {
+ AtomicInteger count = DOMAIN_NUMS_MAP.computeIfAbsent(domain, k -> new AtomicInteger(0));
+ return count.getAndIncrement();
+ }
+
+ /**
+ * Get and decrement the number of the ingress that enables the domain name to take effect.
+ *
+ * @param domain tls domain
+ * @return the previous number of the ingress that enables the domain name to take effect
+ */
+ public Integer getAndDecrementDomainNums(final String domain) {
+ AtomicInteger count = DOMAIN_NUMS_MAP.computeIfAbsent(domain, k -> new AtomicInteger(0));
+ if (count.intValue() > 0) {
+ return count.getAndDecrement();
+ }
+ throw new ShenyuException("Decrement when domain ssl counts <= 0, an unknown exception has occurred.");
+ }
+
+ private String getKey(final String namespace, final String name) {
+ return namespace + "-" + name;
+ }
+}
diff --git a/shenyu-kubernetes-controller/src/main/java/org/apache/shenyu/k8s/cache/IngressSelectorCache.java b/shenyu-kubernetes-controller/src/main/java/org/apache/shenyu/k8s/cache/IngressSelectorCache.java
new file mode 100644
index 000000000..2d61876bc
--- /dev/null
+++ b/shenyu-kubernetes-controller/src/main/java/org/apache/shenyu/k8s/cache/IngressSelectorCache.java
@@ -0,0 +1,123 @@
+/*
+ * 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.shenyu.k8s.cache;
+
+import com.google.common.collect.Maps;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicLong;
+
+/**
+ * The cache mapping Ingress to Selector id list.
+ */
+public final class IngressSelectorCache implements SelectorCache {
+
+ private static final IngressSelectorCache INSTANCE = new IngressSelectorCache();
+
+ private static final Map<String, List<String>> SELECTOR_MAP = Maps.newConcurrentMap();
+
+ private static final AtomicLong GROWING_ID = new AtomicLong(10);
+
+ private IngressSelectorCache() {
+ }
+
+ /**
+ * Get singleton of IngressSelectorCache.
+ *
+ * @return IngressSelectorCache
+ */
+ public static IngressSelectorCache getInstance() {
+ return INSTANCE;
+ }
+
+ /**
+ * Put selector id list by ingress.
+ *
+ * @param namespace ingress namespace
+ * @param name ingress name
+ * @param pluginName plugin name
+ * @param selectorIdList selector id list
+ * @return the previous selector id list
+ */
+ @Override
+ public List<String> put(final String namespace,
+ final String name,
+ final String pluginName,
+ final List<String> selectorIdList) {
+ return SELECTOR_MAP.put(getKey(namespace, name, pluginName), selectorIdList);
+ }
+
+ /**
+ * Put selector id by ingress.
+ *
+ * @param namespace namespace
+ * @param name name
+ * @param pluginName plugin name
+ * @param selectorId selector id
+ * @return the previous selector id list
+ */
+ public List<String> put(final String namespace,
+ final String name,
+ final String pluginName,
+ final String selectorId) {
+ List<String> selectorIdList = SELECTOR_MAP.computeIfAbsent(getKey(namespace, name, pluginName), k -> new ArrayList<>());
+ selectorIdList.add(selectorId);
+ return SELECTOR_MAP.put(getKey(namespace, name, pluginName), selectorIdList);
+ }
+
+ /**
+ * Get Selector id list by ingress.
+ *
+ * @param namespace ingress namespace
+ * @param name ingress name
+ * @param pluginName plugin name
+ * @return selector id list
+ */
+ @Override
+ public List<String> get(final String namespace, final String name, final String pluginName) {
+ return SELECTOR_MAP.get(getKey(namespace, name, pluginName));
+ }
+
+ /**
+ * Remove Selector id list by ingress.
+ *
+ * @param namespace ingress namespace
+ * @param name ingress name
+ * @param pluginName plugin name
+ * @return selector id list
+ */
+ @Override
+ public List<String> remove(final String namespace, final String name, final String pluginName) {
+ return SELECTOR_MAP.remove(getKey(namespace, name, pluginName));
+ }
+
+ /**
+ * Get auto-incremented selector id.
+ *
+ * @return selector id
+ */
+ public String generateSelectorId() {
+ return String.valueOf(GROWING_ID.getAndIncrement());
+ }
+
+ private String getKey(final String namespace, final String name, final String pluginName) {
+ return String.format("%s-%s-%s", namespace, name, pluginName);
+ }
+}
diff --git a/shenyu-kubernetes-controller/src/main/java/org/apache/shenyu/k8s/cache/K8sResourceCache.java b/shenyu-kubernetes-controller/src/main/java/org/apache/shenyu/k8s/cache/K8sResourceCache.java
new file mode 100644
index 000000000..aa0c46998
--- /dev/null
+++ b/shenyu-kubernetes-controller/src/main/java/org/apache/shenyu/k8s/cache/K8sResourceCache.java
@@ -0,0 +1,53 @@
+/*
+ * 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.shenyu.k8s.cache;
+
+/**
+ * The cache for Kubernetes resource.
+ *
+ * @param <T> resource type
+ */
+public interface K8sResourceCache<T> {
+
+ /**
+ * Put Kubernetes resource.
+ *
+ * @param namespace namespace
+ * @param name name
+ * @param resource resource
+ */
+ void put(String namespace, String name, T resource);
+
+ /**
+ * Get Kubernetes resource.
+ *
+ * @param namespace namespace
+ * @param name name
+ * @return resource
+ */
+ T get(String namespace, String name);
+
+ /**
+ * Remove Kubernetes resource.
+ *
+ * @param namespace namespace
+ * @param name name
+ * @return the resource removed
+ */
+ T remove(String namespace, String name);
+}
diff --git a/shenyu-kubernetes-controller/src/main/java/org/apache/shenyu/k8s/cache/SelectorCache.java b/shenyu-kubernetes-controller/src/main/java/org/apache/shenyu/k8s/cache/SelectorCache.java
new file mode 100644
index 000000000..513a3bb45
--- /dev/null
+++ b/shenyu-kubernetes-controller/src/main/java/org/apache/shenyu/k8s/cache/SelectorCache.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.shenyu.k8s.cache;
+
+import java.util.List;
+
+/**
+ * The cache for Selector id.
+ */
+public interface SelectorCache {
+
+ /**
+ * Put Selector id list by resource namespace, name and plugin name.
+ *
+ * @param namespace resource namespace
+ * @param name resource name
+ * @param pluginName plugin name
+ * @param selectorIdList selector id list
+ * @return The previous value associated with key, or null if there was no mapping for key
+ */
+ List<String> put(String namespace, String name, String pluginName, List<String> selectorIdList);
+
+ /**
+ * Get Selector id list by resource namespace, name and plugin name.
+ *
+ * @param namespace resource namespace
+ * @param name resource name
+ * @param pluginName plugin name
+ * @return Selector id list
+ */
+ List<String> get(String namespace, String name, String pluginName);
+
+ /**
+ * Remove Selector id list by resource namespace, name and plugin name.
+ *
+ * @param namespace resource namespace
+ * @param name resource name
+ * @param pluginName plugin name
+ * @return Selector id list
+ */
+ List<String> remove(String namespace, String name, String pluginName);
+}
diff --git a/shenyu-kubernetes-controller/src/main/java/org/apache/shenyu/k8s/cache/ServiceIngressCache.java b/shenyu-kubernetes-controller/src/main/java/org/apache/shenyu/k8s/cache/ServiceIngressCache.java
new file mode 100644
index 000000000..4531e713f
--- /dev/null
+++ b/shenyu-kubernetes-controller/src/main/java/org/apache/shenyu/k8s/cache/ServiceIngressCache.java
@@ -0,0 +1,101 @@
+/*
+ * 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.shenyu.k8s.cache;
+
+import com.google.common.collect.Maps;
+import org.apache.commons.lang3.tuple.Pair;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * The cache for mapping service name to ingress name.
+ */
+public final class ServiceIngressCache {
+
+ private static final ServiceIngressCache INSTANCE = new ServiceIngressCache();
+
+ private static final Map<String, List<Pair<String, String>>> INGRESS_MAP = Maps.newConcurrentMap();
+
+ private ServiceIngressCache() {
+ }
+
+ /**
+ * Get singleton of ServiceIngressCache.
+ *
+ * @return ServiceIngressCache
+ */
+ public static ServiceIngressCache getInstance() {
+ return INSTANCE;
+ }
+
+ /**
+ * Get ingress namespace and name by service namespace and namespace.
+ *
+ * @param namespace namespace
+ * @param serviceName service name
+ * @return ingress namespace and name
+ */
+ public List<Pair<String, String>> getIngressName(final String namespace, final String serviceName) {
+ return INGRESS_MAP.get(getKey(namespace, serviceName));
+ }
+
+ /**
+ * Put ingress by service namespace and name.
+ *
+ * @param namespace service namespace
+ * @param serviceName service name
+ * @param ingressNamespace ingress namespace
+ * @param ingressName ingress name
+ */
+ public void putIngressName(final String namespace, final String serviceName, final String ingressNamespace, final String ingressName) {
+ List<Pair<String, String>> list = INGRESS_MAP.computeIfAbsent(getKey(namespace, serviceName), k -> new ArrayList<>());
+ list.add(Pair.of(ingressNamespace, ingressName));
+ }
+
+ /**
+ * Remove all ingress by service namespace and name.
+ *
+ * @param namespace service namespace
+ * @param serviceName service name
+ * @return the ingress list removed
+ */
+ public List<Pair<String, String>> removeAllIngressName(final String namespace, final String serviceName) {
+ return INGRESS_MAP.remove(getKey(namespace, serviceName));
+ }
+
+ /**
+ * Remove specified ingress by service and ingress.
+ *
+ * @param namespace service namespace
+ * @param serviceName service name
+ * @param ingressNamespace ingress namespace
+ * @param ingressName ingress name
+ */
+ public void removeSpecifiedIngressName(final String namespace, final String serviceName, final String ingressNamespace, final String ingressName) {
+ List<Pair<String, String>> list = INGRESS_MAP.get(getKey(namespace, serviceName));
+ if (list != null) {
+ list.removeIf(item -> item.getLeft().equals(ingressNamespace) && item.getRight().equals(ingressName));
+ }
+ }
+
+ private String getKey(final String namespace, final String name) {
+ return String.format("%s-%s", namespace, name);
+ }
+}
diff --git a/shenyu-kubernetes-controller/src/main/java/org/apache/shenyu/k8s/common/IngressConstants.java b/shenyu-kubernetes-controller/src/main/java/org/apache/shenyu/k8s/common/IngressConstants.java
new file mode 100644
index 000000000..e3383c171
--- /dev/null
+++ b/shenyu-kubernetes-controller/src/main/java/org/apache/shenyu/k8s/common/IngressConstants.java
@@ -0,0 +1,40 @@
+/*
+ * 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.shenyu.k8s.common;
+
+public class IngressConstants {
+
+ public static final String K8S_INGRESS_CLASS_ANNOTATION_KEY = "kubernetes.io/ingress.class";
+
+ public static final String SHENYU_INGRESS_CLASS = "shenyu";
+
+ // Load balance type name, refer to LoadBalanceEnum
+ public static final String LOADBALANCER_ANNOTATION_KEY = "shenyu.apache.org/loadbalancer";
+
+ // number of retries
+ public static final String RETRY_ANNOTATION_KEY = "shenyu.apache.org/retry";
+
+ // timeout, in milliseconds
+ public static final String TIMEOUT_ANNOTATION_KEY = "shenyu.apache.org/timeout";
+
+ // The maximum length of the request body, in bytes
+ public static final String HEADER_MAX_SIZE_ANNOTATION_KEY = "shenyu.apache.org/header-max-size";
+
+ // The maximum length of the request header, in bytes
+ public static final String REQUEST_MAX_SIZE_ANNOTATION_KEY = "shenyu.apache.org/request-max-size";
+}
diff --git a/shenyu-kubernetes-controller/src/main/java/org/apache/shenyu/k8s/common/ShenyuMemoryConfig.java b/shenyu-kubernetes-controller/src/main/java/org/apache/shenyu/k8s/common/ShenyuMemoryConfig.java
new file mode 100644
index 000000000..702000413
--- /dev/null
+++ b/shenyu-kubernetes-controller/src/main/java/org/apache/shenyu/k8s/common/ShenyuMemoryConfig.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.shenyu.k8s.common;
+
+import org.apache.commons.lang3.tuple.Pair;
+import org.apache.shenyu.common.config.ssl.SslCrtAndKeyStream;
+import org.apache.shenyu.common.dto.RuleData;
+import org.apache.shenyu.common.dto.SelectorData;
+
+import java.util.List;
+
+/**
+ * The configuration that shenyu can read directly.
+ */
+public class ShenyuMemoryConfig {
+
+ private Pair<Pair<String, String>, Pair<SelectorData, RuleData>> globalDefaultBackend;
+
+ private List<Pair<SelectorData, RuleData>> routeConfigList;
+
+ private List<SslCrtAndKeyStream> tlsConfigList;
+
+ /**
+ * ShenyuMemoryConfig Constructor.
+ */
+ public ShenyuMemoryConfig() {
+ }
+
+ /**
+ * Get GlobalDefaultBackend.
+ *
+ * @return GlobalDefaultBackend
+ */
+ public Pair<Pair<String, String>, Pair<SelectorData, RuleData>> getGlobalDefaultBackend() {
+ return globalDefaultBackend;
+ }
+
+ /**
+ * Set GlobalDefaultBackend.
+ *
+ * @param globalDefaultBackend GlobalDefaultBackend
+ */
+ public void setGlobalDefaultBackend(final Pair<Pair<String, String>, Pair<SelectorData, RuleData>> globalDefaultBackend) {
+ this.globalDefaultBackend = globalDefaultBackend;
+ }
+
+ /**
+ * Get RouteConfigList.
+ *
+ * @return RouteConfigList
+ */
+ public List<Pair<SelectorData, RuleData>> getRouteConfigList() {
+ return routeConfigList;
+ }
+
+ /**
+ * Set RouteConfigList.
+ *
+ * @param routeConfigList RouteConfigList
+ */
+ public void setRouteConfigList(final List<Pair<SelectorData, RuleData>> routeConfigList) {
+ this.routeConfigList = routeConfigList;
+ }
+
+ /**
+ * Get TlsConfigList.
+ *
+ * @return TlsConfigList
+ */
+ public List<SslCrtAndKeyStream> getTlsConfigList() {
+ return tlsConfigList;
+ }
+
+ /**
+ * Set TlsConfigList.
+ *
+ * @param tlsConfigList TlsConfigList
+ */
+ public void setTlsConfigList(final List<SslCrtAndKeyStream> tlsConfigList) {
+ this.tlsConfigList = tlsConfigList;
+ }
+}
diff --git a/shenyu-kubernetes-controller/src/main/java/org/apache/shenyu/k8s/parser/IngressParser.java b/shenyu-kubernetes-controller/src/main/java/org/apache/shenyu/k8s/parser/IngressParser.java
new file mode 100644
index 000000000..2483c7bc4
--- /dev/null
+++ b/shenyu-kubernetes-controller/src/main/java/org/apache/shenyu/k8s/parser/IngressParser.java
@@ -0,0 +1,356 @@
+/*
+ * 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.shenyu.k8s.parser;
+
+import io.kubernetes.client.informer.SharedIndexInformer;
+import io.kubernetes.client.informer.cache.Lister;
+import io.kubernetes.client.openapi.ApiException;
+import io.kubernetes.client.openapi.apis.CoreV1Api;
+import io.kubernetes.client.openapi.models.V1Endpoints;
+import io.kubernetes.client.openapi.models.V1EndpointSubset;
+import io.kubernetes.client.openapi.models.V1EndpointAddress;
+import io.kubernetes.client.openapi.models.V1Ingress;
+import io.kubernetes.client.openapi.models.V1IngressBackend;
+import io.kubernetes.client.openapi.models.V1IngressRule;
+import io.kubernetes.client.openapi.models.V1IngressTLS;
+import io.kubernetes.client.openapi.models.V1HTTPIngressPath;
+import io.kubernetes.client.openapi.models.V1Service;
+import io.kubernetes.client.openapi.models.V1IngressServiceBackend;
+import io.kubernetes.client.openapi.models.V1Secret;
+import org.apache.commons.lang3.tuple.Pair;
+import org.apache.shenyu.common.config.ssl.SslCrtAndKeyStream;
+import org.apache.shenyu.common.dto.ConditionData;
+import org.apache.shenyu.common.dto.RuleData;
+import org.apache.shenyu.common.dto.SelectorData;
+import org.apache.shenyu.common.dto.convert.rule.impl.DivideRuleHandle;
+import org.apache.shenyu.common.dto.convert.selector.DivideUpstream;
+import org.apache.shenyu.common.enums.MatchModeEnum;
+import org.apache.shenyu.common.enums.LoadBalanceEnum;
+import org.apache.shenyu.common.enums.OperatorEnum;
+import org.apache.shenyu.common.enums.ParamTypeEnum;
+import org.apache.shenyu.common.enums.PluginEnum;
+import org.apache.shenyu.common.enums.SelectorTypeEnum;
+import org.apache.shenyu.common.utils.GsonUtils;
+import org.apache.shenyu.k8s.common.IngressConstants;
+import org.apache.shenyu.k8s.common.ShenyuMemoryConfig;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+import java.util.Map;
+
+/**
+ * Parser of Ingress.
+ */
+public class IngressParser implements K8sResourceParser<V1Ingress> {
+
+ private static final Logger LOG = LoggerFactory.getLogger(IngressParser.class);
+
+ private final Lister<V1Service> serviceLister;
+
+ private final Lister<V1Endpoints> endpointsLister;
+
+ /**
+ * IngressParser Constructor.
+ *
+ * @param serviceInformer serviceInformer
+ * @param endpointsInformer endpointsInformer
+ */
+ public IngressParser(final SharedIndexInformer<V1Service> serviceInformer, final SharedIndexInformer<V1Endpoints> endpointsInformer) {
+ this.serviceLister = new Lister<>(serviceInformer.getIndexer());
+ this.endpointsLister = new Lister<>(endpointsInformer.getIndexer());
+ }
+
+ /**
+ * Parse ingress to ShenyuMemoryConfig.
+ *
+ * @param ingress ingress resource
+ * @param coreV1Api coreV1Api
+ * @return ShenyuMemoryConfig
+ */
+ @Override
+ public ShenyuMemoryConfig parse(final V1Ingress ingress, final CoreV1Api coreV1Api) {
+ ShenyuMemoryConfig res = new ShenyuMemoryConfig();
+
+ if (ingress.getSpec() != null) {
+ // Parse the default backend
+ V1IngressBackend defaultBackend = ingress.getSpec().getDefaultBackend();
+ List<V1IngressRule> rules = ingress.getSpec().getRules();
+ List<V1IngressTLS> tlsList = ingress.getSpec().getTls();
+
+ String namespace = Objects.requireNonNull(ingress.getMetadata()).getNamespace();
+ List<DivideUpstream> defaultUpstreamList = parseDefaultService(defaultBackend, namespace);
+
+ if (rules == null || rules.isEmpty()) {
+ // if rules is null, defaultBackend become global default
+ if (defaultBackend != null && defaultBackend.getService() != null) {
+ Pair<SelectorData, RuleData> defaultRouteConfig = getDefaultRouteConfig(defaultUpstreamList, ingress.getMetadata().getAnnotations());
+ res.setGlobalDefaultBackend(Pair.of(Pair.of(namespace + "/" + ingress.getMetadata().getName(), defaultBackend.getService().getName()),
+ defaultRouteConfig));
+ }
+ } else {
+ // if rules is not null, defaultBackend is default in this ingress
+ List<Pair<SelectorData, RuleData>> routeList = new ArrayList<>(rules.size());
+ for (V1IngressRule ingressRule : rules) {
+ List<Pair<SelectorData, RuleData>> routes = parseIngressRule(ingressRule, defaultUpstreamList,
+ Objects.requireNonNull(ingress.getMetadata()).getNamespace(), ingress.getMetadata().getAnnotations());
+ routeList.addAll(routes);
+ }
+ res.setRouteConfigList(routeList);
+ }
+
+ // Parse tls
+ if (tlsList != null && !tlsList.isEmpty()) {
+ List<SslCrtAndKeyStream> sslList = new ArrayList<>();
+ for (V1IngressTLS tls : tlsList) {
+ if (tls.getSecretName() != null && tls.getHosts() != null && !tls.getHosts().isEmpty()) {
+ try {
+ V1Secret secret = coreV1Api.readNamespacedSecret(tls.getSecretName(), namespace, "ture");
+ if (secret.getData() != null) {
+ InputStream keyCertChainInputStream = new ByteArrayInputStream(secret.getData().get("tls.crt"));
+ InputStream keyInputStream = new ByteArrayInputStream(secret.getData().get("tls.key"));
+ tls.getHosts().forEach(host ->
+ sslList.add(new SslCrtAndKeyStream(host, keyCertChainInputStream, keyInputStream))
+ );
+ }
+ } catch (ApiException e) {
+ LOG.error("parse tls failed ", e);
+ }
+ }
+ }
+ res.setTlsConfigList(sslList);
+ }
+ }
+ return res;
+ }
+
+ private List<DivideUpstream> parseDefaultService(final V1IngressBackend defaultBackend, final String namespace) {
+ List<DivideUpstream> defaultUpstreamList = new ArrayList<>();
+ if (defaultBackend != null && defaultBackend.getService() != null) {
+ String serviceName = defaultBackend.getService().getName();
+ // shenyu routes directly to the container
+ V1Endpoints v1Endpoints = endpointsLister.namespace(namespace).get(serviceName);
+ List<V1EndpointSubset> subsets = v1Endpoints.getSubsets();
+ if (subsets == null || subsets.isEmpty()) {
+ LOG.info("Endpoints {} do not have subsets", serviceName);
+ } else {
+ for (V1EndpointSubset subset : subsets) {
+ List<V1EndpointAddress> addresses = subset.getAddresses();
+ if (addresses == null || addresses.isEmpty()) {
+ continue;
+ }
+ for (V1EndpointAddress address : addresses) {
+ String upstreamIp = address.getIp();
+ String defaultPort = parsePort(defaultBackend.getService());
+ if (defaultPort != null) {
+ DivideUpstream upstream = new DivideUpstream();
+ upstream.setUpstreamUrl(upstreamIp + ":" + defaultPort);
+ upstream.setWeight(100);
+ // TODO support config protocol in annotation
+ upstream.setProtocol("http://");
+ upstream.setWarmup(0);
+ upstream.setStatus(true);
+ upstream.setUpstreamHost("");
+ defaultUpstreamList.add(upstream);
+ }
+ }
+ }
+ }
+ }
+ return defaultUpstreamList;
+ }
+
+ private List<Pair<SelectorData, RuleData>> parseIngressRule(final V1IngressRule ingressRule,
+ final List<DivideUpstream> defaultUpstream,
+ final String namespace,
+ final Map<String, String> annotations) {
+ List<Pair<SelectorData, RuleData>> res = new ArrayList<>();
+
+ ConditionData hostCondition = null;
+ if (ingressRule.getHost() != null) {
+ hostCondition = new ConditionData();
+ hostCondition.setParamType(ParamTypeEnum.DOMAIN.getName());
+ hostCondition.setOperator(OperatorEnum.EQ.getAlias());
+ hostCondition.setParamValue(ingressRule.getHost());
+ }
+ if (ingressRule.getHttp() != null) {
+ List<V1HTTPIngressPath> paths = ingressRule.getHttp().getPaths();
+ if (paths != null) {
+ for (V1HTTPIngressPath path : paths) {
+ if (path.getPath() == null) {
+ continue;
+ }
+
+ OperatorEnum operator;
+ if ("ImplementationSpecific".equals(path.getPathType())) {
+ operator = OperatorEnum.MATCH;
+ } else if ("Prefix".equals(path.getPathType())) {
+ operator = OperatorEnum.STARTS_WITH;
+ } else if ("Exact".equals(path.getPathType())) {
+ operator = OperatorEnum.EQ;
+ } else {
+ LOG.info("Invalid path type, set it with match operator");
+ operator = OperatorEnum.MATCH;
+ }
+
+ ConditionData pathCondition = new ConditionData();
+ pathCondition.setOperator(operator.getAlias());
+ pathCondition.setParamType(ParamTypeEnum.URI.getName());
+ pathCondition.setParamValue(path.getPath());
+ List<ConditionData> conditionList = new ArrayList<>(2);
+ if (hostCondition != null) {
+ conditionList.add(hostCondition);
+ }
+ conditionList.add(pathCondition);
+
+ SelectorData selectorData = SelectorData.builder()
+ .pluginId(String.valueOf(PluginEnum.DIVIDE.getCode()))
+ .pluginName(PluginEnum.DIVIDE.getName())
+ .name(path.getPath())
+ .matchMode(MatchModeEnum.AND.getCode())
+ .type(SelectorTypeEnum.CUSTOM_FLOW.getCode())
+ .enabled(true)
+ .logged(false)
+ .continued(true)
+ .conditionList(conditionList).build();
+ List<DivideUpstream> upstreamList = parseUpstream(path.getBackend(), namespace);
+ if (upstreamList.isEmpty()) {
+ upstreamList = defaultUpstream;
+ }
+ selectorData.setHandle(GsonUtils.getInstance().toJson(upstreamList));
+
+ DivideRuleHandle divideRuleHandle = new DivideRuleHandle();
+ if (annotations != null) {
+ divideRuleHandle.setLoadBalance(annotations.getOrDefault(IngressConstants.LOADBALANCER_ANNOTATION_KEY, LoadBalanceEnum.RANDOM.getName()));
+ divideRuleHandle.setRetry(Integer.parseInt(annotations.getOrDefault(IngressConstants.RETRY_ANNOTATION_KEY, "3")));
+ divideRuleHandle.setTimeout(Long.parseLong(annotations.getOrDefault(IngressConstants.TIMEOUT_ANNOTATION_KEY, "3000")));
+ divideRuleHandle.setHeaderMaxSize(Long.parseLong(annotations.getOrDefault(IngressConstants.HEADER_MAX_SIZE_ANNOTATION_KEY, "10240")));
+ divideRuleHandle.setRequestMaxSize(Long.parseLong(annotations.getOrDefault(IngressConstants.REQUEST_MAX_SIZE_ANNOTATION_KEY, "102400")));
+ }
+ RuleData ruleData = RuleData.builder()
+ .name(path.getPath())
+ .pluginName(PluginEnum.DIVIDE.getName())
+ .matchMode(MatchModeEnum.AND.getCode())
+ .conditionDataList(conditionList)
+ .handle(GsonUtils.getInstance().toJson(divideRuleHandle))
+ .loged(false)
+ .enabled(true).build();
+
+ res.add(Pair.of(selectorData, ruleData));
+ }
+ }
+ }
+ return res;
+ }
+
+ private String parsePort(final V1IngressServiceBackend service) {
+ if (service.getPort() != null) {
+ if (service.getPort().getNumber() != null && service.getPort().getNumber() > 0) {
+ return String.valueOf(service.getPort().getNumber());
+ } else if (service.getPort().getName() != null && !"".equals(service.getPort().getName().trim())) {
+ return service.getPort().getName().trim();
+ }
+ }
+ return null;
+ }
+
+ private List<DivideUpstream> parseUpstream(final V1IngressBackend backend, final String namespace) {
+ List<DivideUpstream> upstreamList = new ArrayList<>();
+ if (backend != null && backend.getService() != null && backend.getService().getName() != null) {
+ String serviceName = backend.getService().getName();
+ // shenyu routes directly to the container
+ V1Endpoints v1Endpoints = endpointsLister.namespace(namespace).get(serviceName);
+ List<V1EndpointSubset> subsets = v1Endpoints.getSubsets();
+ if (subsets == null || subsets.isEmpty()) {
+ LOG.info("Endpoints {} do not have subsets", serviceName);
+ } else {
+ for (V1EndpointSubset subset : subsets) {
+ List<V1EndpointAddress> addresses = subset.getAddresses();
+ if (addresses == null || addresses.isEmpty()) {
+ continue;
+ }
+ for (V1EndpointAddress address : addresses) {
+ String upstreamIp = address.getIp();
+ String defaultPort = parsePort(backend.getService());
+ if (defaultPort != null) {
+ DivideUpstream upstream = new DivideUpstream();
+ upstream.setUpstreamUrl(upstreamIp + ":" + defaultPort);
+ upstream.setWeight(100);
+ // TODO support config protocol in annotation
+ upstream.setProtocol("http://");
+ upstream.setWarmup(0);
+ upstream.setStatus(true);
+ upstream.setUpstreamHost("");
+ upstreamList.add(upstream);
+ }
+ }
+ }
+ }
+ }
+ return upstreamList;
+ }
+
+ private Pair<SelectorData, RuleData> getDefaultRouteConfig(final List<DivideUpstream> divideUpstream, final Map<String, String> annotations) {
+ final ConditionData conditionData = new ConditionData();
+ conditionData.setParamName("default");
+ conditionData.setParamType(ParamTypeEnum.URI.getName());
+ conditionData.setOperator(OperatorEnum.PATH_PATTERN.getAlias());
+ conditionData.setParamValue("/**");
+
+ final SelectorData selectorData = SelectorData.builder()
+ .name("default-selector")
+ .sort(Integer.MAX_VALUE)
+ .conditionList(Collections.singletonList(conditionData))
+ .handle(GsonUtils.getInstance().toJson(divideUpstream))
+ .enabled(true)
+ .id("1")
+ .pluginName(PluginEnum.DIVIDE.getName())
+ .pluginId(String.valueOf(PluginEnum.DIVIDE.getCode()))
+ .logged(false)
+ .continued(true)
+ .matchMode(MatchModeEnum.AND.getCode())
+ .type(SelectorTypeEnum.FULL_FLOW.getCode()).build();
+
+ DivideRuleHandle divideRuleHandle = new DivideRuleHandle();
+ // TODO need an annotation parsing common way
+ if (annotations != null) {
+ divideRuleHandle.setLoadBalance(annotations.getOrDefault(IngressConstants.LOADBALANCER_ANNOTATION_KEY, LoadBalanceEnum.RANDOM.getName()));
+ divideRuleHandle.setRetry(Integer.parseInt(annotations.getOrDefault(IngressConstants.RETRY_ANNOTATION_KEY, "3")));
+ divideRuleHandle.setTimeout(Long.parseLong(annotations.getOrDefault(IngressConstants.TIMEOUT_ANNOTATION_KEY, "3000")));
+ divideRuleHandle.setHeaderMaxSize(Long.parseLong(annotations.getOrDefault(IngressConstants.HEADER_MAX_SIZE_ANNOTATION_KEY, "10240")));
+ divideRuleHandle.setRequestMaxSize(Long.parseLong(annotations.getOrDefault(IngressConstants.REQUEST_MAX_SIZE_ANNOTATION_KEY, "102400")));
+ }
+ final RuleData ruleData = RuleData.builder()
+ .selectorId("1")
+ .pluginName(PluginEnum.DIVIDE.getName())
+ .name("default-rule")
+ .matchMode(MatchModeEnum.AND.getCode())
+ .conditionDataList(Collections.singletonList(conditionData))
+ .handle(GsonUtils.getInstance().toJson(divideRuleHandle))
+ .loged(false)
+ .enabled(true)
+ .sort(Integer.MAX_VALUE).build();
+
+ return Pair.of(selectorData, ruleData);
+ }
+}
diff --git a/shenyu-kubernetes-controller/src/main/java/org/apache/shenyu/k8s/parser/K8sResourceParser.java b/shenyu-kubernetes-controller/src/main/java/org/apache/shenyu/k8s/parser/K8sResourceParser.java
new file mode 100644
index 000000000..5e541aa3b
--- /dev/null
+++ b/shenyu-kubernetes-controller/src/main/java/org/apache/shenyu/k8s/parser/K8sResourceParser.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.shenyu.k8s.parser;
+
+import io.kubernetes.client.openapi.apis.CoreV1Api;
+import org.apache.shenyu.k8s.common.ShenyuMemoryConfig;
+
+/**
+ * Parser of Kubernetes resource.
+ * Such as ingress, gateway, or even custom api resource.
+ *
+ * @param <T> resource type
+ */
+public interface K8sResourceParser<T> {
+
+ /**
+ * Parse resource to ShenyuMemoryConfig.
+ *
+ * @param resource resource
+ * @param coreV1Api coreV1Api
+ * @return ShenyuMemoryConfig
+ */
+ ShenyuMemoryConfig parse(T resource, CoreV1Api coreV1Api);
+}
diff --git a/shenyu-kubernetes-controller/src/main/java/org/apache/shenyu/k8s/reconciler/EndpointsReconciler.java b/shenyu-kubernetes-controller/src/main/java/org/apache/shenyu/k8s/reconciler/EndpointsReconciler.java
new file mode 100644
index 000000000..d96cea67c
--- /dev/null
+++ b/shenyu-kubernetes-controller/src/main/java/org/apache/shenyu/k8s/reconciler/EndpointsReconciler.java
@@ -0,0 +1,171 @@
+/*
+ * 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.shenyu.k8s.reconciler;
+
+import io.kubernetes.client.extended.controller.reconciler.Reconciler;
+import io.kubernetes.client.extended.controller.reconciler.Request;
+import io.kubernetes.client.extended.controller.reconciler.Result;
+import io.kubernetes.client.informer.SharedIndexInformer;
+import io.kubernetes.client.informer.cache.Lister;
+import io.kubernetes.client.openapi.ApiClient;
+import io.kubernetes.client.openapi.models.CoreV1EndpointPort;
+import io.kubernetes.client.openapi.models.V1EndpointSubset;
+import io.kubernetes.client.openapi.models.V1EndpointAddress;
+import io.kubernetes.client.openapi.models.V1Endpoints;
+import io.kubernetes.client.openapi.models.V1Ingress;
+import org.apache.commons.lang3.tuple.Pair;
+import org.apache.shenyu.common.dto.SelectorData;
+import org.apache.shenyu.common.dto.convert.selector.DivideUpstream;
+import org.apache.shenyu.common.enums.PluginEnum;
+import org.apache.shenyu.common.exception.ShenyuException;
+import org.apache.shenyu.common.utils.GsonUtils;
+import org.apache.shenyu.k8s.cache.IngressSelectorCache;
+import org.apache.shenyu.k8s.cache.ServiceIngressCache;
+import org.apache.shenyu.k8s.repository.ShenyuCacheRepository;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * The Reconciler of Endpoints.
+ */
+public class EndpointsReconciler implements Reconciler {
+
+ private static final Logger LOG = LoggerFactory.getLogger(EndpointsReconciler.class);
+
+ private final Lister<V1Ingress> ingressLister;
+
+ private final Lister<V1Endpoints> endpointsLister;
+
+ private final ShenyuCacheRepository shenyuCacheRepository;
+
+ private final ApiClient apiClient;
+
+ /**
+ * EndpointsReconciler Constructor.
+ *
+ * @param ingressInformer ingressInformer
+ * @param endpointsInformer endpointsInformer
+ * @param shenyuCacheRepository shenyuCacheRepository
+ * @param apiClient apiClient
+ */
+ public EndpointsReconciler(final SharedIndexInformer<V1Ingress> ingressInformer,
+ final SharedIndexInformer<V1Endpoints> endpointsInformer,
+ final ShenyuCacheRepository shenyuCacheRepository,
+ final ApiClient apiClient) {
+ this.ingressLister = new Lister<>(ingressInformer.getIndexer());
+ this.endpointsLister = new Lister<>(endpointsInformer.getIndexer());
+ this.shenyuCacheRepository = shenyuCacheRepository;
+ this.apiClient = apiClient;
+ }
+
+ /**
+ * Reconcile cycle.
+ *
+ * @param request request
+ * @return reconcile result
+ */
+ @Override
+ public Result reconcile(final Request request) {
+ List<Pair<String, String>> ingressList = ServiceIngressCache.getInstance().getIngressName(request.getNamespace(), request.getName());
+ if (ingressList == null || ingressList.isEmpty()) {
+ return new Result(false);
+ }
+
+ V1Endpoints v1Endpoints = endpointsLister.namespace(request.getNamespace()).get(request.getName());
+ if (v1Endpoints == null) {
+ // The deletion event is not processed, because deleting all upstreams in the Selector has
+ // the same effect as not deleting them, and they cannot be accessed
+ LOG.info("Cannot find endpoints {}", request);
+ return new Result(false);
+ }
+
+ // 1. Obtain upstream according to endpoints
+ List<DivideUpstream> upstreamList = getUpstreamFromEndpoints(v1Endpoints);
+
+ // 2. Update the handler of the selector
+ List<SelectorData> totalSelectors = shenyuCacheRepository.findSelectorDataList(PluginEnum.DIVIDE.getName());
+ Set<String> needUpdateSelectorId = new HashSet<>();
+ ingressList.forEach(item -> {
+ List<String> selectorIdList = IngressSelectorCache.getInstance().get(item.getLeft(), item.getRight(), PluginEnum.DIVIDE.getName());
+ needUpdateSelectorId.addAll(selectorIdList);
+ });
+ totalSelectors.forEach(selectorData -> {
+ if (needUpdateSelectorId.contains(selectorData.getId())) {
+ SelectorData newSelectorData = SelectorData.builder().id(selectorData.getId())
+ .pluginId(selectorData.getPluginId())
+ .pluginName(selectorData.getPluginName())
+ .name(selectorData.getName())
+ .matchMode(selectorData.getMatchMode())
+ .type(selectorData.getType())
+ .sort(selectorData.getSort())
+ .enabled(selectorData.getEnabled())
+ .logged(selectorData.getLogged())
+ .continued(selectorData.getContinued())
+ .handle(GsonUtils.getInstance().toJson(upstreamList))
+ .conditionList(selectorData.getConditionList())
+ .matchRestful(selectorData.getMatchRestful()).build();
+ shenyuCacheRepository.saveOrUpdateSelectorData(newSelectorData);
+ }
+ });
+ LOG.info("Update selector for endpoint {}", request);
+
+ return new Result(false);
+ }
+
+ private List<DivideUpstream> getUpstreamFromEndpoints(final V1Endpoints v1Endpoints) {
+ List<DivideUpstream> res = new ArrayList<>();
+ if (v1Endpoints.getSubsets() != null) {
+ for (V1EndpointSubset subset : v1Endpoints.getSubsets()) {
+ List<CoreV1EndpointPort> ports = subset.getPorts();
+ if (ports == null || ports.isEmpty() || subset.getAddresses() == null || subset.getAddresses().isEmpty()) {
+ continue;
+ }
+ CoreV1EndpointPort endpointPort = ports.stream()
+ .filter(coreV1EndpointPort -> "TCP".equals(coreV1EndpointPort.getProtocol()))
+ .findFirst()
+ .orElseThrow(() -> new ShenyuException("Can't find port from endpoints"));
+ String port = null;
+ if (endpointPort.getPort() > 0) {
+ port = String.valueOf(endpointPort.getPort());
+ } else if (endpointPort.getName() != null) {
+ port = endpointPort.getName();
+ }
+ for (V1EndpointAddress address : subset.getAddresses()) {
+ String ip = address.getIp();
+ if (ip != null) {
+ DivideUpstream upstream = new DivideUpstream();
+ upstream.setUpstreamUrl(ip + ":" + port);
+ upstream.setWeight(100);
+ // TODO support config protocol in annotation
+ upstream.setProtocol("http://");
+ upstream.setWarmup(0);
+ upstream.setStatus(true);
+ upstream.setUpstreamHost("");
+ res.add(upstream);
+ }
+ }
+ }
+ }
+ return res;
+ }
+}
diff --git a/shenyu-kubernetes-controller/src/main/java/org/apache/shenyu/k8s/reconciler/IngressReconciler.java b/shenyu-kubernetes-controller/src/main/java/org/apache/shenyu/k8s/reconciler/IngressReconciler.java
new file mode 100644
index 000000000..27b286854
--- /dev/null
+++ b/shenyu-kubernetes-controller/src/main/java/org/apache/shenyu/k8s/reconciler/IngressReconciler.java
@@ -0,0 +1,406 @@
+/*
+ * 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.shenyu.k8s.reconciler;
+
+import io.kubernetes.client.extended.controller.reconciler.Reconciler;
+import io.kubernetes.client.extended.controller.reconciler.Request;
+import io.kubernetes.client.extended.controller.reconciler.Result;
+import io.kubernetes.client.informer.SharedIndexInformer;
+import io.kubernetes.client.informer.cache.Lister;
+import io.kubernetes.client.openapi.ApiClient;
+import io.kubernetes.client.openapi.apis.CoreV1Api;
+import io.kubernetes.client.openapi.models.V1HTTPIngressPath;
+import io.kubernetes.client.openapi.models.V1Ingress;
+import io.kubernetes.client.openapi.models.V1IngressRule;
+import io.kubernetes.client.openapi.models.V1Secret;
+import io.kubernetes.client.openapi.models.V1IngressBuilder;
+import org.apache.commons.lang3.tuple.Pair;
+import org.apache.shenyu.common.config.ssl.ShenyuSniAsyncMapping;
+import org.apache.shenyu.common.config.ssl.SslCrtAndKeyStream;
+import org.apache.shenyu.common.dto.PluginData;
+import org.apache.shenyu.common.dto.RuleData;
+import org.apache.shenyu.common.dto.SelectorData;
+import org.apache.shenyu.common.enums.PluginEnum;
+import org.apache.shenyu.common.enums.PluginRoleEnum;
+import org.apache.shenyu.k8s.cache.IngressCache;
+import org.apache.shenyu.k8s.cache.IngressSecretCache;
+import org.apache.shenyu.k8s.cache.IngressSelectorCache;
+import org.apache.shenyu.k8s.cache.ServiceIngressCache;
+import org.apache.shenyu.k8s.common.IngressConstants;
+import org.apache.shenyu.k8s.common.ShenyuMemoryConfig;
+import org.apache.shenyu.k8s.parser.IngressParser;
+import org.apache.shenyu.k8s.repository.ShenyuCacheRepository;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.Optional;
+
+/**
+ * The Reconciler of Ingress.
+ */
+public class IngressReconciler implements Reconciler {
+
+ private static final Logger LOG = LoggerFactory.getLogger(IngressReconciler.class);
+
+ // ingressName serviceName selectorData ruleData
+ private static Pair<Pair<String, String>, Pair<SelectorData, RuleData>> globalDefaultBackend;
+
+ private final Lister<V1Ingress> ingressLister;
+
+ private final Lister<V1Secret> secretLister;
+
+ private final ShenyuCacheRepository shenyuCacheRepository;
+
+ private final ShenyuSniAsyncMapping shenyuSniAsyncMapping;
+
+ private final IngressParser ingressParser;
+
+ private final ApiClient apiClient;
+
+ /**
+ * IngressReconciler Constructor.
+ *
+ * @param ingressInformer ingressInformer
+ * @param secretInformer secretInformer
+ * @param shenyuCacheRepository shenyuCacheRepository
+ * @param shenyuSniAsyncMapping shenyuSniAsyncMapping
+ * @param ingressParser ingressParser
+ * @param apiClient apiClient
+ */
+ public IngressReconciler(final SharedIndexInformer<V1Ingress> ingressInformer,
+ final SharedIndexInformer<V1Secret> secretInformer,
+ final ShenyuCacheRepository shenyuCacheRepository,
+ final ShenyuSniAsyncMapping shenyuSniAsyncMapping,
+ final IngressParser ingressParser,
+ final ApiClient apiClient) {
+ this.ingressLister = new Lister<>(ingressInformer.getIndexer());
+ this.secretLister = new Lister<>(secretInformer.getIndexer());
+ this.shenyuCacheRepository = shenyuCacheRepository;
+ this.shenyuSniAsyncMapping = shenyuSniAsyncMapping;
+ this.ingressParser = ingressParser;
+ this.apiClient = apiClient;
+ initPlugins(shenyuCacheRepository);
+ }
+
+ /**
+ * Reconcile cycle.
+ *
+ * @param request request
+ * @return reconcile result
+ */
+ @Override
+ public Result reconcile(final Request request) {
+ LOG.info("Starting to reconcile ingress {}", request);
+
+ // Do not modify current ingress object directly
+ final V1Ingress v1Ingress = this.ingressLister.namespace(request.getNamespace()).get(request.getName());
+ final V1Ingress oldIngress = IngressCache.getInstance().get(request.getNamespace(), request.getName());
+ if (v1Ingress == null) {
+ if (oldIngress != null) {
+ // Delete ingress binding selectors
+ doDeleteConfigByIngress(request, oldIngress);
+
+ // Remove ssl config
+ Set<String> sslDomainSet = IngressSecretCache.getInstance().getDomainByIngress(request.getNamespace(), request.getName());
+ if (sslDomainSet != null && !sslDomainSet.isEmpty()) {
+ for (String sslDomain : sslDomainSet) {
+ Integer preDomainSslNums = IngressSecretCache.getInstance().getAndDecrementDomainNums(sslDomain);
+ if (preDomainSslNums == 1) {
+ shenyuSniAsyncMapping.removeSslCertificate(sslDomain);
+ LOG.info("Remove ssl config for domain {}", sslDomain);
+ }
+ }
+ }
+ IngressSecretCache.getInstance().removeDomainByIngress(request.getNamespace(), request.getName());
+
+ IngressCache.getInstance().remove(request.getNamespace(), request.getName());
+ LOG.info("Delete selector and rule for ingress {}", request);
+ } else {
+ LOG.info("Cannot find ingress {}", request);
+ }
+ return new Result(false);
+ }
+
+ if (!checkIngressClass(v1Ingress)) {
+ LOG.info("IngressClass is not match {}", request);
+ return new Result(false);
+ }
+
+ if (oldIngress == null) {
+ try {
+ addNewIngressConfigToShenyu(v1Ingress, new CoreV1Api(apiClient));
+ } catch (IOException e) {
+ LOG.error("add new ingress config error", e);
+ }
+ } else if (needUpdate(oldIngress, v1Ingress)) {
+ // Update logic
+ // 1. clean old config
+ doDeleteConfigByIngress(request, oldIngress);
+
+ // 2. add new config
+ try {
+ addNewIngressConfigToShenyu(v1Ingress, new CoreV1Api(apiClient));
+ } catch (IOException e) {
+ LOG.error("add new ingress config error", e);
+ }
+ }
+ IngressCache.getInstance().put(request.getNamespace(), request.getName(), v1Ingress);
+ List<Pair<String, String>> serviceList = parseServiceFromIngress(v1Ingress);
+ Objects.requireNonNull(serviceList).forEach(pair -> {
+ ServiceIngressCache.getInstance().putIngressName(pair.getLeft(), pair.getRight(), request.getNamespace(), request.getName());
+ LOG.info("Add service cache {} for ingress {}", pair.getLeft() + "/" + pair.getRight(), request.getNamespace() + "/" + request.getName());
+ });
+
+ return new Result(false);
+ }
+
+ private void doDeleteConfigByIngress(final Request request, final V1Ingress oldIngress) {
+ List<String> selectorList = deleteSelectorByIngressName(request.getNamespace(), request.getName(), PluginEnum.DIVIDE.getName());
+ if (selectorList != null && !selectorList.isEmpty()) {
+ IngressSelectorCache.getInstance().remove(request.getNamespace(), request.getName(), PluginEnum.DIVIDE.getName());
+ }
+ List<Pair<String, String>> serviceList = parseServiceFromIngress(oldIngress);
+ Objects.requireNonNull(serviceList).forEach(pair -> {
+ ServiceIngressCache.getInstance().removeSpecifiedIngressName(pair.getLeft(), pair.getRight(), request.getNamespace(), request.getName());
+ LOG.info("Delete service cache {} for ingress {}", pair.getLeft() + "/" + pair.getRight(), request.getNamespace() + "/" + request.getName());
+ });
+ deleteGlobalDefaultBackend(request.getNamespace(), request.getName());
+ }
+
+ private void deleteGlobalDefaultBackend(final String namespace, final String name) {
+ if (globalDefaultBackend != null && (namespace + "/" + name).equals(globalDefaultBackend.getLeft().getLeft())) {
+ globalDefaultBackend = null;
+ }
+ }
+
+ private void initPlugins(final ShenyuCacheRepository shenyuCacheRepository) {
+ //GLOBAL
+ PluginData globalPlugin = PluginData.builder()
+ .id(String.valueOf(PluginEnum.GLOBAL.getCode()))
+ .name(PluginEnum.GLOBAL.getName())
+ .config("")
+ .role(PluginRoleEnum.SYS.getName())
+ .enabled(true)
+ .sort(PluginEnum.GLOBAL.getCode())
+ .build();
+ shenyuCacheRepository.saveOrUpdatePluginData(globalPlugin);
+ //uri
+ PluginData uriPlugin = PluginData.builder()
+ .id(String.valueOf(PluginEnum.URI.getCode()))
+ .name(PluginEnum.URI.getName())
+ .config("")
+ .role(PluginRoleEnum.SYS.getName())
+ .enabled(true)
+ .sort(PluginEnum.URI.getCode())
+ .build();
+ shenyuCacheRepository.saveOrUpdatePluginData(uriPlugin);
+ //nettyHttpClient
+ PluginData webclientPlugin = PluginData.builder()
+ .id(String.valueOf(PluginEnum.NETTY_HTTP_CLIENT.getCode()))
+ .config("")
+ .name(PluginEnum.NETTY_HTTP_CLIENT.getName())
+ .role(PluginRoleEnum.SYS.getName())
+ .enabled(true)
+ .sort(PluginEnum.NETTY_HTTP_CLIENT.getCode())
+ .build();
+ shenyuCacheRepository.saveOrUpdatePluginData(webclientPlugin);
+ //divide
+ PluginData dividePlugin = PluginData.builder()
+ .id(String.valueOf(PluginEnum.DIVIDE.getCode()))
+ .name(PluginEnum.DIVIDE.getName())
+ .config("{multiSelectorHandle: 1, multiRuleHandle:0}")
+ .role(PluginRoleEnum.SYS.getName())
+ .enabled(true)
+ .sort(PluginEnum.DIVIDE.getCode())
+ .build();
+ shenyuCacheRepository.saveOrUpdatePluginData(dividePlugin);
+ //GeneralContextPlugin
+ PluginData generalContextPlugin = PluginData.builder()
+ .id(String.valueOf(PluginEnum.GENERAL_CONTEXT.getCode()))
+ .config("")
+ .name(PluginEnum.GENERAL_CONTEXT.getName())
+ .role(PluginRoleEnum.SYS.getName())
+ .enabled(true)
+ .sort(PluginEnum.GENERAL_CONTEXT.getCode())
+ .build();
+ shenyuCacheRepository.saveOrUpdatePluginData(generalContextPlugin);
+ }
+
+ /**
+ * Check whether the IngressClass is shenyu, check the annotation first.
+ *
+ * @param v1Ingress v1Ingress
+ * @return boolean
+ */
+ private boolean checkIngressClass(final V1Ingress v1Ingress) {
+ if (v1Ingress.getMetadata() != null) {
+ Map<String, String> annotations = v1Ingress.getMetadata().getAnnotations();
+ if (annotations != null
+ && annotations.get(IngressConstants.K8S_INGRESS_CLASS_ANNOTATION_KEY) != null) {
+ return IngressConstants.SHENYU_INGRESS_CLASS.equals(annotations.get(IngressConstants.K8S_INGRESS_CLASS_ANNOTATION_KEY));
+ } else {
+ return v1Ingress.getSpec() != null && IngressConstants.SHENYU_INGRESS_CLASS.equals(v1Ingress.getSpec().getIngressClassName());
+ }
+ } else {
+ return false;
+ }
+ }
+
+ private List<String> deleteSelectorByIngressName(final String namespace, final String name, final String pluginName) {
+ final List<String> selectorList = IngressSelectorCache.getInstance().get(namespace, name, pluginName);
+ if (selectorList != null && !selectorList.isEmpty()) {
+ for (String selectorId : selectorList) {
+ List<RuleData> ruleList = shenyuCacheRepository.findRuleDataList(selectorId);
+ // To avoid ConcurrentModificationException, copy the ruleId to list
+ List<String> ruleIdList = new ArrayList<>();
+ ruleList.forEach(rule -> ruleIdList.add(rule.getId()));
+ for (String id : ruleIdList) {
+ shenyuCacheRepository.deleteRuleData(pluginName, selectorId, id);
+ }
+ shenyuCacheRepository.deleteSelectorData(pluginName, selectorId);
+ }
+ }
+ return selectorList;
+ }
+
+ private List<Pair<String, String>> parseServiceFromIngress(final V1Ingress ingress) {
+ List<Pair<String, String>> res = new ArrayList<>();
+ if (ingress == null || ingress.getSpec() == null) {
+ return res;
+ }
+ String namespace = Objects.requireNonNull(ingress.getMetadata()).getNamespace();
+ String name = ingress.getMetadata().getName();
+ String namespacedName = namespace + "/" + name;
+ String defaultService = null;
+ if (ingress.getSpec().getDefaultBackend() != null && ingress.getSpec().getDefaultBackend().getService() != null) {
+ defaultService = ingress.getSpec().getDefaultBackend().getService().getName();
+ if (ingress.getSpec().getRules() == null) {
+ if (globalDefaultBackend != null) {
+ if (globalDefaultBackend.getLeft().getLeft().equals(namespacedName)) {
+ res.add(Pair.of(namespace, defaultService));
+ }
+ } else {
+ res.add(Pair.of(namespace, defaultService));
+ }
+ return res;
+ }
+ }
+ Set<String> deduplicateSet = new HashSet<>();
+ if (ingress.getSpec().getRules() == null) {
+ return res;
+ }
+ for (V1IngressRule rule : ingress.getSpec().getRules()) {
+ if (rule.getHttp() != null && rule.getHttp().getPaths() != null) {
+ for (V1HTTPIngressPath path : rule.getHttp().getPaths()) {
+ if (path.getBackend() != null && path.getBackend().getService() != null) {
+ if (!deduplicateSet.contains(path.getBackend().getService().getName())) {
+ res.add(Pair.of(namespace, path.getBackend().getService().getName()));
+ deduplicateSet.add(path.getBackend().getService().getName());
+ }
+ } else {
+ if (defaultService != null && !deduplicateSet.contains(defaultService)) {
+ res.add(Pair.of(namespace, defaultService));
+ deduplicateSet.add(defaultService);
+ }
+ }
+ }
+ }
+ }
+ return res;
+ }
+
+ private boolean needUpdate(final V1Ingress oldIngress, final V1Ingress currentIngress) {
+ return !oldIngress.equals(currentIngress);
+ }
+
+ private void addNewIngressConfigToShenyu(final V1Ingress v1Ingress, final CoreV1Api apiClient) throws IOException {
+ V1Ingress ingressCopy = new V1IngressBuilder(v1Ingress).build();
+ ShenyuMemoryConfig shenyuMemoryConfig = ingressParser.parse(ingressCopy, apiClient);
+ if (shenyuMemoryConfig != null) {
+ List<Pair<SelectorData, RuleData>> routeConfigList = shenyuMemoryConfig.getRouteConfigList();
+ List<SslCrtAndKeyStream> tlsConfigList = shenyuMemoryConfig.getTlsConfigList();
+
+ if (routeConfigList != null) {
+ routeConfigList.forEach(routeConfig -> {
+ SelectorData selectorData = routeConfig.getLeft();
+ RuleData ruleData = routeConfig.getRight();
+ if (selectorData != null) {
+ selectorData.setId(IngressSelectorCache.getInstance().generateSelectorId());
+ selectorData.setSort(100);
+ shenyuCacheRepository.saveOrUpdateSelectorData(selectorData);
+ if (ruleData != null) {
+ ruleData.setId(selectorData.getId());
+ ruleData.setSelectorId(selectorData.getId());
+ ruleData.setSort(100);
+ shenyuCacheRepository.saveOrUpdateRuleData(ruleData);
+ IngressSelectorCache.getInstance().put(Objects.requireNonNull(v1Ingress.getMetadata()).getNamespace(),
+ v1Ingress.getMetadata().getName(), PluginEnum.DIVIDE.getName(), selectorData.getId());
+ } else {
+ shenyuCacheRepository.deleteSelectorData(selectorData.getPluginName(), selectorData.getId());
+ }
+ }
+ });
+ }
+
+ if (shenyuMemoryConfig.getGlobalDefaultBackend() != null) {
+ synchronized (IngressReconciler.class) {
+ if (globalDefaultBackend == null) {
+ // Add a default backend
+ shenyuCacheRepository.saveOrUpdateSelectorData(shenyuMemoryConfig.getGlobalDefaultBackend().getRight().getLeft());
+ shenyuCacheRepository.saveOrUpdateRuleData(shenyuMemoryConfig.getGlobalDefaultBackend().getRight().getRight());
+ globalDefaultBackend = shenyuMemoryConfig.getGlobalDefaultBackend();
+ IngressSelectorCache.getInstance().put(Objects.requireNonNull(v1Ingress.getMetadata()).getNamespace(),
+ v1Ingress.getMetadata().getName(), PluginEnum.DIVIDE.getName(), shenyuMemoryConfig.getGlobalDefaultBackend().getRight().getLeft().getId());
+ }
+ }
+ }
+
+ if (tlsConfigList != null) {
+ final String namespace = Objects.requireNonNull(v1Ingress.getMetadata()).getNamespace();
+ final String ingressName = v1Ingress.getMetadata().getName();
+ Set<String> oldDomainSet = Optional.ofNullable(IngressSecretCache.getInstance().removeDomainByIngress(namespace, ingressName)).orElse(new HashSet<>());
+ Set<String> newDomainSet = new HashSet<>();
+ for (SslCrtAndKeyStream sslCrtAndKeyStream : tlsConfigList) {
+ final String domain = sslCrtAndKeyStream.getDomain();
+ if (!oldDomainSet.contains(domain)) {
+ if (IngressSecretCache.getInstance().getAndIncrementDomainNums(domain) == 0) {
+ shenyuSniAsyncMapping.addSslCertificate(sslCrtAndKeyStream);
+ LOG.info("Add ssl config for domain {}", domain);
+ }
+ }
+ newDomainSet.add(domain);
+ }
+ oldDomainSet.removeAll(newDomainSet);
+ oldDomainSet.forEach(domain -> {
+ if (IngressSecretCache.getInstance().getAndDecrementDomainNums(domain) == 1) {
+ shenyuSniAsyncMapping.removeSslCertificate(domain);
+ LOG.info("Remove ssl config for domain {}", domain);
+ }
+ });
+ IngressSecretCache.getInstance().putDomainByIngress(namespace, ingressName, newDomainSet);
+ }
+ }
+ }
+}
diff --git a/shenyu-kubernetes-controller/src/main/java/org/apache/shenyu/k8s/repository/ShenyuCacheRepository.java b/shenyu-kubernetes-controller/src/main/java/org/apache/shenyu/k8s/repository/ShenyuCacheRepository.java
new file mode 100644
index 000000000..d5c7ac456
--- /dev/null
+++ b/shenyu-kubernetes-controller/src/main/java/org/apache/shenyu/k8s/repository/ShenyuCacheRepository.java
@@ -0,0 +1,135 @@
+/*
+ * 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.shenyu.k8s.repository;
+
+import org.apache.shenyu.common.dto.PluginData;
+import org.apache.shenyu.common.dto.RuleData;
+import org.apache.shenyu.common.dto.SelectorData;
+import org.apache.shenyu.plugin.base.cache.BaseDataCache;
+import org.apache.shenyu.sync.data.api.PluginDataSubscriber;
+
+import java.util.List;
+
+/**
+ * The repository to config shenyu.
+ *
+ * <p>Should try to avoid directly operating memory configuration through PluginDataSubscriber
+ * in ingress-controller, but use ShenyuCacheRepository.
+ * This will make it easier for us if we have architectural changes to the ingress-controller.
+ * </p>
+ */
+public class ShenyuCacheRepository {
+
+ private final PluginDataSubscriber subscriber;
+
+ /**
+ * Shenyu Cache Repository Constructor.
+ *
+ * @param subscriber PluginDataSubscriber
+ */
+ public ShenyuCacheRepository(final PluginDataSubscriber subscriber) {
+ this.subscriber = subscriber;
+ }
+
+ /**
+ * Find PluginData by plugin name.
+ *
+ * @param pluginName plugin name
+ * @return PluginData
+ */
+ public PluginData findPluginData(final String pluginName) {
+ return BaseDataCache.getInstance().obtainPluginData(pluginName);
+ }
+
+ /**
+ * Save or update PluginData by PluginData.
+ *
+ * @param pluginData PluginData
+ */
+ public void saveOrUpdatePluginData(final PluginData pluginData) {
+ subscriber.onSubscribe(pluginData);
+ }
+
+ /**
+ * Delete PluginData by plugin name.
+ *
+ * @param pluginName plugin name
+ */
+ public void deletePluginData(final String pluginName) {
+ subscriber.unSubscribe(PluginData.builder().name(pluginName).build());
+ }
+
+ /**
+ * Find SelectorData list by pluginName.
+ *
+ * @param pluginName plugin name
+ * @return SelectorData list
+ */
+ public List<SelectorData> findSelectorDataList(final String pluginName) {
+ return BaseDataCache.getInstance().obtainSelectorData(pluginName);
+ }
+
+ /**
+ * Save or update SelectorData by SelectorData.
+ *
+ * @param selectorData SelectorData
+ */
+ public void saveOrUpdateSelectorData(final SelectorData selectorData) {
+ subscriber.onSelectorSubscribe(selectorData);
+ }
+
+ /**
+ * Delete SelectorData by plugin name and selector id.
+ *
+ * @param pluginName plugin name
+ * @param selectorId selector id
+ */
+ public void deleteSelectorData(final String pluginName, final String selectorId) {
+ subscriber.unSelectorSubscribe(SelectorData.builder().pluginName(pluginName).id(selectorId).build());
+ }
+
+ /**
+ * Find RuleData list by selector id.
+ *
+ * @param selectorId selector id
+ * @return RuleData list
+ */
+ public List<RuleData> findRuleDataList(final String selectorId) {
+ return BaseDataCache.getInstance().obtainRuleData(selectorId);
+ }
+
+ /**
+ * Save or update RuleData by RuleData.
+ *
+ * @param ruleData RuleData
+ */
+ public void saveOrUpdateRuleData(final RuleData ruleData) {
+ subscriber.onRuleSubscribe(ruleData);
+ }
+
+ /**
+ * Delete RuleData by plugin name, selector id and rule id.
+ *
+ * @param pluginName plugin name
+ * @param selectorId selector id
+ * @param ruleId rule id
+ */
+ public void deleteRuleData(final String pluginName, final String selectorId, final String ruleId) {
+ subscriber.unRuleSubscribe(RuleData.builder().pluginName(pluginName).selectorId(selectorId).id(ruleId).build());
+ }
+}
diff --git a/shenyu-plugin/shenyu-plugin-base/src/main/java/org/apache/shenyu/plugin/base/trie/ShenyuTrie.java b/shenyu-plugin/shenyu-plugin-base/src/main/java/org/apache/shenyu/plugin/base/trie/ShenyuTrie.java
index 80cedf0c8..dd8aef30d 100644
--- a/shenyu-plugin/shenyu-plugin-base/src/main/java/org/apache/shenyu/plugin/base/trie/ShenyuTrie.java
+++ b/shenyu-plugin/shenyu-plugin-base/src/main/java/org/apache/shenyu/plugin/base/trie/ShenyuTrie.java
@@ -274,7 +274,9 @@ public class ShenyuTrie {
String[] parentPathArray = Arrays.copyOfRange(pathParts, 0, pathParts.length - 1);
String parentPath = String.join("/", parentPathArray);
ShenyuTrieNode parentNode = this.getNode(parentPath);
- parentNode.getChildren().remove(key);
+ if (parentNode != null) {
+ parentNode.getChildren().remove(key);
+ }
}
}
}
diff --git a/shenyu-spring-boot-starter/pom.xml b/shenyu-spring-boot-starter/pom.xml
index 071894ae9..4150dcc15 100644
--- a/shenyu-spring-boot-starter/pom.xml
+++ b/shenyu-spring-boot-starter/pom.xml
@@ -33,6 +33,7 @@
<module>shenyu-spring-boot-starter-client</module>
<module>shenyu-spring-boot-starter-instance</module>
<module>shenyu-spring-boot-starter-sdk</module>
+ <module>shenyu-spring-boot-starter-k8s</module>
</modules>
<dependencies>
diff --git a/shenyu-spring-boot-starter/shenyu-spring-boot-starter-gateway/src/main/java/org/apache/shenyu/springboot/starter/netty/ShenyuNettyWebServerConfiguration.java b/shenyu-spring-boot-starter/shenyu-spring-boot-starter-gateway/src/main/java/org/apache/shenyu/springboot/starter/netty/ShenyuNettyWebServerConfiguration.java
index ce1a1683b..8d2cb692c 100644
--- a/shenyu-spring-boot-starter/shenyu-spring-boot-starter-gateway/src/main/java/org/apache/shenyu/springboot/starter/netty/ShenyuNettyWebServerConfiguration.java
+++ b/shenyu-spring-boot-starter/shenyu-spring-boot-starter-gateway/src/main/java/org/apache/shenyu/springboot/starter/netty/ShenyuNettyWebServerConfiguration.java
@@ -19,6 +19,12 @@ package org.apache.shenyu.springboot.starter.netty;
import io.netty.channel.ChannelOption;
import io.netty.channel.WriteBufferWaterMark;
+import org.apache.shenyu.common.config.NettyHttpProperties;
+import org.apache.shenyu.common.exception.ShenyuException;
+import org.apache.shenyu.common.config.ssl.ShenyuSniAsyncMapping;
+import org.apache.shenyu.common.config.ssl.SslCrtAndKeyFile;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.ConfigurationProperties;
@@ -28,14 +34,23 @@ import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import reactor.netty.http.server.HttpServer;
import reactor.netty.resources.LoopResources;
+import reactor.netty.tcp.SslProvider;
+import reactor.netty.tcp.TcpSslContextSpec;
+import java.io.File;
+import java.io.IOException;
+import java.util.List;
+import java.util.Objects;
import java.util.Optional;
+import java.util.function.Function;
/**
* The type shenyu netty web server factory.
*/
@Configuration
public class ShenyuNettyWebServerConfiguration {
+
+ private static final Logger LOG = LoggerFactory.getLogger(ShenyuNettyWebServerConfiguration.class);
/**
* Netty tcp config.
@@ -47,38 +62,96 @@ public class ShenyuNettyWebServerConfiguration {
public NettyHttpProperties nettyTcpProperties() {
return new NettyHttpProperties();
}
+
+ /**
+ * AsyncMapping for dynamic configure ssl.
+ *
+ * @return ShenyuSniAsyncMapping
+ */
+ @Bean
+ @ConditionalOnProperty(value = {"shenyu.netty.http.web-server-factory-enabled", "shenyu.netty.http.sni.enabled"}, havingValue = "true")
+ public ShenyuSniAsyncMapping shenyuSniAsyncMapping() {
+ return new ShenyuSniAsyncMapping();
+ }
/**
* Netty reactive web server factory.
*
* @param properties the properties
+ * @param shenyuSniAsyncMappingProvider shenyuSniAsyncMapping
+ * @param tcpSslContextSpecs default tcpSslContextSpecs
* @return the netty reactive web server factory
*/
@Bean
@ConditionalOnProperty(value = "shenyu.netty.http.web-server-factory-enabled", havingValue = "true", matchIfMissing = true)
- public NettyReactiveWebServerFactory nettyReactiveWebServerFactory(final ObjectProvider<NettyHttpProperties> properties) {
+ public NettyReactiveWebServerFactory nettyReactiveWebServerFactory(final ObjectProvider<NettyHttpProperties> properties,
+ final ObjectProvider<ShenyuSniAsyncMapping> shenyuSniAsyncMappingProvider,
+ final ObjectProvider<TcpSslContextSpec> tcpSslContextSpecs) {
NettyReactiveWebServerFactory webServerFactory = new NettyReactiveWebServerFactory();
NettyHttpProperties nettyHttpProperties = Optional.ofNullable(properties.getIfAvailable()).orElse(new NettyHttpProperties());
- webServerFactory.addServerCustomizers(new EventLoopNettyCustomizer(nettyHttpProperties));
+ webServerFactory.addServerCustomizers(new EventLoopNettyCustomizer(nettyHttpProperties, httpServer -> {
+ // Configure sni certificates
+ NettyHttpProperties.SniProperties sniProperties = nettyHttpProperties.getSni();
+ if (sniProperties.getEnabled()) {
+ ShenyuSniAsyncMapping shenyuSniAsyncMapping = shenyuSniAsyncMappingProvider.getIfAvailable();
+ if (shenyuSniAsyncMapping == null) {
+ throw new ShenyuException("Can not find shenyuSniAsyncMapping bean");
+ }
+ if ("manual".equals(sniProperties.getMod())) {
+ if (sniProperties.getCertificates() == null || sniProperties.getCertificates().isEmpty()) {
+ throw new ShenyuException("At least one certificate is required");
+ }
+
+ // Use the first certificate as the default certificate (this default certificate will not actually be used)
+ List<SslCrtAndKeyFile> certificates = sniProperties.getCertificates();
+ for (SslCrtAndKeyFile certificate : certificates) {
+ try {
+ shenyuSniAsyncMapping.addSslCertificate(certificate);
+ } catch (IOException e) {
+ LOG.error("add certificate error", e);
+ }
+ }
+
+ SslCrtAndKeyFile defaultCert = certificates.get(0);
+ TcpSslContextSpec defaultSpec = TcpSslContextSpec.forServer(new File(defaultCert.getKeyCertChainFile()),
+ new File(defaultCert.getKeyFile()));
+
+ httpServer = httpServer.secure(spec -> spec.sslContext(defaultSpec)
+ .setSniAsyncMappings(shenyuSniAsyncMapping), false);
+ } else if ("k8s".equals(sniProperties.getMod())) {
+ TcpSslContextSpec defaultSpec = Objects.requireNonNull(tcpSslContextSpecs.getIfAvailable());
+ httpServer = httpServer.secure(spec -> spec.sslContext(defaultSpec)
+ .setSniAsyncMappings(shenyuSniAsyncMapping), false);
+ shenyuSniAsyncMapping.addSslProvider("shenyu-default", SslProvider.builder().sslContext(defaultSpec).build());
+ } else {
+ throw new ShenyuException("Cannot read the sni mod");
+ }
+ }
+ return httpServer;
+ }));
return webServerFactory;
}
private static class EventLoopNettyCustomizer implements NettyServerCustomizer {
private final NettyHttpProperties nettyHttpProperties;
+
+ private final Function<HttpServer, HttpServer> sniProcessor;
/**
* Instantiates a new Event loop netty customizer.
*
* @param nettyHttpProperties the netty tcp config
*/
- EventLoopNettyCustomizer(final NettyHttpProperties nettyHttpProperties) {
+ EventLoopNettyCustomizer(final NettyHttpProperties nettyHttpProperties, final Function<HttpServer, HttpServer> sniProcessor) {
this.nettyHttpProperties = nettyHttpProperties;
+ this.sniProcessor = sniProcessor;
}
@Override
public HttpServer apply(final HttpServer httpServer) {
- return httpServer.runOn(LoopResources.create("shenyu-netty", nettyHttpProperties.getSelectCount(), nettyHttpProperties.getWorkerCount(), true))
+ return sniProcessor.apply(httpServer)
+ .runOn(LoopResources.create("shenyu-netty", nettyHttpProperties.getSelectCount(), nettyHttpProperties.getWorkerCount(), true))
.accessLog(nettyHttpProperties.getAccessLog())
// server socket channel parameters
.option(ChannelOption.SO_BACKLOG, nettyHttpProperties.getServerSocketChannel().getSoBacklog())
diff --git a/shenyu-spring-boot-starter/shenyu-spring-boot-starter-gateway/src/test/java/org/apache/shenyu/springboot/starter/netty/ShenyuNettyWebServerConfigurationTest.java b/shenyu-spring-boot-starter/shenyu-spring-boot-starter-gateway/src/test/java/org/apache/shenyu/springboot/starter/netty/ShenyuNettyWebServerConfigurationTest.java
index e8395bec9..634d4a1db 100644
--- a/shenyu-spring-boot-starter/shenyu-spring-boot-starter-gateway/src/test/java/org/apache/shenyu/springboot/starter/netty/ShenyuNettyWebServerConfigurationTest.java
+++ b/shenyu-spring-boot-starter/shenyu-spring-boot-starter-gateway/src/test/java/org/apache/shenyu/springboot/starter/netty/ShenyuNettyWebServerConfigurationTest.java
@@ -17,6 +17,7 @@
package org.apache.shenyu.springboot.starter.netty;
+import org.apache.shenyu.common.config.NettyHttpProperties;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.boot.autoconfigure.AutoConfigurations;
diff --git a/shenyu-spring-boot-starter/pom.xml b/shenyu-spring-boot-starter/shenyu-spring-boot-starter-k8s/pom.xml
similarity index 57%
copy from shenyu-spring-boot-starter/pom.xml
copy to shenyu-spring-boot-starter/shenyu-spring-boot-starter-k8s/pom.xml
index 071894ae9..933002d3f 100644
--- a/shenyu-spring-boot-starter/pom.xml
+++ b/shenyu-spring-boot-starter/shenyu-spring-boot-starter-k8s/pom.xml
@@ -16,31 +16,27 @@
~ limitations under the License.
-->
-<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<groupId>org.apache.shenyu</groupId>
- <artifactId>shenyu</artifactId>
+ <artifactId>shenyu-spring-boot-starter</artifactId>
<version>2.6.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
- <artifactId>shenyu-spring-boot-starter</artifactId>
- <packaging>pom</packaging>
-
- <modules>
- <module>shenyu-spring-boot-starter-gateway</module>
- <module>shenyu-spring-boot-starter-plugin</module>
- <module>shenyu-spring-boot-starter-sync-data-center</module>
- <module>shenyu-spring-boot-starter-client</module>
- <module>shenyu-spring-boot-starter-instance</module>
- <module>shenyu-spring-boot-starter-sdk</module>
- </modules>
+ <artifactId>shenyu-spring-boot-starter-k8s</artifactId>
<dependencies>
+ <dependency>
+ <groupId>org.apache.shenyu</groupId>
+ <artifactId>shenyu-kubernetes-controller</artifactId>
+ <version>${project.version}</version>
+ </dependency>
+
<dependency>
<groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-configuration-processor</artifactId>
- <optional>true</optional>
+ <artifactId>spring-boot-starter</artifactId>
</dependency>
</dependencies>
-
-</project>
+</project>
\ No newline at end of file
diff --git a/shenyu-spring-boot-starter/shenyu-spring-boot-starter-k8s/src/main/java/org/apache/shenyu/springboot/starter/k8s/IngressControllerConfiguration.java b/shenyu-spring-boot-starter/shenyu-spring-boot-starter-k8s/src/main/java/org/apache/shenyu/springboot/starter/k8s/IngressControllerConfiguration.java
new file mode 100644
index 000000000..e54a42638
--- /dev/null
+++ b/shenyu-spring-boot-starter/shenyu-spring-boot-starter-k8s/src/main/java/org/apache/shenyu/springboot/starter/k8s/IngressControllerConfiguration.java
@@ -0,0 +1,264 @@
+/*
+ * 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.shenyu.springboot.starter.k8s;
+
+import io.kubernetes.client.extended.controller.Controller;
+import io.kubernetes.client.extended.controller.ControllerManager;
+import io.kubernetes.client.extended.controller.builder.ControllerBuilder;
+import io.kubernetes.client.extended.controller.builder.DefaultControllerBuilder;
+import io.kubernetes.client.informer.SharedIndexInformer;
+import io.kubernetes.client.informer.SharedInformerFactory;
+import io.kubernetes.client.openapi.ApiClient;
+import io.kubernetes.client.openapi.ApiException;
+import io.kubernetes.client.openapi.apis.CoreV1Api;
+import io.kubernetes.client.openapi.models.V1Endpoints;
+import io.kubernetes.client.openapi.models.V1Ingress;
+import io.kubernetes.client.openapi.models.V1Secret;
+import io.kubernetes.client.openapi.models.V1Service;
+import io.kubernetes.client.openapi.models.V1SecretList;
+import io.kubernetes.client.openapi.models.V1ServiceList;
+import io.kubernetes.client.openapi.models.V1IngressList;
+import io.kubernetes.client.openapi.models.V1EndpointsList;
+import io.kubernetes.client.util.generic.GenericKubernetesApi;
+import org.apache.shenyu.common.config.NettyHttpProperties;
+import org.apache.shenyu.common.config.ssl.ShenyuSniAsyncMapping;
+import org.apache.shenyu.common.exception.ShenyuException;
+import org.apache.shenyu.k8s.parser.IngressParser;
+import org.apache.shenyu.k8s.reconciler.EndpointsReconciler;
+import org.apache.shenyu.k8s.reconciler.IngressReconciler;
+import org.apache.shenyu.k8s.repository.ShenyuCacheRepository;
+import org.apache.shenyu.sync.data.api.PluginDataSubscriber;
+import org.springframework.beans.factory.ObjectProvider;
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import reactor.netty.tcp.TcpSslContextSpec;
+
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
+import java.time.Duration;
+import java.util.Optional;
+import java.util.concurrent.Executors;
+
+/**
+ * The type shenyu ingress controller configuration.
+ */
+@Configuration
+public class IngressControllerConfiguration {
+
+ /**
+ * Controller Manager.
+ *
+ * @param sharedInformerFactory sharedInformerFactory
+ * @param ingressController ingressController
+ * @param endpointsController endpointsController
+ * @return Controller Manager
+ */
+ @Bean("controller-manager")
+ public ControllerManager controllerManager(final SharedInformerFactory sharedInformerFactory,
+ @Qualifier("ingress-controller") final Controller ingressController,
+ @Qualifier("endpoints-controller") final Controller endpointsController) {
+ ControllerManager controllerManager = new ControllerManager(sharedInformerFactory, ingressController, endpointsController);
+ Executors.newSingleThreadExecutor().submit(controllerManager);
+ return controllerManager;
+ }
+
+ /**
+ * Ingress Controller.
+ *
+ * @param sharedInformerFactory sharedInformerFactory
+ * @param ingressReconciler ingressReconciler
+ * @return Ingress Controller
+ */
+ @Bean("ingress-controller")
+ public Controller ingressController(final SharedInformerFactory sharedInformerFactory, final IngressReconciler ingressReconciler) {
+ DefaultControllerBuilder builder = ControllerBuilder.defaultBuilder(sharedInformerFactory);
+ builder = builder.watch(q -> ControllerBuilder.controllerWatchBuilder(V1Ingress.class, q)
+ .withResyncPeriod(Duration.ofMinutes(1))
+ .build());
+ // TODO support config in application.yaml
+ builder.withWorkerCount(2);
+// builder.withReadyFunc(ingressReconciler::informerReady);
+ return builder.withReconciler(ingressReconciler).withName("ingressController").build();
+ }
+
+ /**
+ * Ingress Reconciler.
+ *
+ * @param ingressInformer ingress shared informer
+ * @param secretInformer secret shared informer
+ * @param shenyuCacheRepository ShenyuCacheRepository
+ * @param shenyuSniAsyncMappingProvider shenyuSniAsyncMappingProvider
+ * @param ingressParser IngressParser
+ * @param apiClient ApiClient
+ * @return Ingress Reconciler
+ */
+ @Bean
+ public IngressReconciler ingressReconciler(final SharedIndexInformer<V1Ingress> ingressInformer,
+ final SharedIndexInformer<V1Secret> secretInformer,
+ final ShenyuCacheRepository shenyuCacheRepository,
+ final ObjectProvider<ShenyuSniAsyncMapping> shenyuSniAsyncMappingProvider,
+ final IngressParser ingressParser,
+ final ApiClient apiClient) {
+ ShenyuSniAsyncMapping shenyuSniAsyncMapping = Optional.ofNullable(shenyuSniAsyncMappingProvider.getIfAvailable()).orElse(new ShenyuSniAsyncMapping());
+ return new IngressReconciler(ingressInformer, secretInformer, shenyuCacheRepository, shenyuSniAsyncMapping, ingressParser, apiClient);
+ }
+
+ /**
+ * Endpoints Controller.
+ *
+ * @param sharedInformerFactory sharedInformerFactory
+ * @param endpointsReconciler endpointsReconciler
+ * @return Endpoints Controller
+ */
+ @Bean("endpoints-controller")
+ public Controller endpointsController(final SharedInformerFactory sharedInformerFactory, final EndpointsReconciler endpointsReconciler) {
+ DefaultControllerBuilder builder = ControllerBuilder.defaultBuilder(sharedInformerFactory);
+ builder = builder.watch(q -> ControllerBuilder.controllerWatchBuilder(V1Endpoints.class, q)
+ .withResyncPeriod(Duration.ofMinutes(1))
+ .build());
+ builder.withWorkerCount(2);
+ return builder.withReconciler(endpointsReconciler).withName("ingressController").build();
+ }
+
+ /**
+ * EndpointsReconciler.
+ *
+ * @param ingressInformer ingressInformer
+ * @param endpointsInformer endpointsInformer
+ * @param shenyuCacheRepository shenyuCacheRepository
+ * @param apiClient apiClient
+ * @return EndpointsReconciler
+ */
+ @Bean
+ public EndpointsReconciler endpointsReconciler(final SharedIndexInformer<V1Ingress> ingressInformer,
+ final SharedIndexInformer<V1Endpoints> endpointsInformer,
+ final ShenyuCacheRepository shenyuCacheRepository,
+ final ApiClient apiClient) {
+ return new EndpointsReconciler(ingressInformer, endpointsInformer, shenyuCacheRepository, apiClient);
+ }
+
+ /**
+ * ShenyuCacheRepository.
+ *
+ * @param subscriber PluginDataSubscriber
+ * @return ShenyuCacheRepository
+ */
+ @Bean
+ public ShenyuCacheRepository shenyuCacheRepository(final PluginDataSubscriber subscriber) {
+ return new ShenyuCacheRepository(subscriber);
+ }
+
+ /**
+ * IngressParser.
+ *
+ * @param serviceInformer serviceInformer
+ * @param endpointsInformer endpointsInformer
+ * @return IngressParser
+ */
+ @Bean
+ public IngressParser ingressParser(final SharedIndexInformer<V1Service> serviceInformer, final SharedIndexInformer<V1Endpoints> endpointsInformer) {
+ return new IngressParser(serviceInformer, endpointsInformer);
+ }
+
+ /**
+ * ServiceInformer.
+ *
+ * @param apiClient apiClient
+ * @param sharedInformerFactory sharedInformerFactory
+ * @return serviceInformer
+ */
+ @Bean
+ public SharedIndexInformer<V1Service> serviceInformer(final ApiClient apiClient, final SharedInformerFactory sharedInformerFactory) {
+ GenericKubernetesApi<V1Service, V1ServiceList> genericApi = new GenericKubernetesApi<>(V1Service.class,
+ V1ServiceList.class, "", "v1", "services", apiClient);
+ return sharedInformerFactory.sharedIndexInformerFor(genericApi, V1Service.class, 0);
+ }
+
+ /**
+ * EndpointsInformer.
+ *
+ * @param apiClient apiClient
+ * @param sharedInformerFactory sharedInformerFactory
+ * @return endpointsInformer
+ */
+ @Bean
+ public SharedIndexInformer<V1Endpoints> endpointsInformer(final ApiClient apiClient, final SharedInformerFactory sharedInformerFactory) {
+ GenericKubernetesApi<V1Endpoints, V1EndpointsList> genericApi = new GenericKubernetesApi<>(V1Endpoints.class,
+ V1EndpointsList.class, "", "v1", "endpoints", apiClient);
+ return sharedInformerFactory.sharedIndexInformerFor(genericApi, V1Endpoints.class, 0);
+ }
+
+ /**
+ * SecretInformer.
+ *
+ * @param apiClient apiClient
+ * @param sharedInformerFactory sharedInformerFactory
+ * @return secretInformer
+ */
+ @Bean
+ public SharedIndexInformer<V1Secret> secretInformer(final ApiClient apiClient, final SharedInformerFactory sharedInformerFactory) {
+ GenericKubernetesApi<V1Secret, V1SecretList> genericApi = new GenericKubernetesApi<>(V1Secret.class,
+ V1SecretList.class, "", "v1", "secrets", apiClient);
+ return sharedInformerFactory.sharedIndexInformerFor(genericApi, V1Secret.class, 0);
+ }
+
+ /**
+ * IngressInformer.
+ *
+ * @param apiClient apiClient
+ * @param sharedInformerFactory sharedInformerFactory
+ * @return ingressInformer
+ */
+ @Bean
+ public SharedIndexInformer<V1Ingress> ingressInformer(final ApiClient apiClient, final SharedInformerFactory sharedInformerFactory) {
+ GenericKubernetesApi<V1Ingress, V1IngressList> genericApi = new GenericKubernetesApi<>(V1Ingress.class,
+ V1IngressList.class, "networking.k8s.io", "v1", "ingresses", apiClient);
+ return sharedInformerFactory.sharedIndexInformerFor(genericApi, V1Ingress.class, 0);
+ }
+
+ /**
+ * TcpSslContextSpec.
+ *
+ * @param properties NettyHttpProperties
+ * @param apiClient ApiClient
+ * @return TcpSslContextSpec
+ * @throws ApiException the exception when use apiClient directly
+ */
+ @Bean
+ @ConditionalOnProperty(value = {"shenyu.netty.http.web-server-factory-enabled", "shenyu.netty.http.sni.enabled"}, havingValue = "true")
+ public TcpSslContextSpec tcpSslContextSpec(final ObjectProvider<NettyHttpProperties> properties, final ApiClient apiClient) throws ApiException {
+ NettyHttpProperties nettyHttpProperties = Optional.ofNullable(properties.getIfAvailable()).orElse(new NettyHttpProperties());
+ NettyHttpProperties.SniProperties sniProperties = nettyHttpProperties.getSni();
+ if (sniProperties != null && sniProperties.getEnabled() && "k8s".equals(sniProperties.getMod())) {
+ String defaultName = Optional.ofNullable(sniProperties.getDefaultK8sSecretName()).orElse("default-ingress-crt");
+ String defaultNamespace = Optional.ofNullable(sniProperties.getDefaultK8sSecretNamespace()).orElse("default");
+ CoreV1Api coreV1Api = new CoreV1Api(apiClient);
+ V1Secret secret = coreV1Api.readNamespacedSecret(defaultName, defaultNamespace, "true");
+ if (secret.getData() != null) {
+ InputStream crtStream = new ByteArrayInputStream(secret.getData().get("tls.crt"));
+ InputStream keyStream = new ByteArrayInputStream(secret.getData().get("tls.key"));
+ return TcpSslContextSpec.forServer(crtStream, keyStream);
+ } else {
+ throw new ShenyuException(String.format("Can not read cert and key from default secret %s/%s", defaultNamespace, defaultName));
+ }
+ }
+ return TcpSslContextSpec.forServer(new ByteArrayInputStream(new byte[]{}), new ByteArrayInputStream(new byte[]{}));
+ }
+}
diff --git a/shenyu-spring-boot-starter/shenyu-spring-boot-starter-k8s/src/main/resources/META-INF/spring.factories b/shenyu-spring-boot-starter/shenyu-spring-boot-starter-k8s/src/main/resources/META-INF/spring.factories
new file mode 100644
index 000000000..6899440f0
--- /dev/null
+++ b/shenyu-spring-boot-starter/shenyu-spring-boot-starter-k8s/src/main/resources/META-INF/spring.factories
@@ -0,0 +1,3 @@
+# Auto Configure
+org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
+org.apache.shenyu.springboot.starter.k8s.IngressControllerConfiguration
diff --git a/shenyu-spring-boot-starter/shenyu-spring-boot-starter-k8s/src/main/resources/META-INF/spring.provides b/shenyu-spring-boot-starter/shenyu-spring-boot-starter-k8s/src/main/resources/META-INF/spring.provides
new file mode 100644
index 000000000..db2bf2607
--- /dev/null
+++ b/shenyu-spring-boot-starter/shenyu-spring-boot-starter-k8s/src/main/resources/META-INF/spring.provides
@@ -0,0 +1 @@
+provides: shenyu-spring-boot-starter-ingress
\ No newline at end of file
diff --git a/shenyu-web/src/main/java/org/apache/shenyu/web/configuration/ShenyuThreadPoolConfiguration.java b/shenyu-web/src/main/java/org/apache/shenyu/web/configuration/ShenyuThreadPoolConfiguration.java
index 6e9c48e90..aeb567aa8 100644
--- a/shenyu-web/src/main/java/org/apache/shenyu/web/configuration/ShenyuThreadPoolConfiguration.java
+++ b/shenyu-web/src/main/java/org/apache/shenyu/web/configuration/ShenyuThreadPoolConfiguration.java
@@ -28,6 +28,7 @@ import org.apache.shenyu.common.constant.Constants;
import org.apache.shenyu.common.exception.ShenyuException;
import org.apache.shenyu.plugin.api.utils.SpringBeanUtils;
import org.springframework.beans.factory.ObjectProvider;
+import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
@@ -54,6 +55,7 @@ public class ShenyuThreadPoolConfiguration {
*/
@Bean
@ConditionalOnMissingBean(TaskQueue.class)
+ @Qualifier("taskQueue")
@ConditionalOnProperty("shenyu.shared-pool.max-work-queue-memory")
public TaskQueue<Runnable> memoryLimitedTaskQueue(final ShenyuConfig shenyuConfig) {
final Instrumentation instrumentation = ByteBuddyAgent.install();
@@ -73,6 +75,7 @@ public class ShenyuThreadPoolConfiguration {
*/
@Bean
@ConditionalOnMissingBean(TaskQueue.class)
+ @Qualifier("taskQueue")
@ConditionalOnProperty("shenyu.shared-pool.max-free-memory")
public TaskQueue<Runnable> memorySafeTaskQueue(final ShenyuConfig shenyuConfig) {
final ShenyuConfig.SharedPool sharedPool = shenyuConfig.getSharedPool();
@@ -93,7 +96,7 @@ public class ShenyuThreadPoolConfiguration {
@Bean
@ConditionalOnProperty(name = "shenyu.shared-pool.enable", havingValue = "true", matchIfMissing = true)
public ShenyuThreadPoolExecutor shenyuThreadPoolExecutor(final ShenyuConfig shenyuConfig,
- final ObjectProvider<TaskQueue<Runnable>> provider) {
+ final @Qualifier("taskQueue") ObjectProvider<TaskQueue<Runnable>> provider) {
final ShenyuConfig.SharedPool sharedPool = shenyuConfig.getSharedPool();
final Integer corePoolSize = sharedPool.getCorePoolSize();
final Integer maximumPoolSize = sharedPool.getMaximumPoolSize();