From 37cafe4e9f79c1a8edab09aaf925991d2ccbbac2 Mon Sep 17 00:00:00 2001 From: Freeman Lau Date: Sat, 28 Jan 2023 23:28:43 +0800 Subject: [PATCH 01/37] add kubernetes module --- spring-cloud-alibaba-dependencies/pom.xml | 12 ++++++++ spring-cloud-alibaba-starters/pom.xml | 1 + .../spring-cloud-alibaba-kubernetes/pom.xml | 21 ++++++++++++++ .../pom.xml | 28 +++++++++++++++++++ .../pom.xml | 27 ++++++++++++++++++ 5 files changed, 89 insertions(+) create mode 100644 spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/pom.xml create mode 100644 spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-alibaba-kubernetes-commons/pom.xml create mode 100644 spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/pom.xml diff --git a/spring-cloud-alibaba-dependencies/pom.xml b/spring-cloud-alibaba-dependencies/pom.xml index 06ac146704..f651310e0a 100644 --- a/spring-cloud-alibaba-dependencies/pom.xml +++ b/spring-cloud-alibaba-dependencies/pom.xml @@ -228,6 +228,18 @@ ${revision} + + com.alibaba.cloud + spring-cloud-alibaba-kubernetes-commons + ${revision} + + + + com.alibaba.cloud + spring-cloud-starter-alibaba-kubernetes-config + ${revision} + + com.alibaba.spring spring-context-support diff --git a/spring-cloud-alibaba-starters/pom.xml b/spring-cloud-alibaba-starters/pom.xml index 0476ad9635..2d4c8bed3a 100644 --- a/spring-cloud-alibaba-starters/pom.xml +++ b/spring-cloud-alibaba-starters/pom.xml @@ -26,6 +26,7 @@ spring-cloud-alibaba-sentinel-datasource spring-cloud-alibaba-sentinel-gateway spring-cloud-alibaba-commons + spring-cloud-alibaba-kubernetes diff --git a/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/pom.xml b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/pom.xml new file mode 100644 index 0000000000..9f7b848ab8 --- /dev/null +++ b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/pom.xml @@ -0,0 +1,21 @@ + + 4.0.0 + + + com.alibaba.cloud + spring-cloud-alibaba-starters + ${revision} + ../pom.xml + + + spring-cloud-alibaba-kubernetes + pom + Spring Cloud Alibaba Kubernetes + Spring Cloud Alibaba Kubernetes + + + spring-cloud-alibaba-kubernetes-commons + spring-cloud-starter-alibaba-kubernetes-config + + \ No newline at end of file diff --git a/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-alibaba-kubernetes-commons/pom.xml b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-alibaba-kubernetes-commons/pom.xml new file mode 100644 index 0000000000..1d70862d25 --- /dev/null +++ b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-alibaba-kubernetes-commons/pom.xml @@ -0,0 +1,28 @@ + + 4.0.0 + + + com.alibaba.cloud + spring-cloud-alibaba-kubernetes + ${revision} + ../pom.xml + + + spring-cloud-alibaba-kubernetes-commons + Spring Cloud Alibaba Kubernetes Commons + Spring Cloud Alibaba Kubernetes Commons + + + + org.springframework.cloud + spring-cloud-starter + true + + + io.fabric8 + kubernetes-client + + + + \ No newline at end of file diff --git a/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/pom.xml b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/pom.xml new file mode 100644 index 0000000000..3553a472c7 --- /dev/null +++ b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/pom.xml @@ -0,0 +1,27 @@ + + 4.0.0 + + + com.alibaba.cloud + spring-cloud-alibaba-kubernetes + ${revision} + ../pom.xml + + + spring-cloud-starter-alibaba-kubernetes-config + Spring Cloud Starter Alibaba Kubernetes Config + Spring Cloud Starter Alibaba Kubernetes Config + + + + com.alibaba.cloud + spring-cloud-alibaba-kubernetes-commons + + + org.springframework.cloud + spring-cloud-starter + + + + \ No newline at end of file From 7a09dda7bafe90194514652de8903bc8ca460925 Mon Sep 17 00:00:00 2001 From: Freeman Lau Date: Sun, 29 Jan 2023 23:59:39 +0800 Subject: [PATCH 02/37] keep going, almost done --- .../src/main/asciidoc-zh/sentinel.adoc | 2 +- .../src/main/asciidoc/sentinel.adoc | 2 +- .../pom.xml | 4 +- .../KubernetesClientConfiguration.java | 58 +++ .../commons/KubernetesClientHolder.java | 53 +++ .../kubernetes/commons/KubernetesUtils.java | 51 +++ .../pom.xml | 14 +- .../KubernetesConfigAutoConfiguration.java | 46 ++ .../config/KubernetesConfigProperties.java | 399 ++++++++++++++++++ .../core/ConfigEnvironmentPostProcessor.java | 220 ++++++++++ .../kubernetes/config/core/ConfigWatcher.java | 123 ++++++ .../core/HasMetadataResourceEventHandler.java | 105 +++++ .../config/core/SinglePairPropertySource.java | 43 ++ .../exception/ConfigMissingException.java | 45 ++ .../ConfigMissingFailureAnalyzer.java | 39 ++ .../config/processor/FileProcessor.java | 42 ++ .../config/processor/JsonFileProcessor.java | 108 +++++ .../processor/PropertiesFileProcessor.java | 60 +++ .../config/processor/YamlFileProcessor.java | 61 +++ .../config/util/ConfigPreference.java | 31 ++ .../kubernetes/config/util/Converters.java | 140 ++++++ .../cloud/kubernetes/config/util/Pair.java | 42 ++ .../kubernetes/config/util/Processors.java | 43 ++ .../config/util/RefreshContext.java | 85 ++++ .../kubernetes/config/util/ResourceKey.java | 80 ++++ .../cloud/kubernetes/config/util/Util.java | 82 ++++ .../main/resources/META-INF/spring.factories | 8 + .../cloud/kubernetes/config/Empty.java | 28 ++ .../config/MissingConfigIntegrationTests.java | 49 +++ .../config/NormalIntegrationTests.java | 86 ++++ .../NotRefreshableIntegrationTests.java | 89 ++++ .../config/SecretIntegrationTests.java | 88 ++++ .../processor/JsonFileProcessorTest.java | 71 ++++ .../PropertiesFileProcessorTest.java | 46 ++ .../processor/YamlFileProcessorTest.java | 64 +++ .../testsupport/KubernetesAvailable.java | 37 ++ .../KubernetesAvailableCondition.java | 50 +++ .../testsupport/KubernetesTestUtil.java | 75 ++++ .../config/util/ConvertersTest.java | 37 ++ .../resources/application-missing-config.yml | 12 + .../src/test/resources/application-normal.yml | 17 + .../resources/application-not-refreshable.yml | 15 + .../src/test/resources/application-secret.yml | 16 + .../resources/normal/configmap-changed.yaml | 13 + .../src/test/resources/normal/configmap.yaml | 12 + .../not_refreshable/configmap-01-changed.yaml | 9 + .../not_refreshable/configmap-01.yaml | 9 + .../not_refreshable/configmap-02-changed.yaml | 11 + .../not_refreshable/configmap-02.yaml | 10 + .../src/test/resources/secret/configmap.yaml | 12 + .../test/resources/secret/secret-changed.yaml | 9 + .../secret/secret-refreshable-changed.yaml | 8 + .../resources/secret/secret-refreshable.yaml | 8 + .../src/test/resources/secret/secret.yaml | 9 + 54 files changed, 2870 insertions(+), 6 deletions(-) create mode 100644 spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-alibaba-kubernetes-commons/src/main/java/com/alibaba/cloud/kubernetes/commons/KubernetesClientConfiguration.java create mode 100644 spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-alibaba-kubernetes-commons/src/main/java/com/alibaba/cloud/kubernetes/commons/KubernetesClientHolder.java create mode 100644 spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-alibaba-kubernetes-commons/src/main/java/com/alibaba/cloud/kubernetes/commons/KubernetesUtils.java create mode 100644 spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/KubernetesConfigAutoConfiguration.java create mode 100644 spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/KubernetesConfigProperties.java create mode 100644 spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/core/ConfigEnvironmentPostProcessor.java create mode 100644 spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/core/ConfigWatcher.java create mode 100644 spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/core/HasMetadataResourceEventHandler.java create mode 100644 spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/core/SinglePairPropertySource.java create mode 100644 spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/exception/ConfigMissingException.java create mode 100644 spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/exception/ConfigMissingFailureAnalyzer.java create mode 100644 spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/processor/FileProcessor.java create mode 100644 spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/processor/JsonFileProcessor.java create mode 100644 spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/processor/PropertiesFileProcessor.java create mode 100644 spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/processor/YamlFileProcessor.java create mode 100644 spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/util/ConfigPreference.java create mode 100644 spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/util/Converters.java create mode 100644 spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/util/Pair.java create mode 100644 spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/util/Processors.java create mode 100644 spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/util/RefreshContext.java create mode 100644 spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/util/ResourceKey.java create mode 100644 spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/util/Util.java create mode 100644 spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/resources/META-INF/spring.factories create mode 100644 spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/test/java/com/alibaba/cloud/kubernetes/config/Empty.java create mode 100644 spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/test/java/com/alibaba/cloud/kubernetes/config/MissingConfigIntegrationTests.java create mode 100644 spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/test/java/com/alibaba/cloud/kubernetes/config/NormalIntegrationTests.java create mode 100644 spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/test/java/com/alibaba/cloud/kubernetes/config/NotRefreshableIntegrationTests.java create mode 100644 spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/test/java/com/alibaba/cloud/kubernetes/config/SecretIntegrationTests.java create mode 100644 spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/test/java/com/alibaba/cloud/kubernetes/config/processor/JsonFileProcessorTest.java create mode 100644 spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/test/java/com/alibaba/cloud/kubernetes/config/processor/PropertiesFileProcessorTest.java create mode 100644 spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/test/java/com/alibaba/cloud/kubernetes/config/processor/YamlFileProcessorTest.java create mode 100644 spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/test/java/com/alibaba/cloud/kubernetes/config/testsupport/KubernetesAvailable.java create mode 100644 spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/test/java/com/alibaba/cloud/kubernetes/config/testsupport/KubernetesAvailableCondition.java create mode 100644 spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/test/java/com/alibaba/cloud/kubernetes/config/testsupport/KubernetesTestUtil.java create mode 100644 spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/test/java/com/alibaba/cloud/kubernetes/config/util/ConvertersTest.java create mode 100644 spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/test/resources/application-missing-config.yml create mode 100644 spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/test/resources/application-normal.yml create mode 100644 spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/test/resources/application-not-refreshable.yml create mode 100644 spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/test/resources/application-secret.yml create mode 100644 spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/test/resources/normal/configmap-changed.yaml create mode 100644 spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/test/resources/normal/configmap.yaml create mode 100644 spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/test/resources/not_refreshable/configmap-01-changed.yaml create mode 100644 spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/test/resources/not_refreshable/configmap-01.yaml create mode 100644 spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/test/resources/not_refreshable/configmap-02-changed.yaml create mode 100644 spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/test/resources/not_refreshable/configmap-02.yaml create mode 100644 spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/test/resources/secret/configmap.yaml create mode 100644 spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/test/resources/secret/secret-changed.yaml create mode 100644 spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/test/resources/secret/secret-refreshable-changed.yaml create mode 100644 spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/test/resources/secret/secret-refreshable.yaml create mode 100644 spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/test/resources/secret/secret.yaml diff --git a/spring-cloud-alibaba-docs/src/main/asciidoc-zh/sentinel.adoc b/spring-cloud-alibaba-docs/src/main/asciidoc-zh/sentinel.adoc index 2a45f6d134..a29105a13e 100644 --- a/spring-cloud-alibaba-docs/src/main/asciidoc-zh/sentinel.adoc +++ b/spring-cloud-alibaba-docs/src/main/asciidoc-zh/sentinel.adoc @@ -240,7 +240,7 @@ spring.cloud.sentinel.datasource.ds4.apollo.rule-type=param-flow 这种配置方式参考了 Spring Cloud Stream Binder 的配置,内部使用了 `TreeMap` 进行存储,comparator 为 `String.CASE_INSENSITIVE_ORDER` 。 -NOTE: d1, ds2, ds3, ds4 是 `ReadableDataSource` 的名字,可随意编写。后面的 `file` ,`zk` ,`nacos` , `apollo` 就是对应具体的数据源。 它们后面的配置就是这些具体数据源各自的配置。 +NOTE: d1, ds2, ds3, ds4 是 `ReadableDataSource` 的名字,可随意编写。后面的 `processor` ,`zk` ,`nacos` , `apollo` 就是对应具体的数据源。 它们后面的配置就是这些具体数据源各自的配置。 每种数据源都有两个共同的配置项: `data-type`、 `converter-class` 以及 `rule-type`。 diff --git a/spring-cloud-alibaba-docs/src/main/asciidoc/sentinel.adoc b/spring-cloud-alibaba-docs/src/main/asciidoc/sentinel.adoc index 0dfac80140..645787a5b1 100644 --- a/spring-cloud-alibaba-docs/src/main/asciidoc/sentinel.adoc +++ b/spring-cloud-alibaba-docs/src/main/asciidoc/sentinel.adoc @@ -243,7 +243,7 @@ spring.cloud.sentinel.datasource.ds4.apollo.rule-type=param-flow This method follows the configuration of Spring Cloud Stream Binder. `TreeMap` is used for storage internally, and comparator is `String.CASE_INSENSITIVE_ORDER`. -NOTE: d1, ds2, ds3, ds4 are the names of `ReadableDataSource`, and can be coded as you like. The `file`, `zk`, `nacos` , `apollo` refer to the specific data sources. The configurations following them are the specific configurations of these data sources respecitively. +NOTE: d1, ds2, ds3, ds4 are the names of `ReadableDataSource`, and can be coded as you like. The `processor`, `zk`, `nacos` , `apollo` refer to the specific data sources. The configurations following them are the specific configurations of these data sources respecitively. Every data source has 3 common configuration items: `data-type`, `converter-class` and `rule-type`. diff --git a/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-alibaba-kubernetes-commons/pom.xml b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-alibaba-kubernetes-commons/pom.xml index 1d70862d25..3c2983bf6f 100644 --- a/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-alibaba-kubernetes-commons/pom.xml +++ b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-alibaba-kubernetes-commons/pom.xml @@ -15,8 +15,8 @@ - org.springframework.cloud - spring-cloud-starter + org.springframework.boot + spring-boot-starter true diff --git a/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-alibaba-kubernetes-commons/src/main/java/com/alibaba/cloud/kubernetes/commons/KubernetesClientConfiguration.java b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-alibaba-kubernetes-commons/src/main/java/com/alibaba/cloud/kubernetes/commons/KubernetesClientConfiguration.java new file mode 100644 index 0000000000..2a8325210d --- /dev/null +++ b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-alibaba-kubernetes-commons/src/main/java/com/alibaba/cloud/kubernetes/commons/KubernetesClientConfiguration.java @@ -0,0 +1,58 @@ +/* + * Copyright 2013-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.alibaba.cloud.kubernetes.commons; + +import io.fabric8.kubernetes.client.KubernetesClient; + +import org.springframework.beans.factory.DisposableBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * We don't provide autoconfiguration for KubernetesClient in this module. + *

+ * Instead, we provide a configuration class, let the user decide whether to use it, and + * it can be imported manually. + *

+ * For example: + * + *

+ * @Configuration(proxyBeanMethods = false)
+ * @Import(KubernetesClientConfiguration.class)
+ * public class MyConfiguration {
+ * }
+ * 
+ * + * @author Freeman + */ +@Configuration(proxyBeanMethods = false) +@ConditionalOnClass(KubernetesClient.class) +public class KubernetesClientConfiguration implements DisposableBean { + + @Bean + @ConditionalOnMissingBean + public KubernetesClient fabric8KubernetesClient() { + return KubernetesClientHolder.getKubernetesClient(); + } + + @Override + public void destroy() { + KubernetesClientHolder.remove(); + } +} diff --git a/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-alibaba-kubernetes-commons/src/main/java/com/alibaba/cloud/kubernetes/commons/KubernetesClientHolder.java b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-alibaba-kubernetes-commons/src/main/java/com/alibaba/cloud/kubernetes/commons/KubernetesClientHolder.java new file mode 100644 index 0000000000..e30d1770c6 --- /dev/null +++ b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-alibaba-kubernetes-commons/src/main/java/com/alibaba/cloud/kubernetes/commons/KubernetesClientHolder.java @@ -0,0 +1,53 @@ +/* + * Copyright 2013-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.alibaba.cloud.kubernetes.commons; + +import java.util.concurrent.atomic.AtomicReference; + +import io.fabric8.kubernetes.client.KubernetesClient; + +/** + * KubernetesClient holder, we need to ensure that only one KubernetesClient instance in + * one application. + * + *

+ * Note: there's only one KubernetesClient instance in one application, + * but one JVM may have multiple instances, because the tests need to use multiple + * instances. + * + * @author Freeman + */ +public final class KubernetesClientHolder { + + private KubernetesClientHolder() { + throw new UnsupportedOperationException("No KubernetesClientHolder instances for you!"); + } + + private static final AtomicReference kubernetesClient = new AtomicReference<>(); + + public static synchronized KubernetesClient getKubernetesClient() { + KubernetesClient client = kubernetesClient.get(); + if (client == null) { + kubernetesClient.set(KubernetesUtils.newKubernetesClient()); + } + return kubernetesClient.get(); + } + + public static synchronized void remove() { + kubernetesClient.set(null); + } +} diff --git a/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-alibaba-kubernetes-commons/src/main/java/com/alibaba/cloud/kubernetes/commons/KubernetesUtils.java b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-alibaba-kubernetes-commons/src/main/java/com/alibaba/cloud/kubernetes/commons/KubernetesUtils.java new file mode 100644 index 0000000000..286c621c96 --- /dev/null +++ b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-alibaba-kubernetes-commons/src/main/java/com/alibaba/cloud/kubernetes/commons/KubernetesUtils.java @@ -0,0 +1,51 @@ +/* + * Copyright 2013-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.alibaba.cloud.kubernetes.commons; + +import io.fabric8.kubernetes.client.Config; +import io.fabric8.kubernetes.client.ConfigBuilder; +import io.fabric8.kubernetes.client.DefaultKubernetesClient; +import io.fabric8.kubernetes.client.KubernetesClient; + +/** + * @author Freeman + */ +public final class KubernetesUtils { + + private KubernetesUtils() { + throw new UnsupportedOperationException("No KubernetesUtil instances for you!"); + } + + private static final Config config = new ConfigBuilder().build(); + + public static Config config() { + return config; + } + + public static String currentNamespace() { + return config.getNamespace(); + } + + /** + * New a KubernetesClient instance. + * + * @return new KubernetesClient instance + */ + public static KubernetesClient newKubernetesClient() { + return new DefaultKubernetesClient(config); + } +} diff --git a/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/pom.xml b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/pom.xml index 3553a472c7..e5de64bc8b 100644 --- a/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/pom.xml +++ b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/pom.xml @@ -14,13 +14,23 @@ Spring Cloud Starter Alibaba Kubernetes Config + + org.springframework.cloud + spring-cloud-starter + com.alibaba.cloud spring-cloud-alibaba-kubernetes-commons - org.springframework.cloud - spring-cloud-starter + org.springframework.boot + spring-boot-configuration-processor + true + + + org.springframework.boot + spring-boot-starter-test + test diff --git a/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/KubernetesConfigAutoConfiguration.java b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/KubernetesConfigAutoConfiguration.java new file mode 100644 index 0000000000..70f1b5c638 --- /dev/null +++ b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/KubernetesConfigAutoConfiguration.java @@ -0,0 +1,46 @@ +/* + * Copyright 2013-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.alibaba.cloud.kubernetes.config; + +import com.alibaba.cloud.kubernetes.commons.KubernetesClientConfiguration; +import com.alibaba.cloud.kubernetes.config.core.ConfigWatcher; +import io.fabric8.kubernetes.api.model.ConfigMap; +import io.fabric8.kubernetes.client.KubernetesClient; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; + +/** + * @author Freeman + */ +@Configuration(proxyBeanMethods = false) +@ConditionalOnClass({ KubernetesClient.class, ConfigMap.class }) +@ConditionalOnProperty(prefix = KubernetesConfigProperties.PREFIX, name = "enabled", matchIfMissing = true) +@EnableConfigurationProperties(KubernetesConfigProperties.class) +@Import(KubernetesClientConfiguration.class) +public class KubernetesConfigAutoConfiguration { + + @Bean + public ConfigWatcher configWatcher(KubernetesConfigProperties properties, + KubernetesClient kubernetesClient) { + return new ConfigWatcher(properties, kubernetesClient); + } +} diff --git a/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/KubernetesConfigProperties.java b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/KubernetesConfigProperties.java new file mode 100644 index 0000000000..735b5bd423 --- /dev/null +++ b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/KubernetesConfigProperties.java @@ -0,0 +1,399 @@ +/* + * Copyright 2013-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.alibaba.cloud.kubernetes.config; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +import com.alibaba.cloud.kubernetes.commons.KubernetesUtils; +import com.alibaba.cloud.kubernetes.config.util.ConfigPreference; + +import org.springframework.beans.factory.InitializingBean; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.util.StringUtils; + +/** + * @author Freeman + */ +@ConfigurationProperties(KubernetesConfigProperties.PREFIX) +public class KubernetesConfigProperties implements InitializingBean { + /** + * Prefix of {@link KubernetesConfigProperties}. + */ + public static final String PREFIX = "spring.cloud.k8s.config"; + + /** + * Whether to enable the kubernetes config feature. + */ + private boolean enabled = true; + + /** + * Default namespace for configmaps and secrets. + *

+ * If in Kubernetes environment, use the namespace of the current pod. + *

+ * If not in Kubernetes environment, use the namespace of the current context. + */ + private String namespace = determineNamespace(); + + /** + * Config preference, default is {@link ConfigPreference#REMOTE}, means remote + * configurations 'win', will override the local configurations. + */ + private ConfigPreference preference = ConfigPreference.REMOTE; + + /** + * Whether to refresh environment when remote resource was deleted, default value is + * {@code false}. + *

+ * The default value is {@code false} to prevent app arises abnormal situation from + * resource being deleted by mistake. + */ + private boolean refreshOnDelete = false; + + /** + * Whether to fail when the config (configmap/secret) is missing, default value is + * {@code true}. + *

+ * The default value is true to prevent unintended problems caused by not + * synchronizing the configuration between environments. + */ + private boolean failOnMissingConfig = true; + + private List configMaps = new ArrayList<>(); + + private List secrets = new ArrayList<>(); + + /** + * Whether to enable the auto refresh feature, default value is {@code false}. + */ + private boolean refreshable = false; + + public boolean isEnabled() { + return enabled; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + public String getNamespace() { + return namespace; + } + + public void setNamespace(String namespace) { + this.namespace = namespace; + } + + public ConfigPreference getPreference() { + return preference; + } + + public void setPreference(ConfigPreference preference) { + this.preference = preference; + } + + public List getConfigMaps() { + return configMaps; + } + + public void setConfigMaps(List configMaps) { + this.configMaps = configMaps; + } + + public List getSecrets() { + return secrets; + } + + public void setSecrets(List secrets) { + this.secrets = secrets; + } + + public boolean isRefreshable() { + return refreshable; + } + + public void setRefreshable(boolean refreshable) { + this.refreshable = refreshable; + } + + public boolean isRefreshOnDelete() { + return refreshOnDelete; + } + + public void setRefreshOnDelete(boolean refreshOnDelete) { + this.refreshOnDelete = refreshOnDelete; + } + + public boolean isFailOnMissingConfig() { + return failOnMissingConfig; + } + + public void setFailOnMissingConfig(boolean failOnMissingConfig) { + this.failOnMissingConfig = failOnMissingConfig; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + KubernetesConfigProperties that = (KubernetesConfigProperties) o; + return enabled == that.enabled && refreshOnDelete == that.refreshOnDelete + && failOnMissingConfig == that.failOnMissingConfig + && refreshable == that.refreshable + && Objects.equals(namespace, that.namespace) + && preference == that.preference + && Objects.equals(configMaps, that.configMaps) + && Objects.equals(secrets, that.secrets); + } + + @Override + public int hashCode() { + return Objects.hash(enabled, namespace, preference, refreshOnDelete, + failOnMissingConfig, configMaps, secrets, refreshable); + } + + @Override + public String toString() { + return "KubernetesConfigProperties{" + "enabled=" + enabled + ", namespace='" + + namespace + '\'' + ", preference=" + preference + ", refreshOnDelete=" + + refreshOnDelete + ", failOnMissingConfig=" + failOnMissingConfig + + ", configMaps=" + configMaps + ", secrets=" + secrets + + ", refreshEnabled=" + refreshable + '}'; + } + + @Override + public void afterPropertiesSet() { + mergeConfigmaps(); + mergeSecrets(); + } + + private void mergeSecrets() { + for (Secret secret : secrets) { + if (!StringUtils.hasText(secret.getName())) { + throw new IllegalArgumentException("Secret name must not be empty."); + } + if (!StringUtils.hasText(secret.getNamespace())) { + secret.setNamespace(namespace); + } + if (secret.getRefreshable() == null) { + secret.setRefreshable(refreshable); + } + if (secret.getPreference() == null) { + secret.setPreference(preference); + } + } + } + + private void mergeConfigmaps() { + for (ConfigMap configMap : configMaps) { + if (!StringUtils.hasText(configMap.getName())) { + throw new IllegalArgumentException("ConfigMap name must not be empty."); + } + if (!StringUtils.hasText(configMap.getNamespace())) { + configMap.setNamespace(namespace); + } + if (configMap.getRefreshable() == null) { + configMap.setRefreshable(refreshable); + } + if (configMap.getPreference() == null) { + configMap.setPreference(preference); + } + } + } + + private static String determineNamespace() { + String ns = KubernetesUtils.currentNamespace(); + return StringUtils.hasText(ns) ? ns : "default"; + } + + public static class ConfigMap { + /** + * ConfigMap name. + */ + private String name; + /** + * Namespace, using + * {@code spring.cloud.k8s.config.namespace} if not + * set. + */ + private String namespace; + /** + * Whether to enable the auto refresh on current ConfigMap, using + * {@code spring.cloud.k8s.config.refreshable} if not + * set. + */ + private Boolean refreshable; + /** + * Config preference, using + * {@code spring.cloud.k8s.config.preference} if not + * set. + */ + private ConfigPreference preference; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getNamespace() { + return namespace; + } + + public void setNamespace(String namespace) { + this.namespace = namespace; + } + + public Boolean getRefreshable() { + return refreshable; + } + + public void setRefreshable(Boolean refreshable) { + this.refreshable = refreshable; + } + + public ConfigPreference getPreference() { + return preference; + } + + public void setPreference(ConfigPreference preference) { + this.preference = preference; + } + + @Override + public String toString() { + return "ConfigMap{" + "name='" + name + '\'' + ", namespace='" + namespace + + '\'' + ", refreshEnabled=" + refreshable + ", preference=" + + preference + '}'; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + ConfigMap configMap = (ConfigMap) o; + return Objects.equals(name, configMap.name) + && Objects.equals(namespace, configMap.namespace) + && Objects.equals(refreshable, configMap.refreshable) + && preference == configMap.preference; + } + + @Override + public int hashCode() { + return Objects.hash(name, namespace, refreshable, preference); + } + } + + public static class Secret { + /** + * Secret name. + */ + private String name; + /** + * Namespace, + * {@code spring.cloud.k8s.config.namespace} if not + * set. + */ + private String namespace; + /** + * Whether to enable the auto refresh on current Secret, default value is + * {@code false}. + *

+ * Because Secret is usually used to save sensitive information, the auto refresh + * function is not enabled by default. Please consider using ConfigMap if there is + * an auto refresh requirement. + */ + private Boolean refreshable = false; + /** + * Config preference, using + * {@code spring.cloud.k8s.config.preference} if not + * set. + */ + private ConfigPreference preference; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getNamespace() { + return namespace; + } + + public void setNamespace(String namespace) { + this.namespace = namespace; + } + + public Boolean getRefreshable() { + return refreshable; + } + + public void setRefreshable(Boolean refreshable) { + this.refreshable = refreshable; + } + + public ConfigPreference getPreference() { + return preference; + } + + public void setPreference(ConfigPreference preference) { + this.preference = preference; + } + + @Override + public String toString() { + return "Secret{" + "name='" + name + '\'' + ", namespace='" + namespace + '\'' + + ", refreshEnabled=" + refreshable + ", preference=" + preference + + '}'; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Secret secret = (Secret) o; + return Objects.equals(name, secret.name) + && Objects.equals(namespace, secret.namespace) + && Objects.equals(refreshable, secret.refreshable) + && preference == secret.preference; + } + + @Override + public int hashCode() { + return Objects.hash(name, namespace, refreshable, preference); + } + } +} diff --git a/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/core/ConfigEnvironmentPostProcessor.java b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/core/ConfigEnvironmentPostProcessor.java new file mode 100644 index 0000000000..92d33036b9 --- /dev/null +++ b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/core/ConfigEnvironmentPostProcessor.java @@ -0,0 +1,220 @@ +/* + * Copyright 2013-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.alibaba.cloud.kubernetes.config.core; + +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.Optional; + +import com.alibaba.cloud.kubernetes.commons.KubernetesClientHolder; +import com.alibaba.cloud.kubernetes.config.KubernetesConfigProperties; +import com.alibaba.cloud.kubernetes.config.exception.ConfigMissingException; +import com.alibaba.cloud.kubernetes.config.util.ConfigPreference; +import com.alibaba.cloud.kubernetes.config.util.Converters; +import com.alibaba.cloud.kubernetes.config.util.Pair; +import com.alibaba.cloud.kubernetes.config.util.RefreshContext; +import com.alibaba.cloud.kubernetes.config.util.Util; +import io.fabric8.kubernetes.api.model.ConfigMap; +import io.fabric8.kubernetes.api.model.Secret; +import io.fabric8.kubernetes.client.KubernetesClient; +import org.apache.commons.logging.Log; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.context.properties.bind.Binder; +import org.springframework.boot.env.EnvironmentPostProcessor; +import org.springframework.boot.logging.DeferredLogFactory; +import org.springframework.cloud.endpoint.event.RefreshEvent; +import org.springframework.core.Ordered; +import org.springframework.core.env.ConfigurableEnvironment; +import org.springframework.core.env.EnumerablePropertySource; +import org.springframework.core.env.MutablePropertySources; +import org.springframework.core.env.StandardEnvironment; + +import static java.util.stream.Collectors.groupingBy; +import static java.util.stream.Collectors.mapping; +import static java.util.stream.Collectors.toList; + +/** + * @author Freeman + */ +public class ConfigEnvironmentPostProcessor implements EnvironmentPostProcessor, Ordered { + /** + * Order of the post processor. + */ + public static final int ORDER = Ordered.LOWEST_PRECEDENCE - 10; + + private final Log log; + private final KubernetesClient client; + + public ConfigEnvironmentPostProcessor(DeferredLogFactory logFactory) { + this.log = logFactory.getLog(getClass()); + this.client = KubernetesClientHolder.getKubernetesClient(); + } + + @Override + public void postProcessEnvironment(ConfigurableEnvironment environment, + SpringApplication application) { + Boolean enabled = environment.getProperty( + KubernetesConfigProperties.PREFIX + ".enabled", Boolean.class, true); + if (!enabled) { + return; + } + + KubernetesConfigProperties properties = getKubernetesConfigProperties( + environment); + + if (isRefreshing()) { + RefreshEvent event = RefreshContext.get().refreshEvent(); + Object resource = event.getSource(); + if (resource instanceof ConfigMap) { + pullConfigMaps(properties, environment); + } + else if (resource instanceof Secret) { + pullSecrets(properties, environment); + } + else { + log.warn("Refreshed a unknown resource type: " + resource.getClass()); + } + } + else { + pullConfigMaps(properties, environment); + pullSecrets(properties, environment); + } + } + + private static KubernetesConfigProperties getKubernetesConfigProperties( + ConfigurableEnvironment environment) { + return Optional.ofNullable(RefreshContext.get()) + .map(context -> context.applicationContext() + .getBean(KubernetesConfigProperties.class)) + .orElse(Binder.get(environment) + .bind(KubernetesConfigProperties.PREFIX, + KubernetesConfigProperties.class) + .orElseGet(KubernetesConfigProperties::new)); + } + + private void pullConfigMaps(KubernetesConfigProperties properties, + ConfigurableEnvironment environment) { + properties.getConfigMaps().stream() + .map(configmap -> Optional + .ofNullable(propertySourceForConfigMap(configmap, properties)) + .map(ps -> Pair.of(Util.preference(configmap, properties), ps)) + .orElse(null)) + .filter(Objects::nonNull) + .collect(groupingBy(Pair::key, mapping(Pair::value, toList()))) + .forEach((configPreference, remotePropertySources) -> { + addPropertySourcesToEnvironment(environment, configPreference, + remotePropertySources); + }); + } + + private void pullSecrets(KubernetesConfigProperties properties, + ConfigurableEnvironment environment) { + properties.getSecrets().stream() + .map(secret -> Optional + .ofNullable(propertySourceForSecret(secret, properties)) + .map(ps -> Pair.of(Util.preference(secret, properties), ps)) + .orElse(null)) + .filter(Objects::nonNull) + .collect(groupingBy(Pair::key, mapping(Pair::value, toList()))) + .forEach((configPreference, remotePropertySources) -> { + addPropertySourcesToEnvironment(environment, configPreference, + remotePropertySources); + }); + } + + private static void addPropertySourcesToEnvironment( + ConfigurableEnvironment environment, ConfigPreference configPreference, + List> remotePropertySources) { + MutablePropertySources propertySources = environment.getPropertySources(); + switch (configPreference) { + case LOCAL: + // The latter config should win the previous config + Collections.reverse(remotePropertySources); + remotePropertySources.forEach(propertySources::addLast); + break; + case REMOTE: + // we can't let it override the system environment properties + remotePropertySources.forEach(ps -> propertySources.addAfter( + StandardEnvironment.SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME, ps)); + break; + default: + throw new IllegalArgumentException( + "Unknown config preference: " + configPreference.name()); + } + } + + private EnumerablePropertySource propertySourceForConfigMap( + KubernetesConfigProperties.ConfigMap cm, + KubernetesConfigProperties properties) { + if (noNeedToReloadResource(Util.refreshable(cm, properties))) { + return null; + } + ConfigMap configMap = client.configMaps() + .inNamespace(Util.namespace(cm, properties)).withName(cm.getName()).get(); + if (configMap == null) { + log.warn(String.format("ConfigMap '%s' not found in namespace '%s'", + cm.getName(), Util.namespace(cm, properties))); + failApplicationStartUpIfNecessary(ConfigMap.class, cm.getName(), + Util.namespace(cm, properties), properties); + return null; + } + return Converters.toPropertySource(configMap); + } + + private static void failApplicationStartUpIfNecessary(Class type, String name, + String namespace, KubernetesConfigProperties properties) { + if (properties.isFailOnMissingConfig()) { + throw new ConfigMissingException(type, name, namespace); + } + } + + private EnumerablePropertySource propertySourceForSecret( + KubernetesConfigProperties.Secret secret, + KubernetesConfigProperties properties) { + if (noNeedToReloadResource(Util.refreshable(secret, properties))) { + return null; + } + Secret secretInK8s = client.secrets() + .inNamespace(Util.namespace(secret, properties)) + .withName(secret.getName()).get(); + if (secretInK8s == null) { + log.warn(String.format("Secret '%s' not found in namespace '%s'", + secret.getName(), Util.namespace(secret, properties))); + failApplicationStartUpIfNecessary(Secret.class, secret.getName(), + Util.namespace(secret, properties), properties); + return null; + } + return Converters.toPropertySource(secretInK8s); + } + + private static boolean noNeedToReloadResource(boolean refreshable) { + // If this is a refresh event, we need to ignore the resource that not enabled + // auto refresh. + return isRefreshing() && !refreshable; + } + + private static boolean isRefreshing() { + return RefreshContext.get() != null; + } + + @Override + public int getOrder() { + return ORDER; + } +} diff --git a/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/core/ConfigWatcher.java b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/core/ConfigWatcher.java new file mode 100644 index 0000000000..c5837ddbef --- /dev/null +++ b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/core/ConfigWatcher.java @@ -0,0 +1,123 @@ +/* + * Copyright 2013-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.alibaba.cloud.kubernetes.config.core; + +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import com.alibaba.cloud.kubernetes.config.KubernetesConfigProperties; +import com.alibaba.cloud.kubernetes.config.util.ResourceKey; +import com.alibaba.cloud.kubernetes.config.util.Util; +import io.fabric8.kubernetes.api.model.ConfigMap; +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.fabric8.kubernetes.api.model.Secret; +import io.fabric8.kubernetes.client.KubernetesClient; +import io.fabric8.kubernetes.client.informers.SharedIndexInformer; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.DisposableBean; +import org.springframework.beans.factory.SmartInitializingSingleton; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.context.EnvironmentAware; +import org.springframework.core.env.ConfigurableEnvironment; +import org.springframework.core.env.Environment; + +/** + * Watcher for config resource changes. + * + * @author Freeman + */ +public class ConfigWatcher implements SmartInitializingSingleton, ApplicationContextAware, + EnvironmentAware, DisposableBean { + private static final Logger log = LoggerFactory.getLogger(ConfigWatcher.class); + + private final Map> configmapInformers = new LinkedHashMap<>(); + private final Map> secretInformers = new LinkedHashMap<>(); + private final KubernetesConfigProperties properties; + private final KubernetesClient client; + + private ApplicationContext context; + private ConfigurableEnvironment environment; + + public ConfigWatcher(KubernetesConfigProperties properties, KubernetesClient client) { + this.properties = properties; + this.client = client; + } + + @Override + public void setEnvironment(Environment environment) { + if (!(environment instanceof ConfigurableEnvironment)) { + throw new IllegalStateException( + "Environment must be an instance of ConfigurableEnvironment"); + } + this.environment = (ConfigurableEnvironment) environment; + } + + @Override + public void setApplicationContext(ApplicationContext applicationContext) + throws BeansException { + this.context = applicationContext; + } + + @Override + public void afterSingletonsInstantiated() { + watchRefreshableResources(properties, client); + } + + @Override + public void destroy() { + configmapInformers.values().forEach(SharedIndexInformer::close); + secretInformers.values().forEach(SharedIndexInformer::close); + log.info("ConfigMap and Secret informers closed"); + } + + private void watchRefreshableResources(KubernetesConfigProperties properties, + KubernetesClient client) { + properties.getConfigMaps().stream().filter(cm -> Util.refreshable(cm, properties)) + .forEach(cm -> configmapInformers.put(Util.resourceKey(cm, properties), + client.configMaps().inNamespace(Util.namespace(cm, properties)) + .withName(cm.getName()) + .inform(new HasMetadataResourceEventHandler<>(context, + environment, properties)))); + log(configmapInformers); + properties.getSecrets().stream() + .filter(secret -> Util.refreshable(secret, properties)) + .forEach(secret -> secretInformers.put( + Util.resourceKey(secret, properties), + client.secrets().inNamespace(Util.namespace(secret, properties)) + .withName(secret.getName()) + .inform(new HasMetadataResourceEventHandler<>(context, + environment, properties)))); + log(secretInformers); + } + + private void log( + Map> informers) { + List names = informers.keySet().stream().map(resourceKey -> String + .join(".", resourceKey.name(), resourceKey.namespace())) + .collect(Collectors.toList()); + if (!names.isEmpty() && log.isInfoEnabled()) { + log.info("Start watching {}s: {}", + informers.keySet().iterator().next().type(), names); + } + } +} diff --git a/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/core/HasMetadataResourceEventHandler.java b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/core/HasMetadataResourceEventHandler.java new file mode 100644 index 0000000000..ad050e2a01 --- /dev/null +++ b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/core/HasMetadataResourceEventHandler.java @@ -0,0 +1,105 @@ +/* + * Copyright 2013-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.alibaba.cloud.kubernetes.config.core; + +import com.alibaba.cloud.kubernetes.config.KubernetesConfigProperties; +import com.alibaba.cloud.kubernetes.config.util.Converters; +import com.alibaba.cloud.kubernetes.config.util.RefreshContext; +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.fabric8.kubernetes.client.informers.ResourceEventHandler; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.springframework.cloud.endpoint.event.RefreshEvent; +import org.springframework.context.ApplicationContext; +import org.springframework.core.env.ConfigurableEnvironment; + +/** + * @author Freeman + */ +public class HasMetadataResourceEventHandler + implements ResourceEventHandler { + private static final Logger log = LoggerFactory + .getLogger(HasMetadataResourceEventHandler.class); + + private final ApplicationContext context; + private final ConfigurableEnvironment environment; + private final KubernetesConfigProperties properties; + + public HasMetadataResourceEventHandler(ApplicationContext context, + ConfigurableEnvironment environment, KubernetesConfigProperties properties) { + this.context = context; + this.environment = environment; + this.properties = properties; + } + + @Override + public void onAdd(HasMetadata obj) { + if (log.isDebugEnabled()) { + log.debug("{} '{}' added in namespace '{}'", obj.getKind(), + obj.getMetadata().getName(), obj.getMetadata().getNamespace()); + } + // When application start up, the informer will trigger an onAdd event, but at + // this phase application is not + // ready, and it will not trigger a real refresh. + // see + // org.springframework.cloud.endpoint.event.RefreshEventListener#handle(RefreshEvent) + refresh(obj); + } + + @Override + public void onUpdate(HasMetadata oldObj, HasMetadata newObj) { + if (log.isDebugEnabled()) { + log.debug("{} '{}' updated in namespace '{}'", newObj.getKind(), + newObj.getMetadata().getName(), newObj.getMetadata().getNamespace()); + } + refresh(newObj); + } + + @Override + public void onDelete(HasMetadata obj, boolean deletedFinalStateUnknown) { + if (log.isDebugEnabled()) { + log.debug("{} '{}' deleted in namespace '{}'", obj.getKind(), + obj.getMetadata().getName(), obj.getMetadata().getNamespace()); + } + if (properties.isRefreshOnDelete()) { + deletePropertySourceOfResource(obj); + refresh(obj); + } + else { + log.info("Refresh on delete is disabled, ignore the delete event"); + } + } + + private void deletePropertySourceOfResource(HasMetadata resource) { + String propertySourceName = Converters.propertySourceNameForResource(resource); + environment.getPropertySources().remove(propertySourceName); + } + + private void refresh(HasMetadata obj) { + // Need to handle the case where events are processed asynchronously? + RefreshEvent refreshEvent = new RefreshEvent(obj, null, + String.format("%s changed", obj.getKind())); + RefreshContext.set(new RefreshContext(context, refreshEvent)); + try { + context.publishEvent(refreshEvent); + } + finally { + RefreshContext.remove(); + } + } +} diff --git a/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/core/SinglePairPropertySource.java b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/core/SinglePairPropertySource.java new file mode 100644 index 0000000000..22015a6a83 --- /dev/null +++ b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/core/SinglePairPropertySource.java @@ -0,0 +1,43 @@ +/* + * Copyright 2013-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.alibaba.cloud.kubernetes.config.core; + +import java.util.Collections; +import java.util.Map; + +import com.alibaba.cloud.kubernetes.config.util.Pair; + +import org.springframework.core.env.MapPropertySource; +import org.springframework.core.env.PropertySource; + +/** + * {@link PropertySource} that contains a single key-value pair. + * + * @author Freeman + */ +public class SinglePairPropertySource extends MapPropertySource { + + public SinglePairPropertySource(String propertySourceName, String key, Object value) { + super(propertySourceName, Collections.singletonMap(key, value)); + } + + public Pair getSinglePair() { + Map source = getSource(); + Map.Entry entry = source.entrySet().iterator().next(); + return Pair.of(entry.getKey(), entry.getValue()); + } +} diff --git a/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/exception/ConfigMissingException.java b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/exception/ConfigMissingException.java new file mode 100644 index 0000000000..9d37e31025 --- /dev/null +++ b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/exception/ConfigMissingException.java @@ -0,0 +1,45 @@ +/* + * Copyright 2013-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.alibaba.cloud.kubernetes.config.exception; + +/** + * @author Freeman + */ +public class ConfigMissingException extends RuntimeException { + + private final Class type; + private final String name; + private final String namespace; + + public ConfigMissingException(Class type, String name, String namespace) { + this.type = type; + this.name = name; + this.namespace = namespace; + } + + public Class getType() { + return type; + } + + public String getName() { + return name; + } + + public String getNamespace() { + return namespace; + } +} diff --git a/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/exception/ConfigMissingFailureAnalyzer.java b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/exception/ConfigMissingFailureAnalyzer.java new file mode 100644 index 0000000000..eaf04ab750 --- /dev/null +++ b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/exception/ConfigMissingFailureAnalyzer.java @@ -0,0 +1,39 @@ +/* + * Copyright 2013-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.alibaba.cloud.kubernetes.config.exception; + +import com.alibaba.cloud.kubernetes.config.KubernetesConfigProperties; + +import org.springframework.boot.diagnostics.AbstractFailureAnalyzer; +import org.springframework.boot.diagnostics.FailureAnalysis; + +/** + * @author Freeman + */ +public class ConfigMissingFailureAnalyzer + extends AbstractFailureAnalyzer { + @Override + protected FailureAnalysis analyze(Throwable rootFailure, + ConfigMissingException cause) { + String description = String.format("%s name '%s' is missing in namespace '%s'", + cause.getType().getSimpleName(), cause.getName(), cause.getNamespace()); + String action = String.format( + "You can set '%s.fail-on-missing-config' to 'false' to not prevent the application start up.", + KubernetesConfigProperties.PREFIX); + return new FailureAnalysis(description, action, cause); + } +} diff --git a/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/processor/FileProcessor.java b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/processor/FileProcessor.java new file mode 100644 index 0000000000..04673d567f --- /dev/null +++ b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/processor/FileProcessor.java @@ -0,0 +1,42 @@ +/* + * Copyright 2013-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.alibaba.cloud.kubernetes.config.processor; + +import org.springframework.core.env.EnumerablePropertySource; + +/** + * @author Freeman + */ +public interface FileProcessor { + + /** + * Whether the fileName is supported by the processor. + * + * @param fileName file name + * @return true if hit + */ + boolean hit(String fileName); + + /** + * Generate property source from file content. + * + * @param name property source name + * @param content file content + * @return property source + */ + EnumerablePropertySource generate(String name, String content); +} diff --git a/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/processor/JsonFileProcessor.java b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/processor/JsonFileProcessor.java new file mode 100644 index 0000000000..006e4e6966 --- /dev/null +++ b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/processor/JsonFileProcessor.java @@ -0,0 +1,108 @@ +/* + * Copyright 2013-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.alibaba.cloud.kubernetes.config.processor; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.yaml.snakeyaml.Yaml; + +import org.springframework.boot.env.YamlPropertySourceLoader; +import org.springframework.core.env.CompositePropertySource; +import org.springframework.core.env.EnumerablePropertySource; +import org.springframework.core.env.PropertySource; +import org.springframework.core.io.ByteArrayResource; + +/** + * @author Freeman + */ +public class JsonFileProcessor implements FileProcessor { + private static final Logger log = LoggerFactory.getLogger(JsonFileProcessor.class); + + private static final YamlPropertySourceLoader loader = new YamlPropertySourceLoader(); + + private static final Yaml yaml = new Yaml(); + + private static final ObjectMapper objectMapper = new ObjectMapper(); + + @Override + public boolean hit(String fileName) { + return fileName.endsWith(".json"); + } + + @Override + @SuppressWarnings("rawtypes") + public EnumerablePropertySource generate(String name, String content) { + if (content.trim().startsWith("{")) { + // json object + return convertJsonObjectStringToPropertySource(name, content); + } + CompositePropertySource result = new CompositePropertySource(name); + try { + List list = objectMapper.readValue(content, List.class); + if (list.isEmpty()) { + return result; + } + for (int i = 0; i < list.size(); i++) { + Object o = list.get(i); + if (o instanceof Map) { + // means it's a json object + result.addPropertySource(convertJsonObjectStringToPropertySource( + String.format("%s[%d]", name, i), + objectMapper.writeValueAsString(o))); + } + } + } + catch (JsonProcessingException e) { + log.warn("Failed to parse json file", e); + } + return result; + } + + @SuppressWarnings("rawtypes") + private static CompositePropertySource convertJsonObjectStringToPropertySource( + String name, String jsonObjectString) { + // We don't want to change the Spring default behavior + // this is how we convert json to PropertySource + // json -> java.util.Map -> yaml -> PropertySource + Map map = new HashMap<>(); + try { + map = objectMapper.readValue(jsonObjectString, Map.class); + } + catch (JsonProcessingException e) { + log.warn("Failed to parse json file", e); + } + CompositePropertySource propertySource = new CompositePropertySource(name); + try { + String yamlString = yaml.dump(map); + List> pss = loader.load(name, + new ByteArrayResource(yamlString.getBytes(StandardCharsets.UTF_8))); + propertySource.getPropertySources().addAll(pss); + } + catch (IOException e) { + log.warn("Failed to parse yaml file", e); + } + return propertySource; + } +} diff --git a/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/processor/PropertiesFileProcessor.java b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/processor/PropertiesFileProcessor.java new file mode 100644 index 0000000000..bbab1194a8 --- /dev/null +++ b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/processor/PropertiesFileProcessor.java @@ -0,0 +1,60 @@ +/* + * Copyright 2013-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.alibaba.cloud.kubernetes.config.processor; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.List; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.springframework.boot.env.PropertiesPropertySourceLoader; +import org.springframework.core.env.CompositePropertySource; +import org.springframework.core.env.EnumerablePropertySource; +import org.springframework.core.env.PropertySource; +import org.springframework.core.io.ByteArrayResource; + +/** + * @author Freeman + */ +public class PropertiesFileProcessor implements FileProcessor { + private static final Logger log = LoggerFactory + .getLogger(PropertiesFileProcessor.class); + + private static final PropertiesPropertySourceLoader loader = new PropertiesPropertySourceLoader(); + + @Override + public boolean hit(String fileName) { + return Arrays.stream(loader.getFileExtensions()).anyMatch(fileName::endsWith); + } + + @Override + public EnumerablePropertySource generate(String name, String content) { + CompositePropertySource propertySource = new CompositePropertySource(name); + try { + List> pss = loader.load(name, + new ByteArrayResource(content.getBytes(StandardCharsets.UTF_8))); + propertySource.getPropertySources().addAll(pss); + } + catch (IOException e) { + log.warn("Failed to parse properties file", e); + } + return propertySource; + } +} diff --git a/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/processor/YamlFileProcessor.java b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/processor/YamlFileProcessor.java new file mode 100644 index 0000000000..a3f792317f --- /dev/null +++ b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/processor/YamlFileProcessor.java @@ -0,0 +1,61 @@ +/* + * Copyright 2013-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.alibaba.cloud.kubernetes.config.processor; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.List; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.springframework.boot.env.YamlPropertySourceLoader; +import org.springframework.core.env.CompositePropertySource; +import org.springframework.core.env.EnumerablePropertySource; +import org.springframework.core.env.PropertySource; +import org.springframework.core.io.ByteArrayResource; +import org.springframework.core.io.Resource; + +/** + * @author Freeman + */ +public class YamlFileProcessor implements FileProcessor { + private static final Logger log = LoggerFactory.getLogger(YamlFileProcessor.class); + + private static final YamlPropertySourceLoader loader = new YamlPropertySourceLoader(); + + @Override + public boolean hit(String fileName) { + return Arrays.stream(loader.getFileExtensions()).anyMatch(fileName::endsWith); + } + + @Override + public EnumerablePropertySource generate(String name, String content) { + Resource resource = new ByteArrayResource( + content.getBytes(StandardCharsets.UTF_8)); + CompositePropertySource propertySource = new CompositePropertySource(name); + try { + List> sources = loader.load(name, resource); + propertySource.getPropertySources().addAll(sources); + } + catch (IOException e) { + log.warn("Failed to parse yaml file", e); + } + return propertySource; + } +} diff --git a/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/util/ConfigPreference.java b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/util/ConfigPreference.java new file mode 100644 index 0000000000..e242151247 --- /dev/null +++ b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/util/ConfigPreference.java @@ -0,0 +1,31 @@ +/* + * Copyright 2013-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.alibaba.cloud.kubernetes.config.util; + +/** + * @author Freeman + */ +public enum ConfigPreference { + /** + * LOCAL means local configuration has higher priority and will override the remote configuration. + */ + LOCAL, + /** + * REMOTE means remote configuration has higher priority and will override the local configuration. + */ + REMOTE +} diff --git a/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/util/Converters.java b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/util/Converters.java new file mode 100644 index 0000000000..ed931042c0 --- /dev/null +++ b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/util/Converters.java @@ -0,0 +1,140 @@ +/* + * Copyright 2013-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.alibaba.cloud.kubernetes.config.util; + +import java.util.ArrayList; +import java.util.Base64; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import com.alibaba.cloud.kubernetes.config.core.SinglePairPropertySource; +import com.alibaba.cloud.kubernetes.config.processor.FileProcessor; +import io.fabric8.kubernetes.api.model.ConfigMap; +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.fabric8.kubernetes.api.model.Secret; + +import org.springframework.core.env.CompositePropertySource; +import org.springframework.core.env.EnumerablePropertySource; +import org.springframework.core.env.MapPropertySource; + +import static com.alibaba.cloud.kubernetes.config.util.Processors.fileProcessors; +import static java.nio.charset.StandardCharsets.UTF_8; + +/** + * @author Freeman + */ +public final class Converters { + private Converters() { + throw new UnsupportedOperationException("No Converter instances for you!"); + } + + private static EnumerablePropertySource toPropertySource(String propertySourceName, + Map data) { + CompositePropertySource compositePropertySource = new CompositePropertySource( + propertySourceName); + List singlePairPropertySources = new ArrayList<>(); + data.forEach((key, content) -> { + EnumerablePropertySource ps = toPropertySource(key, content, + propertySourceName + "[" + key + "]"); + if (ps instanceof SinglePairPropertySource) { + singlePairPropertySources.add((SinglePairPropertySource) ps); + } + else { + compositePropertySource.addPropertySource(ps); + } + }); + if (!singlePairPropertySources.isEmpty()) { + Map pairProperties = singlePairPropertySources.stream() + .map(SinglePairPropertySource::getSinglePair) + .collect(Collectors.toMap(Pair::key, Pair::value, + (oldValue, newValue) -> newValue, LinkedHashMap::new)); + compositePropertySource.addPropertySource( + new MapPropertySource(propertySourceName + "[pair]", pairProperties)); + } + return compositePropertySource; + } + + private static EnumerablePropertySource toPropertySource(String key, + String content, String propertySourceName) { + for (FileProcessor fileProcessor : fileProcessors()) { + if (fileProcessor.hit(key)) { + return fileProcessor.generate(propertySourceName, content); + } + } + // key-value pair + return new SinglePairPropertySource(propertySourceName, key, content); + } + + /** + * Generate a {@link EnumerablePropertySource} from a {@link ConfigMap}. + * + * @param configMap the config map + * @return the property source + */ + public static EnumerablePropertySource toPropertySource(ConfigMap configMap) { + return toPropertySource(propertySourceNameForResource(configMap), + configMap.getData()); + } + + /** + * Generate a {@link EnumerablePropertySource} from a {@link Secret}. + * + * @param secret the secret + * @return the property source + */ + public static EnumerablePropertySource toPropertySource(Secret secret) { + // data is base64 encoded + Map data = secret.getData(); + Map encodedValue = new LinkedHashMap<>(data); + data.replaceAll((key, value) -> stripTrailing( + new String(Base64.getDecoder().decode(value), UTF_8))); // secret will add + // newlines + // automatically + Map decodedValue = new LinkedHashMap<>(data); + CompositePropertySource result = new CompositePropertySource( + propertySourceNameForResource(secret)); + result.addPropertySource(toPropertySource( + propertySourceNameForResource(secret) + "[decoded]", decodedValue)); + result.addPropertySource(new MapPropertySource( + propertySourceNameForResource(secret) + "[encoded]", encodedValue)); + return result; + } + + /** + * Strip trailing whitespace from a string. + * + * @param str string + * @return string without trailing whitespace + */ + static String stripTrailing(String str) { + return str.replaceAll("\\s+$", ""); + } + + /** + * Generate property source name for resource that have metadata. + * + * @param hasMetadataResource the resource that have metadata + * @return the property source name + */ + public static String propertySourceNameForResource(HasMetadata hasMetadataResource) { + return String.format("%s:%s.%s", hasMetadataResource.getKind(), + hasMetadataResource.getMetadata().getName(), + hasMetadataResource.getMetadata().getNamespace()); + } +} diff --git a/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/util/Pair.java b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/util/Pair.java new file mode 100644 index 0000000000..fa8921db7e --- /dev/null +++ b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/util/Pair.java @@ -0,0 +1,42 @@ +/* + * Copyright 2013-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.alibaba.cloud.kubernetes.config.util; + +/** + * @author Freeman + */ +public final class Pair { + private final K key; + private final V right; + + private Pair(K key, V right) { + this.key = key; + this.right = right; + } + + public K key() { + return key; + } + + public V value() { + return right; + } + + public static Pair of(K key, V value) { + return new Pair<>(key, value); + } +} diff --git a/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/util/Processors.java b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/util/Processors.java new file mode 100644 index 0000000000..a797c2b657 --- /dev/null +++ b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/util/Processors.java @@ -0,0 +1,43 @@ +/* + * Copyright 2013-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.alibaba.cloud.kubernetes.config.util; + +import java.util.Arrays; +import java.util.List; + +import com.alibaba.cloud.kubernetes.config.processor.FileProcessor; +import com.alibaba.cloud.kubernetes.config.processor.JsonFileProcessor; +import com.alibaba.cloud.kubernetes.config.processor.PropertiesFileProcessor; +import com.alibaba.cloud.kubernetes.config.processor.YamlFileProcessor; + +/** + * @author Freeman + */ +public final class Processors { + + private Processors() { + throw new UnsupportedOperationException("No Processors instances for you!"); + } + + private static final List processors = Arrays.asList( + new YamlFileProcessor(), new PropertiesFileProcessor(), + new JsonFileProcessor()); + + public static List fileProcessors() { + return processors; + } +} diff --git a/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/util/RefreshContext.java b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/util/RefreshContext.java new file mode 100644 index 0000000000..fd339940b2 --- /dev/null +++ b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/util/RefreshContext.java @@ -0,0 +1,85 @@ +/* + * Copyright 2013-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.alibaba.cloud.kubernetes.config.util; + +import java.util.Objects; + +import org.springframework.cloud.endpoint.event.RefreshEvent; +import org.springframework.context.ApplicationContext; + +/** + * Helper class to get the Spring ApplicationContext and RefreshEvent when refreshing the + * context. + * + * @author Freeman + */ +public final class RefreshContext { + private final ApplicationContext applicationContext; + private final RefreshEvent refreshEvent; + + public RefreshContext(ApplicationContext applicationContext, + RefreshEvent refreshEvent) { + this.applicationContext = applicationContext; + this.refreshEvent = refreshEvent; + } + + public ApplicationContext applicationContext() { + return applicationContext; + } + + public RefreshEvent refreshEvent() { + return refreshEvent; + } + + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + if (obj == null || obj.getClass() != this.getClass()) { + return false; + } + RefreshContext that = (RefreshContext) obj; + return Objects.equals(this.applicationContext, that.applicationContext) + && Objects.equals(this.refreshEvent, that.refreshEvent); + } + + @Override + public int hashCode() { + return Objects.hash(applicationContext, refreshEvent); + } + + @Override + public String toString() { + return "RefreshContext[" + "applicationContext=" + applicationContext + ", " + + "refreshEvent=" + refreshEvent + ']'; + } + + private static final ThreadLocal holder = new ThreadLocal<>(); + + public static void set(RefreshContext refreshContext) { + holder.set(refreshContext); + } + + public static RefreshContext get() { + return holder.get(); + } + + public static void remove() { + holder.remove(); + } +} diff --git a/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/util/ResourceKey.java b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/util/ResourceKey.java new file mode 100644 index 0000000000..936036a92f --- /dev/null +++ b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/util/ResourceKey.java @@ -0,0 +1,80 @@ +/* + * Copyright 2013-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.alibaba.cloud.kubernetes.config.util; + +import java.util.Objects; + +/** + * @author Freeman + */ +public final class ResourceKey { + private final String type; + private final String name; + private final String namespace; + private final boolean refreshEnabled; + + public ResourceKey(String type, String name, String namespace, + boolean refreshEnabled) { + this.type = type; + this.name = name; + this.namespace = namespace; + this.refreshEnabled = refreshEnabled; + } + + public String type() { + return type; + } + + public String name() { + return name; + } + + public String namespace() { + return namespace; + } + + public boolean refreshEnabled() { + return refreshEnabled; + } + + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + if (obj == null || obj.getClass() != this.getClass()) { + return false; + } + ResourceKey that = (ResourceKey) obj; + return Objects.equals(this.type, that.type) + && Objects.equals(this.name, that.name) + && Objects.equals(this.namespace, that.namespace) + && this.refreshEnabled == that.refreshEnabled; + } + + @Override + public int hashCode() { + return Objects.hash(type, name, namespace, refreshEnabled); + } + + @Override + public String toString() { + return "ResourceKey[" + "type=" + type + ", " + "name=" + name + ", " + + "namespace=" + namespace + ", " + "refreshEnabled=" + refreshEnabled + + ']'; + } +} diff --git a/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/util/Util.java b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/util/Util.java new file mode 100644 index 0000000000..e801000dc6 --- /dev/null +++ b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/util/Util.java @@ -0,0 +1,82 @@ +/* + * Copyright 2013-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.alibaba.cloud.kubernetes.config.util; + +import java.util.Optional; + +import com.alibaba.cloud.kubernetes.config.KubernetesConfigProperties; +import io.fabric8.kubernetes.api.model.ConfigMap; +import io.fabric8.kubernetes.api.model.Secret; + +/** + * @author Freeman + */ +public final class Util { + + private Util() { + throw new UnsupportedOperationException("No Util instances for you!"); + } + + public static ResourceKey resourceKey(KubernetesConfigProperties.ConfigMap configMap, + KubernetesConfigProperties properties) { + return new ResourceKey(ConfigMap.class.getSimpleName(), configMap.getName(), + namespace(configMap, properties), refreshable(configMap, properties)); + } + + public static ResourceKey resourceKey(KubernetesConfigProperties.Secret secret, + KubernetesConfigProperties properties) { + return new ResourceKey(Secret.class.getSimpleName(), secret.getName(), + namespace(secret, properties), refreshable(secret, properties)); + } + + public static String namespace(KubernetesConfigProperties.ConfigMap configMap, + KubernetesConfigProperties properties) { + return Optional.ofNullable(configMap.getNamespace()) + .orElseGet(properties::getNamespace); + } + + public static String namespace(KubernetesConfigProperties.Secret secret, + KubernetesConfigProperties properties) { + return Optional.ofNullable(secret.getNamespace()) + .orElseGet(properties::getNamespace); + } + + public static boolean refreshable(KubernetesConfigProperties.ConfigMap configMap, + KubernetesConfigProperties properties) { + return Optional.ofNullable(configMap.getRefreshable()) + .orElseGet(properties::isRefreshable); + } + + public static boolean refreshable(KubernetesConfigProperties.Secret secret, + KubernetesConfigProperties properties) { + return Optional.ofNullable(secret.getRefreshable()) + .orElseGet(properties::isRefreshable); + } + + public static ConfigPreference preference( + KubernetesConfigProperties.ConfigMap configMap, + KubernetesConfigProperties properties) { + return Optional.ofNullable(configMap.getPreference()) + .orElseGet(properties::getPreference); + } + + public static ConfigPreference preference(KubernetesConfigProperties.Secret secret, + KubernetesConfigProperties properties) { + return Optional.ofNullable(secret.getPreference()) + .orElseGet(properties::getPreference); + } +} diff --git a/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/resources/META-INF/spring.factories b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/resources/META-INF/spring.factories new file mode 100644 index 0000000000..d2fbcf1b7a --- /dev/null +++ b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/resources/META-INF/spring.factories @@ -0,0 +1,8 @@ +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ + com.alibaba.cloud.kubernetes.config.KubernetesConfigAutoConfiguration + +org.springframework.boot.env.EnvironmentPostProcessor=\ + com.alibaba.cloud.kubernetes.config.core.ConfigEnvironmentPostProcessor + +org.springframework.boot.diagnostics.FailureAnalyzer=\ + com.alibaba.cloud.kubernetes.config.exception.ConfigMissingFailureAnalyzer \ No newline at end of file diff --git a/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/test/java/com/alibaba/cloud/kubernetes/config/Empty.java b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/test/java/com/alibaba/cloud/kubernetes/config/Empty.java new file mode 100644 index 0000000000..92b0d715f5 --- /dev/null +++ b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/test/java/com/alibaba/cloud/kubernetes/config/Empty.java @@ -0,0 +1,28 @@ +/* + * Copyright 2013-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.alibaba.cloud.kubernetes.config; + +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.context.annotation.Configuration; + +/** + * @author Freeman + */ +@Configuration(proxyBeanMethods = false) +@EnableAutoConfiguration +public class Empty { +} diff --git a/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/test/java/com/alibaba/cloud/kubernetes/config/MissingConfigIntegrationTests.java b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/test/java/com/alibaba/cloud/kubernetes/config/MissingConfigIntegrationTests.java new file mode 100644 index 0000000000..0d68f18120 --- /dev/null +++ b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/test/java/com/alibaba/cloud/kubernetes/config/MissingConfigIntegrationTests.java @@ -0,0 +1,49 @@ +/* + * Copyright 2013-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.alibaba.cloud.kubernetes.config; + +import com.alibaba.cloud.kubernetes.config.exception.ConfigMissingException; +import com.alibaba.cloud.kubernetes.config.testsupport.KubernetesAvailable; +import org.junit.jupiter.api.Test; + +import org.springframework.boot.WebApplicationType; +import org.springframework.boot.builder.SpringApplicationBuilder; + +import static org.assertj.core.api.Assertions.assertThatCode; + +/** + * @author Freeman + */ +@KubernetesAvailable +public class MissingConfigIntegrationTests { + + @Test + void testEnabledFailOnMissingConfig() { + assertThatCode(() -> new SpringApplicationBuilder(Empty.class) + .web(WebApplicationType.NONE).profiles("missing-config").run()) + .isInstanceOf(ConfigMissingException.class); + } + + @Test + void testDisabledFailOnMissingConfig() { + assertThatCode(() -> new SpringApplicationBuilder(Empty.class) + .web(WebApplicationType.NONE) + .properties(KubernetesConfigProperties.PREFIX + + ".fail-on-missing-config=false") + .profiles("missing-config").run()).doesNotThrowAnyException(); + } +} diff --git a/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/test/java/com/alibaba/cloud/kubernetes/config/NormalIntegrationTests.java b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/test/java/com/alibaba/cloud/kubernetes/config/NormalIntegrationTests.java new file mode 100644 index 0000000000..8ea4a8fcbb --- /dev/null +++ b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/test/java/com/alibaba/cloud/kubernetes/config/NormalIntegrationTests.java @@ -0,0 +1,86 @@ +/* + * Copyright 2013-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.alibaba.cloud.kubernetes.config; + +import com.alibaba.cloud.kubernetes.config.testsupport.KubernetesAvailable; +import com.alibaba.cloud.kubernetes.config.testsupport.KubernetesTestUtil; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.core.env.Environment; +import org.springframework.test.context.ActiveProfiles; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.NONE; + +/** + * @author Freeman + */ +@KubernetesAvailable +@SpringBootTest(classes = Empty.class, webEnvironment = NONE) +@ActiveProfiles("normal") +public class NormalIntegrationTests { + + @BeforeAll + static void init() { + KubernetesTestUtil.createOrReplaceConfigMap("normal/configmap.yaml"); + } + + @AfterAll + static void recover() { + KubernetesTestUtil.deleteConfigMap("normal/configmap-changed.yaml"); + } + + @Autowired + private Environment env; + + @Test + void testNormal() throws InterruptedException { + assertThat(env.getProperty("username")).isEqualTo("admin"); + assertThat(env.getProperty("password")).isEqualTo("666"); + assertThat(env.getProperty("hobbies[0]")).isEqualTo("reading"); + assertThat(env.getProperty("hobbies[1]")).isEqualTo("writing"); + assertThat(env.getProperty("hobbies[2]")).isNull(); + + // update configmap + KubernetesTestUtil.createOrReplaceConfigMap("normal/configmap-changed.yaml"); + + // context is refreshing + Thread.sleep(1000); + + assertThat(env.getProperty("username")).isEqualTo("admin"); + assertThat(env.getProperty("password")).isEqualTo("888"); + assertThat(env.getProperty("hobbies[0]")).isEqualTo("reading"); + assertThat(env.getProperty("hobbies[1]")).isEqualTo("writing"); + assertThat(env.getProperty("hobbies[2]")).isEqualTo("coding"); + + // delete configmap, refresh on delete is disabled by default + KubernetesTestUtil.deleteConfigMap("normal/configmap-changed.yaml"); + + // context is refreshing + Thread.sleep(1000); + + assertThat(env.getProperty("username")).isEqualTo("admin"); + assertThat(env.getProperty("password")).isEqualTo("888"); + assertThat(env.getProperty("hobbies[0]")).isEqualTo("reading"); + assertThat(env.getProperty("hobbies[1]")).isEqualTo("writing"); + assertThat(env.getProperty("hobbies[2]")).isEqualTo("coding"); + } +} diff --git a/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/test/java/com/alibaba/cloud/kubernetes/config/NotRefreshableIntegrationTests.java b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/test/java/com/alibaba/cloud/kubernetes/config/NotRefreshableIntegrationTests.java new file mode 100644 index 0000000000..f00dcf2906 --- /dev/null +++ b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/test/java/com/alibaba/cloud/kubernetes/config/NotRefreshableIntegrationTests.java @@ -0,0 +1,89 @@ +/* + * Copyright 2013-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.alibaba.cloud.kubernetes.config; + +import com.alibaba.cloud.kubernetes.config.testsupport.KubernetesAvailable; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.core.env.Environment; +import org.springframework.test.context.ActiveProfiles; + +import static com.alibaba.cloud.kubernetes.config.testsupport.KubernetesTestUtil.createOrReplaceConfigMap; +import static com.alibaba.cloud.kubernetes.config.testsupport.KubernetesTestUtil.deleteConfigMap; +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.NONE; + +/** + * @author Freeman + */ +@KubernetesAvailable +@SpringBootTest(classes = Empty.class, webEnvironment = NONE) +@ActiveProfiles("not-refreshable") +public class NotRefreshableIntegrationTests { + + @BeforeAll + static void init() { + createOrReplaceConfigMap("not_refreshable/configmap-01.yaml"); + createOrReplaceConfigMap("not_refreshable/configmap-02.yaml"); + } + + @AfterAll + static void recover() { + deleteConfigMap("not_refreshable/configmap-01-changed.yaml"); + deleteConfigMap("not_refreshable/configmap-02-changed.yaml"); + } + + @Autowired + private Environment env; + + @Test + void testNotRefreshable() throws InterruptedException { + assertThat(env.getProperty("username")).isEqualTo("admin"); + assertThat(env.getProperty("password")).isEqualTo("666"); + assertThat(env.getProperty("hobbies[0]")).isEqualTo("reading"); + assertThat(env.getProperty("hobbies[1]")).isEqualTo("writing"); + assertThat(env.getProperty("hobbies[2]")).isNull(); + + // update configmap-01 + createOrReplaceConfigMap("not_refreshable/configmap-01-changed.yaml"); + + // context is refreshing + Thread.sleep(1000); + + assertThat(env.getProperty("username")).isEqualTo("admin"); + assertThat(env.getProperty("password")).isEqualTo("888"); + assertThat(env.getProperty("hobbies[0]")).isEqualTo("reading"); + assertThat(env.getProperty("hobbies[1]")).isEqualTo("writing"); + assertThat(env.getProperty("hobbies[2]")).isNull(); + + // update configmap-02 + createOrReplaceConfigMap("not_refreshable/configmap-02-changed.yaml"); + + // context is refreshing + Thread.sleep(1000); + + assertThat(env.getProperty("username")).isEqualTo("admin"); + assertThat(env.getProperty("password")).isEqualTo("888"); + assertThat(env.getProperty("hobbies[0]")).isEqualTo("reading"); + assertThat(env.getProperty("hobbies[1]")).isNotEqualTo("singing"); + assertThat(env.getProperty("hobbies[2]")).isNull(); + } +} diff --git a/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/test/java/com/alibaba/cloud/kubernetes/config/SecretIntegrationTests.java b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/test/java/com/alibaba/cloud/kubernetes/config/SecretIntegrationTests.java new file mode 100644 index 0000000000..7fc26579dc --- /dev/null +++ b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/test/java/com/alibaba/cloud/kubernetes/config/SecretIntegrationTests.java @@ -0,0 +1,88 @@ +/* + * Copyright 2013-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.alibaba.cloud.kubernetes.config; + +import com.alibaba.cloud.kubernetes.config.testsupport.KubernetesAvailable; +import com.alibaba.cloud.kubernetes.config.testsupport.KubernetesTestUtil; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.core.env.Environment; +import org.springframework.test.context.ActiveProfiles; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.NONE; + +/** + * @author Freeman + */ +@KubernetesAvailable +@SpringBootTest(classes = Empty.class, webEnvironment = NONE) +@ActiveProfiles("secret") +public class SecretIntegrationTests { + + @BeforeAll + static void init() { + KubernetesTestUtil.createOrReplaceConfigMap("secret/configmap.yaml"); + KubernetesTestUtil.createOrReplaceSecret("secret/secret.yaml"); + KubernetesTestUtil.createOrReplaceSecret("secret/secret-refreshable.yaml"); + } + + @AfterAll + static void recover() { + KubernetesTestUtil.deleteConfigMap("secret/configmap.yaml"); + KubernetesTestUtil.deleteSecret("secret/secret.yaml"); + KubernetesTestUtil.deleteSecret("secret/secret-refreshable.yaml"); + } + + @Autowired + private Environment env; + + @Test + void testSecret() throws InterruptedException { + // secret win, configmap lose + assertThat(env.getProperty("username")).isNotEqualTo("admin"); + assertThat(env.getProperty("username")).isNotEqualTo("cm9vdAo="); + assertThat(env.getProperty("username")).isEqualTo("root"); + assertThat(env.getProperty("password")).isNotEqualTo("666"); + assertThat(env.getProperty("password")).isNotEqualTo("MTEyMzIyMwo="); + assertThat(env.getProperty("password")).isEqualTo("1123223"); // MTEyMzIyMwo= + + assertThat(env.getProperty("price")).isEqualTo("100"); + + assertThat(env.getProperty("hobbies[0]")).isEqualTo("reading"); + assertThat(env.getProperty("hobbies[1]")).isEqualTo("writing"); + assertThat(env.getProperty("hobbies[2]")).isNull(); + + KubernetesTestUtil.createOrReplaceSecret("secret/secret-changed.yaml"); + KubernetesTestUtil + .createOrReplaceSecret("secret/secret-refreshable-changed.yaml"); + + // make sure context is refreshed + Thread.sleep(1000); + + // secret refresh is disabled by default + assertThat(env.getProperty("username")).isNotEqualTo("root2"); + assertThat(env.getProperty("username")).isEqualTo("root"); + + // test refreshable secret + assertThat(env.getProperty("price")).isEqualTo("200"); + } +} diff --git a/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/test/java/com/alibaba/cloud/kubernetes/config/processor/JsonFileProcessorTest.java b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/test/java/com/alibaba/cloud/kubernetes/config/processor/JsonFileProcessorTest.java new file mode 100644 index 0000000000..a2f903f7b1 --- /dev/null +++ b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/test/java/com/alibaba/cloud/kubernetes/config/processor/JsonFileProcessorTest.java @@ -0,0 +1,71 @@ +/* + * Copyright 2013-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.alibaba.cloud.kubernetes.config.processor; + +import org.junit.jupiter.api.Test; + +import org.springframework.core.env.EnumerablePropertySource; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * {@link JsonFileProcessor} tester. + */ +class JsonFileProcessorTest { + + /** + * {@link JsonFileProcessor#generate(String, String)}. + */ + @Test + void generate_whenJsonObject() { + String json = "{\n" + " \"username\": \"admin\",\n" + + " \"password\": \"666\",\n" + " \"hobbies\": [\n" + + " \"reading\",\n" + " \"writing\"\n" + " ]\n" + "}\n"; + EnumerablePropertySource ps = new JsonFileProcessor().generate("test_generate", + json); + + assertThat(ps.getPropertyNames()).hasSize(4); + assertThat(ps.getProperty("username")).isEqualTo("admin"); + assertThat(ps.getProperty("password")).isEqualTo("666"); + assertThat(ps.getProperty("hobbies[0]")).isEqualTo("reading"); + assertThat(ps.getProperty("hobbies[1]")).isEqualTo("writing"); + } + + /** + * {@link JsonFileProcessor#generate(String, String)}. + */ + @Test + void generate_whenJsonArray() { + String jsonArray = "[\n" + " {\n" + " \"username\": \"admin\",\n" + + " \"password\": \"666\",\n" + " \"hobbies\": [\n" + + " \"reading\",\n" + " \"writing\"\n" + " ]\n" + " },\n" + + " {\n" + " \"username\": \"root\",\n" + + " \"password\": \"888\",\n" + " \"hobbies\": [\n" + + " \"reading\",\n" + " \"writing\",\n" + " \"coding\"\n" + + " ]\n" + " }\n" + "]\n"; + EnumerablePropertySource ps = new JsonFileProcessor().generate("test_generate", + jsonArray); + + // previous config wins + assertThat(ps.getPropertyNames()).hasSize(5); + assertThat(ps.getProperty("username")).isEqualTo("admin"); + assertThat(ps.getProperty("password")).isEqualTo("666"); + assertThat(ps.getProperty("hobbies[0]")).isEqualTo("reading"); + assertThat(ps.getProperty("hobbies[1]")).isEqualTo("writing"); + assertThat(ps.getProperty("hobbies[2]")).isEqualTo("coding"); + } +} diff --git a/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/test/java/com/alibaba/cloud/kubernetes/config/processor/PropertiesFileProcessorTest.java b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/test/java/com/alibaba/cloud/kubernetes/config/processor/PropertiesFileProcessorTest.java new file mode 100644 index 0000000000..f3217a271f --- /dev/null +++ b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/test/java/com/alibaba/cloud/kubernetes/config/processor/PropertiesFileProcessorTest.java @@ -0,0 +1,46 @@ +/* + * Copyright 2013-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.alibaba.cloud.kubernetes.config.processor; + +import org.junit.jupiter.api.Test; + +import org.springframework.core.env.EnumerablePropertySource; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * {@link PropertiesFileProcessor} tester. + */ +class PropertiesFileProcessorTest { + + /** + * {@link PropertiesFileProcessor#generate(String, String)}. + */ + @Test + void generate() { + String properties = "username=admin\n" + "password=666\n" + "hobbies[0]=reading\n" + + "hobbies[1]=writing\n"; + EnumerablePropertySource ps = new PropertiesFileProcessor() + .generate("test_generate", properties); + + assertThat(ps.getPropertyNames()).hasSize(4); + assertThat(ps.getProperty("username")).isEqualTo("admin"); + assertThat(ps.getProperty("password")).isEqualTo("666"); + assertThat(ps.getProperty("hobbies[0]")).isEqualTo("reading"); + assertThat(ps.getProperty("hobbies[1]")).isEqualTo("writing"); + } +} diff --git a/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/test/java/com/alibaba/cloud/kubernetes/config/processor/YamlFileProcessorTest.java b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/test/java/com/alibaba/cloud/kubernetes/config/processor/YamlFileProcessorTest.java new file mode 100644 index 0000000000..5e8596494f --- /dev/null +++ b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/test/java/com/alibaba/cloud/kubernetes/config/processor/YamlFileProcessorTest.java @@ -0,0 +1,64 @@ +/* + * Copyright 2013-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.alibaba.cloud.kubernetes.config.processor; + +import org.junit.jupiter.api.Test; + +import org.springframework.core.env.EnumerablePropertySource; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * {@link YamlFileProcessor} tester. + */ +class YamlFileProcessorTest { + + /** + * {@link YamlFileProcessor#generate(String, String)}. + */ + @Test + void generate_whenSingleDocument() { + String yaml = "username: admin\n" + "password: \"666\"\n" + "hobbies:\n" + + " - reading\n" + " - writing\n"; + EnumerablePropertySource ps = new YamlFileProcessor().generate("test_generate", + yaml); + assertThat(ps.getPropertyNames()).hasSize(4); + assertThat(ps.getProperty("username")).isEqualTo("admin"); + assertThat(ps.getProperty("password")).isEqualTo("666"); + assertThat(ps.getProperty("hobbies[0]")).isEqualTo("reading"); + assertThat(ps.getProperty("hobbies[1]")).isEqualTo("writing"); + } + + /** + * {@link YamlFileProcessor#generate(String, String)}. + */ + @Test + void generate_whenMultipleDocuments() { + String yaml = "username: admin\n" + "password: \"666\"\n" + "hobbies:\n" + + " - reading\n" + " - writing\n" + "---\n" + "username: adminn\n" + + "password: \"6666\"\n" + "hobbies:\n" + " - readingg\n" + + " - writingg\n"; + EnumerablePropertySource ps = new YamlFileProcessor().generate("test_generate", + yaml); + assertThat(ps.getPropertyNames()).hasSize(4); + // first document 'win' + assertThat(ps.getProperty("username")).isEqualTo("admin"); + assertThat(ps.getProperty("password")).isEqualTo("666"); + assertThat(ps.getProperty("hobbies[0]")).isEqualTo("reading"); + assertThat(ps.getProperty("hobbies[1]")).isEqualTo("writing"); + } +} diff --git a/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/test/java/com/alibaba/cloud/kubernetes/config/testsupport/KubernetesAvailable.java b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/test/java/com/alibaba/cloud/kubernetes/config/testsupport/KubernetesAvailable.java new file mode 100644 index 0000000000..c611e0a75a --- /dev/null +++ b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/test/java/com/alibaba/cloud/kubernetes/config/testsupport/KubernetesAvailable.java @@ -0,0 +1,37 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.alibaba.cloud.kubernetes.config.testsupport; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.junit.jupiter.api.extension.ExtendWith; + +/** + * Disables test execution if Kubernetes is unavailable. + * + * @author Freeman + */ +@Target({ ElementType.TYPE, ElementType.METHOD }) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@ExtendWith(KubernetesAvailableCondition.class) +public @interface KubernetesAvailable { +} diff --git a/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/test/java/com/alibaba/cloud/kubernetes/config/testsupport/KubernetesAvailableCondition.java b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/test/java/com/alibaba/cloud/kubernetes/config/testsupport/KubernetesAvailableCondition.java new file mode 100644 index 0000000000..5fafd44e54 --- /dev/null +++ b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/test/java/com/alibaba/cloud/kubernetes/config/testsupport/KubernetesAvailableCondition.java @@ -0,0 +1,50 @@ +/* + * Copyright 2013-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.alibaba.cloud.kubernetes.config.testsupport; + +import com.alibaba.cloud.kubernetes.commons.KubernetesUtils; +import io.fabric8.kubernetes.client.Config; +import io.fabric8.kubernetes.client.KubernetesClient; +import org.junit.jupiter.api.extension.ConditionEvaluationResult; +import org.junit.jupiter.api.extension.ExecutionCondition; +import org.junit.jupiter.api.extension.ExtensionContext; + +import org.springframework.util.Assert; + +/** + * @author Freeman + */ +public class KubernetesAvailableCondition implements ExecutionCondition { + + @Override + public ConditionEvaluationResult evaluateExecutionCondition( + ExtensionContext extensionContext) { + try { + Config config = KubernetesUtils.config(); + Assert.notEmpty(config.getContexts(), + "No contexts found in kubernetes config"); + try (KubernetesClient client = KubernetesUtils.newKubernetesClient()) { + client.configMaps().inNamespace(KubernetesUtils.currentNamespace()) + .list(); + } + } + catch (Throwable e) { + return ConditionEvaluationResult.disabled("Kubernetes unavailable"); + } + return ConditionEvaluationResult.enabled("Kubernetes available"); + } +} diff --git a/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/test/java/com/alibaba/cloud/kubernetes/config/testsupport/KubernetesTestUtil.java b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/test/java/com/alibaba/cloud/kubernetes/config/testsupport/KubernetesTestUtil.java new file mode 100644 index 0000000000..b4462b9ce9 --- /dev/null +++ b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/test/java/com/alibaba/cloud/kubernetes/config/testsupport/KubernetesTestUtil.java @@ -0,0 +1,75 @@ +/* + * Copyright 2013-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.alibaba.cloud.kubernetes.config.testsupport; + +import java.io.IOException; + +import com.alibaba.cloud.kubernetes.commons.KubernetesUtils; +import io.fabric8.kubernetes.api.model.ConfigMap; +import io.fabric8.kubernetes.api.model.Secret; +import io.fabric8.kubernetes.client.KubernetesClient; + +import org.springframework.core.io.ClassPathResource; + +/** + * @author Freeman + */ +public final class KubernetesTestUtil { + + private KubernetesTestUtil() { + throw new UnsupportedOperationException( + "No KubernetesTestUtil instances for you!"); + } + + static KubernetesClient cli = KubernetesUtils.newKubernetesClient(); + + public static ConfigMap configMap(String classpathFile) { + try { + return cli.configMaps().load(new ClassPathResource(classpathFile).getURL()) + .get(); + } + catch (IOException e) { + throw new RuntimeException(e); + } + } + + public static Secret secret(String classpathFile) { + try { + return cli.secrets().load(new ClassPathResource(classpathFile).getURL()) + .get(); + } + catch (IOException e) { + throw new RuntimeException(e); + } + } + + public static ConfigMap createOrReplaceConfigMap(String classpathFile) { + return cli.resource(configMap(classpathFile)).createOrReplace(); + } + + public static void deleteConfigMap(String classpathFile) { + cli.resource(configMap(classpathFile)).delete(); + } + + public static Secret createOrReplaceSecret(String classpathFile) { + return cli.resource(secret(classpathFile)).createOrReplace(); + } + + public static void deleteSecret(String classpathFile) { + cli.resource(secret(classpathFile)).delete(); + } +} diff --git a/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/test/java/com/alibaba/cloud/kubernetes/config/util/ConvertersTest.java b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/test/java/com/alibaba/cloud/kubernetes/config/util/ConvertersTest.java new file mode 100644 index 0000000000..f915b18f1d --- /dev/null +++ b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/test/java/com/alibaba/cloud/kubernetes/config/util/ConvertersTest.java @@ -0,0 +1,37 @@ +/* + * Copyright 2013-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.alibaba.cloud.kubernetes.config.util; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * {@link Converters} tester. + */ +class ConvertersTest { + + /** + * {@link Converters#stripTrailing(String)}. + */ + @Test + void stripTrailing() { + assertThat(Converters.stripTrailing("abc \n")).isEqualTo("abc"); + assertThat(Converters.stripTrailing("abc \r\t\f")).isEqualTo("abc"); + assertThat(Converters.stripTrailing(" abc ")).isEqualTo(" abc"); + } +} diff --git a/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/test/resources/application-missing-config.yml b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/test/resources/application-missing-config.yml new file mode 100644 index 0000000000..cd02470adf --- /dev/null +++ b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/test/resources/application-missing-config.yml @@ -0,0 +1,12 @@ +spring: + application: + name: missing-config + cloud: + k8s: + config: + namespace: default + configmaps: + - name: missing-configmap +logging: + level: + com.alibaba.kubernetes.config: debug diff --git a/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/test/resources/application-normal.yml b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/test/resources/application-normal.yml new file mode 100644 index 0000000000..d03d1cd7d3 --- /dev/null +++ b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/test/resources/application-normal.yml @@ -0,0 +1,17 @@ +spring: + cloud: + k8s: + config: + namespace: default + configmaps: + - name: my-configmap + refreshable: true + application: + name: normal +logging: + level: + com.alibaba.kubernetes.config: debug + +username: freeman +hobbies: + - coding \ No newline at end of file diff --git a/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/test/resources/application-not-refreshable.yml b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/test/resources/application-not-refreshable.yml new file mode 100644 index 0000000000..749bf74f1c --- /dev/null +++ b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/test/resources/application-not-refreshable.yml @@ -0,0 +1,15 @@ +spring: + cloud: + k8s: + config: + namespace: default + configmaps: + - name: configmap-01 + refreshable: true + - name: configmap-02 + refreshable: false + application: + name: not-refreshable +logging: + level: + com.alibaba.kubernetes.config: debug diff --git a/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/test/resources/application-secret.yml b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/test/resources/application-secret.yml new file mode 100644 index 0000000000..6994a84acc --- /dev/null +++ b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/test/resources/application-secret.yml @@ -0,0 +1,16 @@ +spring: + cloud: + k8s: + config: + config-maps: + - name: secret-configmap-01 + secrets: + - name: secret-secret-01 + refreshable: false + - name: secret-secret-02 + refreshable: true + application: + name: secret +logging: + level: + com.alibaba.kubernetes.config: debug diff --git a/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/test/resources/normal/configmap-changed.yaml b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/test/resources/normal/configmap-changed.yaml new file mode 100644 index 0000000000..5598db9d98 --- /dev/null +++ b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/test/resources/normal/configmap-changed.yaml @@ -0,0 +1,13 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: my-configmap + namespace: default +data: + application-default.yml: | + username: admin + password: 888 + hobbies: + - reading + - writing + - coding \ No newline at end of file diff --git a/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/test/resources/normal/configmap.yaml b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/test/resources/normal/configmap.yaml new file mode 100644 index 0000000000..312f4bb2d8 --- /dev/null +++ b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/test/resources/normal/configmap.yaml @@ -0,0 +1,12 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: my-configmap + namespace: default +data: + application-default.yml: | + username: admin + password: 666 + hobbies: + - reading + - writing \ No newline at end of file diff --git a/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/test/resources/not_refreshable/configmap-01-changed.yaml b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/test/resources/not_refreshable/configmap-01-changed.yaml new file mode 100644 index 0000000000..f88047c646 --- /dev/null +++ b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/test/resources/not_refreshable/configmap-01-changed.yaml @@ -0,0 +1,9 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: configmap-01 + namespace: default +data: + application-default.yml: | + username: admin + password: 888 \ No newline at end of file diff --git a/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/test/resources/not_refreshable/configmap-01.yaml b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/test/resources/not_refreshable/configmap-01.yaml new file mode 100644 index 0000000000..052e59b181 --- /dev/null +++ b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/test/resources/not_refreshable/configmap-01.yaml @@ -0,0 +1,9 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: configmap-01 + namespace: default +data: + application-default.yml: | + username: admin + password: 666 \ No newline at end of file diff --git a/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/test/resources/not_refreshable/configmap-02-changed.yaml b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/test/resources/not_refreshable/configmap-02-changed.yaml new file mode 100644 index 0000000000..730b1ded87 --- /dev/null +++ b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/test/resources/not_refreshable/configmap-02-changed.yaml @@ -0,0 +1,11 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: configmap-02 + namespace: default +data: + application-default.yml: | + hobbies: + - reading + - singing + - coding \ No newline at end of file diff --git a/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/test/resources/not_refreshable/configmap-02.yaml b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/test/resources/not_refreshable/configmap-02.yaml new file mode 100644 index 0000000000..9756694cf7 --- /dev/null +++ b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/test/resources/not_refreshable/configmap-02.yaml @@ -0,0 +1,10 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: configmap-02 + namespace: default +data: + application-default.yml: | + hobbies: + - reading + - writing \ No newline at end of file diff --git a/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/test/resources/secret/configmap.yaml b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/test/resources/secret/configmap.yaml new file mode 100644 index 0000000000..01c1d48ef2 --- /dev/null +++ b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/test/resources/secret/configmap.yaml @@ -0,0 +1,12 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: secret-configmap-01 + namespace: default +data: + application-default.yml: | + username: admin + password: 666 + hobbies: + - reading + - writing \ No newline at end of file diff --git a/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/test/resources/secret/secret-changed.yaml b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/test/resources/secret/secret-changed.yaml new file mode 100644 index 0000000000..7841b70dcc --- /dev/null +++ b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/test/resources/secret/secret-changed.yaml @@ -0,0 +1,9 @@ +apiVersion: v1 +kind: Secret +metadata: + name: secret-secret-01 + namespace: default +data: + username: cm9vdDIK # root2 + password: MTEyMzIyMwo= +type: Opaque \ No newline at end of file diff --git a/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/test/resources/secret/secret-refreshable-changed.yaml b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/test/resources/secret/secret-refreshable-changed.yaml new file mode 100644 index 0000000000..d4f7717429 --- /dev/null +++ b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/test/resources/secret/secret-refreshable-changed.yaml @@ -0,0 +1,8 @@ +apiVersion: v1 +kind: Secret +metadata: + name: secret-secret-02 + namespace: default +data: + price: MjAwCg== # 200 +type: Opaque \ No newline at end of file diff --git a/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/test/resources/secret/secret-refreshable.yaml b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/test/resources/secret/secret-refreshable.yaml new file mode 100644 index 0000000000..55b8ea251e --- /dev/null +++ b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/test/resources/secret/secret-refreshable.yaml @@ -0,0 +1,8 @@ +apiVersion: v1 +kind: Secret +metadata: + name: secret-secret-02 + namespace: default +data: + price: MTAwCg== # 100 +type: Opaque \ No newline at end of file diff --git a/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/test/resources/secret/secret.yaml b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/test/resources/secret/secret.yaml new file mode 100644 index 0000000000..85c6076b7e --- /dev/null +++ b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/test/resources/secret/secret.yaml @@ -0,0 +1,9 @@ +apiVersion: v1 +kind: Secret +metadata: + name: secret-secret-01 + namespace: default +data: + username: cm9vdAo= # root + password: MTEyMzIyMwo= +type: Opaque \ No newline at end of file From 233ce2045a5e2233812d125c34166bbbe9a65178 Mon Sep 17 00:00:00 2001 From: Freeman Lau Date: Mon, 30 Jan 2023 00:06:12 +0800 Subject: [PATCH 03/37] rollback docs --- spring-cloud-alibaba-docs/src/main/asciidoc-zh/sentinel.adoc | 2 +- spring-cloud-alibaba-docs/src/main/asciidoc/sentinel.adoc | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/spring-cloud-alibaba-docs/src/main/asciidoc-zh/sentinel.adoc b/spring-cloud-alibaba-docs/src/main/asciidoc-zh/sentinel.adoc index a29105a13e..2a45f6d134 100644 --- a/spring-cloud-alibaba-docs/src/main/asciidoc-zh/sentinel.adoc +++ b/spring-cloud-alibaba-docs/src/main/asciidoc-zh/sentinel.adoc @@ -240,7 +240,7 @@ spring.cloud.sentinel.datasource.ds4.apollo.rule-type=param-flow 这种配置方式参考了 Spring Cloud Stream Binder 的配置,内部使用了 `TreeMap` 进行存储,comparator 为 `String.CASE_INSENSITIVE_ORDER` 。 -NOTE: d1, ds2, ds3, ds4 是 `ReadableDataSource` 的名字,可随意编写。后面的 `processor` ,`zk` ,`nacos` , `apollo` 就是对应具体的数据源。 它们后面的配置就是这些具体数据源各自的配置。 +NOTE: d1, ds2, ds3, ds4 是 `ReadableDataSource` 的名字,可随意编写。后面的 `file` ,`zk` ,`nacos` , `apollo` 就是对应具体的数据源。 它们后面的配置就是这些具体数据源各自的配置。 每种数据源都有两个共同的配置项: `data-type`、 `converter-class` 以及 `rule-type`。 diff --git a/spring-cloud-alibaba-docs/src/main/asciidoc/sentinel.adoc b/spring-cloud-alibaba-docs/src/main/asciidoc/sentinel.adoc index 645787a5b1..0dfac80140 100644 --- a/spring-cloud-alibaba-docs/src/main/asciidoc/sentinel.adoc +++ b/spring-cloud-alibaba-docs/src/main/asciidoc/sentinel.adoc @@ -243,7 +243,7 @@ spring.cloud.sentinel.datasource.ds4.apollo.rule-type=param-flow This method follows the configuration of Spring Cloud Stream Binder. `TreeMap` is used for storage internally, and comparator is `String.CASE_INSENSITIVE_ORDER`. -NOTE: d1, ds2, ds3, ds4 are the names of `ReadableDataSource`, and can be coded as you like. The `processor`, `zk`, `nacos` , `apollo` refer to the specific data sources. The configurations following them are the specific configurations of these data sources respecitively. +NOTE: d1, ds2, ds3, ds4 are the names of `ReadableDataSource`, and can be coded as you like. The `file`, `zk`, `nacos` , `apollo` refer to the specific data sources. The configurations following them are the specific configurations of these data sources respecitively. Every data source has 3 common configuration items: `data-type`, `converter-class` and `rule-type`. From bb5e46121043662e2bbad22474b8882201dc49c9 Mon Sep 17 00:00:00 2001 From: Freeman Lau Date: Mon, 30 Jan 2023 23:58:31 +0800 Subject: [PATCH 04/37] not use raw type --- .../kubernetes/config/processor/JsonFileProcessor.java | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/processor/JsonFileProcessor.java b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/processor/JsonFileProcessor.java index 006e4e6966..482ce0c99b 100644 --- a/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/processor/JsonFileProcessor.java +++ b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/processor/JsonFileProcessor.java @@ -52,7 +52,6 @@ public boolean hit(String fileName) { } @Override - @SuppressWarnings("rawtypes") public EnumerablePropertySource generate(String name, String content) { if (content.trim().startsWith("{")) { // json object @@ -60,7 +59,7 @@ public EnumerablePropertySource generate(String name, String content) { } CompositePropertySource result = new CompositePropertySource(name); try { - List list = objectMapper.readValue(content, List.class); + List list = objectMapper.readValue(content, List.class); if (list.isEmpty()) { return result; } @@ -80,13 +79,12 @@ public EnumerablePropertySource generate(String name, String content) { return result; } - @SuppressWarnings("rawtypes") private static CompositePropertySource convertJsonObjectStringToPropertySource( String name, String jsonObjectString) { // We don't want to change the Spring default behavior // this is how we convert json to PropertySource // json -> java.util.Map -> yaml -> PropertySource - Map map = new HashMap<>(); + Map map = new HashMap<>(); try { map = objectMapper.readValue(jsonObjectString, Map.class); } From 6b82542069a122080453750bb8bc38d697e118f1 Mon Sep 17 00:00:00 2001 From: Freeman Lau Date: Tue, 31 Jan 2023 00:48:24 +0800 Subject: [PATCH 05/37] remove dummy methods --- .../core/ConfigEnvironmentPostProcessor.java | 50 ++++++++----------- .../kubernetes/config/core/ConfigWatcher.java | 50 ++++++++----------- .../core/HasMetadataResourceEventHandler.java | 12 +++-- .../kubernetes/config/util/ResourceKey.java | 18 +++---- .../cloud/kubernetes/config/util/Util.java | 50 ++----------------- 5 files changed, 63 insertions(+), 117 deletions(-) diff --git a/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/core/ConfigEnvironmentPostProcessor.java b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/core/ConfigEnvironmentPostProcessor.java index 92d33036b9..77979165dd 100644 --- a/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/core/ConfigEnvironmentPostProcessor.java +++ b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/core/ConfigEnvironmentPostProcessor.java @@ -28,7 +28,6 @@ import com.alibaba.cloud.kubernetes.config.util.Converters; import com.alibaba.cloud.kubernetes.config.util.Pair; import com.alibaba.cloud.kubernetes.config.util.RefreshContext; -import com.alibaba.cloud.kubernetes.config.util.Util; import io.fabric8.kubernetes.api.model.ConfigMap; import io.fabric8.kubernetes.api.model.Secret; import io.fabric8.kubernetes.client.KubernetesClient; @@ -99,13 +98,17 @@ else if (resource instanceof Secret) { private static KubernetesConfigProperties getKubernetesConfigProperties( ConfigurableEnvironment environment) { - return Optional.ofNullable(RefreshContext.get()) - .map(context -> context.applicationContext() - .getBean(KubernetesConfigProperties.class)) - .orElse(Binder.get(environment) - .bind(KubernetesConfigProperties.PREFIX, - KubernetesConfigProperties.class) - .orElseGet(KubernetesConfigProperties::new)); + return Optional + .ofNullable(RefreshContext.get()).map(context -> context + .applicationContext().getBean(KubernetesConfigProperties.class)) + .orElseGet(() -> { + KubernetesConfigProperties prop = Binder.get(environment) + .bind(KubernetesConfigProperties.PREFIX, + KubernetesConfigProperties.class) + .orElseGet(KubernetesConfigProperties::new); + prop.afterPropertiesSet(); + return prop; + }); } private void pullConfigMaps(KubernetesConfigProperties properties, @@ -113,8 +116,7 @@ private void pullConfigMaps(KubernetesConfigProperties properties, properties.getConfigMaps().stream() .map(configmap -> Optional .ofNullable(propertySourceForConfigMap(configmap, properties)) - .map(ps -> Pair.of(Util.preference(configmap, properties), ps)) - .orElse(null)) + .map(ps -> Pair.of(configmap.getPreference(), ps)).orElse(null)) .filter(Objects::nonNull) .collect(groupingBy(Pair::key, mapping(Pair::value, toList()))) .forEach((configPreference, remotePropertySources) -> { @@ -128,8 +130,7 @@ private void pullSecrets(KubernetesConfigProperties properties, properties.getSecrets().stream() .map(secret -> Optional .ofNullable(propertySourceForSecret(secret, properties)) - .map(ps -> Pair.of(Util.preference(secret, properties), ps)) - .orElse(null)) + .map(ps -> Pair.of(secret.getPreference(), ps)).orElse(null)) .filter(Objects::nonNull) .collect(groupingBy(Pair::key, mapping(Pair::value, toList()))) .forEach((configPreference, remotePropertySources) -> { @@ -162,16 +163,16 @@ private static void addPropertySourcesToEnvironment( private EnumerablePropertySource propertySourceForConfigMap( KubernetesConfigProperties.ConfigMap cm, KubernetesConfigProperties properties) { - if (noNeedToReloadResource(Util.refreshable(cm, properties))) { + if (isRefreshing() && !cm.getRefreshable()) { return null; } - ConfigMap configMap = client.configMaps() - .inNamespace(Util.namespace(cm, properties)).withName(cm.getName()).get(); + ConfigMap configMap = client.configMaps().inNamespace(cm.getNamespace()) + .withName(cm.getName()).get(); if (configMap == null) { log.warn(String.format("ConfigMap '%s' not found in namespace '%s'", - cm.getName(), Util.namespace(cm, properties))); + cm.getName(), cm.getNamespace())); failApplicationStartUpIfNecessary(ConfigMap.class, cm.getName(), - Util.namespace(cm, properties), properties); + cm.getNamespace(), properties); return null; } return Converters.toPropertySource(configMap); @@ -187,28 +188,21 @@ private static void failApplicationStartUpIfNecessary(Class type, String name private EnumerablePropertySource propertySourceForSecret( KubernetesConfigProperties.Secret secret, KubernetesConfigProperties properties) { - if (noNeedToReloadResource(Util.refreshable(secret, properties))) { + if (isRefreshing() && !secret.getRefreshable()) { return null; } - Secret secretInK8s = client.secrets() - .inNamespace(Util.namespace(secret, properties)) + Secret secretInK8s = client.secrets().inNamespace(secret.getNamespace()) .withName(secret.getName()).get(); if (secretInK8s == null) { log.warn(String.format("Secret '%s' not found in namespace '%s'", - secret.getName(), Util.namespace(secret, properties))); + secret.getName(), secret.getNamespace())); failApplicationStartUpIfNecessary(Secret.class, secret.getName(), - Util.namespace(secret, properties), properties); + secret.getNamespace(), properties); return null; } return Converters.toPropertySource(secretInK8s); } - private static boolean noNeedToReloadResource(boolean refreshable) { - // If this is a refresh event, we need to ignore the resource that not enabled - // auto refresh. - return isRefreshing() && !refreshable; - } - private static boolean isRefreshing() { return RefreshContext.get() != null; } diff --git a/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/core/ConfigWatcher.java b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/core/ConfigWatcher.java index c5837ddbef..fb8da05c77 100644 --- a/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/core/ConfigWatcher.java +++ b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/core/ConfigWatcher.java @@ -23,7 +23,6 @@ import com.alibaba.cloud.kubernetes.config.KubernetesConfigProperties; import com.alibaba.cloud.kubernetes.config.util.ResourceKey; -import com.alibaba.cloud.kubernetes.config.util.Util; import io.fabric8.kubernetes.api.model.ConfigMap; import io.fabric8.kubernetes.api.model.HasMetadata; import io.fabric8.kubernetes.api.model.Secret; @@ -37,17 +36,16 @@ import org.springframework.beans.factory.SmartInitializingSingleton; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; -import org.springframework.context.EnvironmentAware; -import org.springframework.core.env.ConfigurableEnvironment; -import org.springframework.core.env.Environment; + +import static com.alibaba.cloud.kubernetes.config.util.Util.resourceKey; /** * Watcher for config resource changes. * * @author Freeman */ -public class ConfigWatcher implements SmartInitializingSingleton, ApplicationContextAware, - EnvironmentAware, DisposableBean { +public class ConfigWatcher + implements SmartInitializingSingleton, ApplicationContextAware, DisposableBean { private static final Logger log = LoggerFactory.getLogger(ConfigWatcher.class); private final Map> configmapInformers = new LinkedHashMap<>(); @@ -56,22 +54,12 @@ public class ConfigWatcher implements SmartInitializingSingleton, ApplicationCon private final KubernetesClient client; private ApplicationContext context; - private ConfigurableEnvironment environment; public ConfigWatcher(KubernetesConfigProperties properties, KubernetesClient client) { this.properties = properties; this.client = client; } - @Override - public void setEnvironment(Environment environment) { - if (!(environment instanceof ConfigurableEnvironment)) { - throw new IllegalStateException( - "Environment must be an instance of ConfigurableEnvironment"); - } - this.environment = (ConfigurableEnvironment) environment; - } - @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { @@ -92,21 +80,25 @@ public void destroy() { private void watchRefreshableResources(KubernetesConfigProperties properties, KubernetesClient client) { - properties.getConfigMaps().stream().filter(cm -> Util.refreshable(cm, properties)) - .forEach(cm -> configmapInformers.put(Util.resourceKey(cm, properties), - client.configMaps().inNamespace(Util.namespace(cm, properties)) - .withName(cm.getName()) - .inform(new HasMetadataResourceEventHandler<>(context, - environment, properties)))); + properties.getConfigMaps().stream() + .filter(KubernetesConfigProperties.ConfigMap::getRefreshable) + .forEach(cm -> { + SharedIndexInformer informer = client.configMaps() + .inNamespace(cm.getNamespace()).withName(cm.getName()) + .inform(new HasMetadataResourceEventHandler<>(context, + properties)); + configmapInformers.put(resourceKey(cm), informer); + }); log(configmapInformers); properties.getSecrets().stream() - .filter(secret -> Util.refreshable(secret, properties)) - .forEach(secret -> secretInformers.put( - Util.resourceKey(secret, properties), - client.secrets().inNamespace(Util.namespace(secret, properties)) - .withName(secret.getName()) - .inform(new HasMetadataResourceEventHandler<>(context, - environment, properties)))); + .filter(KubernetesConfigProperties.Secret::getRefreshable) + .forEach(secret -> { + SharedIndexInformer informer = client.secrets() + .inNamespace(secret.getNamespace()).withName(secret.getName()) + .inform(new HasMetadataResourceEventHandler<>(context, + properties)); + secretInformers.put(resourceKey(secret), informer); + }); log(secretInformers); } diff --git a/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/core/HasMetadataResourceEventHandler.java b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/core/HasMetadataResourceEventHandler.java index ad050e2a01..eb0cce3fbd 100644 --- a/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/core/HasMetadataResourceEventHandler.java +++ b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/core/HasMetadataResourceEventHandler.java @@ -37,13 +37,11 @@ public class HasMetadataResourceEventHandler .getLogger(HasMetadataResourceEventHandler.class); private final ApplicationContext context; - private final ConfigurableEnvironment environment; private final KubernetesConfigProperties properties; public HasMetadataResourceEventHandler(ApplicationContext context, - ConfigurableEnvironment environment, KubernetesConfigProperties properties) { + KubernetesConfigProperties properties) { this.context = context; - this.environment = environment; this.properties = properties; } @@ -81,13 +79,17 @@ public void onDelete(HasMetadata obj, boolean deletedFinalStateUnknown) { refresh(obj); } else { - log.info("Refresh on delete is disabled, ignore the delete event"); + log.info( + "{} '{}' was deleted in namespace '{}', refresh on delete is disabled, ignore the delete event", + obj.getKind(), obj.getMetadata().getName(), + obj.getMetadata().getNamespace()); } } private void deletePropertySourceOfResource(HasMetadata resource) { String propertySourceName = Converters.propertySourceNameForResource(resource); - environment.getPropertySources().remove(propertySourceName); + ((ConfigurableEnvironment) context.getEnvironment()).getPropertySources() + .remove(propertySourceName); } private void refresh(HasMetadata obj) { diff --git a/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/util/ResourceKey.java b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/util/ResourceKey.java index 936036a92f..d7427aec6f 100644 --- a/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/util/ResourceKey.java +++ b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/util/ResourceKey.java @@ -25,14 +25,13 @@ public final class ResourceKey { private final String type; private final String name; private final String namespace; - private final boolean refreshEnabled; + private final boolean refreshable; - public ResourceKey(String type, String name, String namespace, - boolean refreshEnabled) { + public ResourceKey(String type, String name, String namespace, boolean refreshable) { this.type = type; this.name = name; this.namespace = namespace; - this.refreshEnabled = refreshEnabled; + this.refreshable = refreshable; } public String type() { @@ -47,8 +46,8 @@ public String namespace() { return namespace; } - public boolean refreshEnabled() { - return refreshEnabled; + public boolean refreshable() { + return refreshable; } @Override @@ -63,18 +62,17 @@ public boolean equals(Object obj) { return Objects.equals(this.type, that.type) && Objects.equals(this.name, that.name) && Objects.equals(this.namespace, that.namespace) - && this.refreshEnabled == that.refreshEnabled; + && this.refreshable == that.refreshable; } @Override public int hashCode() { - return Objects.hash(type, name, namespace, refreshEnabled); + return Objects.hash(type, name, namespace, refreshable); } @Override public String toString() { return "ResourceKey[" + "type=" + type + ", " + "name=" + name + ", " - + "namespace=" + namespace + ", " + "refreshEnabled=" + refreshEnabled - + ']'; + + "namespace=" + namespace + ", " + "refreshEnabled=" + refreshable + ']'; } } diff --git a/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/util/Util.java b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/util/Util.java index e801000dc6..a05aadcab4 100644 --- a/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/util/Util.java +++ b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/util/Util.java @@ -16,8 +16,6 @@ package com.alibaba.cloud.kubernetes.config.util; -import java.util.Optional; - import com.alibaba.cloud.kubernetes.config.KubernetesConfigProperties; import io.fabric8.kubernetes.api.model.ConfigMap; import io.fabric8.kubernetes.api.model.Secret; @@ -31,52 +29,14 @@ private Util() { throw new UnsupportedOperationException("No Util instances for you!"); } - public static ResourceKey resourceKey(KubernetesConfigProperties.ConfigMap configMap, - KubernetesConfigProperties properties) { + public static ResourceKey resourceKey( + KubernetesConfigProperties.ConfigMap configMap) { return new ResourceKey(ConfigMap.class.getSimpleName(), configMap.getName(), - namespace(configMap, properties), refreshable(configMap, properties)); + configMap.getNamespace(), configMap.getRefreshable()); } - public static ResourceKey resourceKey(KubernetesConfigProperties.Secret secret, - KubernetesConfigProperties properties) { + public static ResourceKey resourceKey(KubernetesConfigProperties.Secret secret) { return new ResourceKey(Secret.class.getSimpleName(), secret.getName(), - namespace(secret, properties), refreshable(secret, properties)); - } - - public static String namespace(KubernetesConfigProperties.ConfigMap configMap, - KubernetesConfigProperties properties) { - return Optional.ofNullable(configMap.getNamespace()) - .orElseGet(properties::getNamespace); - } - - public static String namespace(KubernetesConfigProperties.Secret secret, - KubernetesConfigProperties properties) { - return Optional.ofNullable(secret.getNamespace()) - .orElseGet(properties::getNamespace); - } - - public static boolean refreshable(KubernetesConfigProperties.ConfigMap configMap, - KubernetesConfigProperties properties) { - return Optional.ofNullable(configMap.getRefreshable()) - .orElseGet(properties::isRefreshable); - } - - public static boolean refreshable(KubernetesConfigProperties.Secret secret, - KubernetesConfigProperties properties) { - return Optional.ofNullable(secret.getRefreshable()) - .orElseGet(properties::isRefreshable); - } - - public static ConfigPreference preference( - KubernetesConfigProperties.ConfigMap configMap, - KubernetesConfigProperties properties) { - return Optional.ofNullable(configMap.getPreference()) - .orElseGet(properties::getPreference); - } - - public static ConfigPreference preference(KubernetesConfigProperties.Secret secret, - KubernetesConfigProperties properties) { - return Optional.ofNullable(secret.getPreference()) - .orElseGet(properties::getPreference); + secret.getNamespace(), secret.getRefreshable()); } } From d425fa28dab4772829efd9ad8cb6eb6116078952 Mon Sep 17 00:00:00 2001 From: Freeman Lau Date: Tue, 31 Jan 2023 00:52:52 +0800 Subject: [PATCH 06/37] add comments --- .../cloud/kubernetes/commons/KubernetesUtils.java | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-alibaba-kubernetes-commons/src/main/java/com/alibaba/cloud/kubernetes/commons/KubernetesUtils.java b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-alibaba-kubernetes-commons/src/main/java/com/alibaba/cloud/kubernetes/commons/KubernetesUtils.java index 286c621c96..71610b8d04 100644 --- a/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-alibaba-kubernetes-commons/src/main/java/com/alibaba/cloud/kubernetes/commons/KubernetesUtils.java +++ b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-alibaba-kubernetes-commons/src/main/java/com/alibaba/cloud/kubernetes/commons/KubernetesUtils.java @@ -32,10 +32,25 @@ private KubernetesUtils() { private static final Config config = new ConfigBuilder().build(); + /** + * Get the kube config. + * + * @return Config + */ public static Config config() { return config; } + /** + * Get the current namespace. + *

+ * If in kubernetes, it will return the namespace of the pod. + *

+ * If not in kubernetes, it will return the namespace of the kube config current + * context. + * + * @return namespace + */ public static String currentNamespace() { return config.getNamespace(); } From bcd5756846b8656a676abbecfd90b44fc543c56c Mon Sep 17 00:00:00 2001 From: Freeman Lau Date: Tue, 31 Jan 2023 00:56:42 +0800 Subject: [PATCH 07/37] rename to Preference --- .../config/KubernetesConfigProperties.java | 22 +++++++++---------- .../core/ConfigEnvironmentPostProcessor.java | 16 +++++++------- ...{ConfigPreference.java => Preference.java} | 8 ++++--- 3 files changed, 24 insertions(+), 22 deletions(-) rename spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/util/{ConfigPreference.java => Preference.java} (89%) diff --git a/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/KubernetesConfigProperties.java b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/KubernetesConfigProperties.java index 735b5bd423..582aef257d 100644 --- a/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/KubernetesConfigProperties.java +++ b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/KubernetesConfigProperties.java @@ -21,7 +21,7 @@ import java.util.Objects; import com.alibaba.cloud.kubernetes.commons.KubernetesUtils; -import com.alibaba.cloud.kubernetes.config.util.ConfigPreference; +import com.alibaba.cloud.kubernetes.config.util.Preference; import org.springframework.beans.factory.InitializingBean; import org.springframework.boot.context.properties.ConfigurationProperties; @@ -52,10 +52,10 @@ public class KubernetesConfigProperties implements InitializingBean { private String namespace = determineNamespace(); /** - * Config preference, default is {@link ConfigPreference#REMOTE}, means remote + * Config preference, default is {@link Preference#REMOTE}, means remote * configurations 'win', will override the local configurations. */ - private ConfigPreference preference = ConfigPreference.REMOTE; + private Preference preference = Preference.REMOTE; /** * Whether to refresh environment when remote resource was deleted, default value is @@ -100,11 +100,11 @@ public void setNamespace(String namespace) { this.namespace = namespace; } - public ConfigPreference getPreference() { + public Preference getPreference() { return preference; } - public void setPreference(ConfigPreference preference) { + public void setPreference(Preference preference) { this.preference = preference; } @@ -248,7 +248,7 @@ public static class ConfigMap { * {@code spring.cloud.k8s.config.preference} if not * set. */ - private ConfigPreference preference; + private Preference preference; public String getName() { return name; @@ -274,11 +274,11 @@ public void setRefreshable(Boolean refreshable) { this.refreshable = refreshable; } - public ConfigPreference getPreference() { + public Preference getPreference() { return preference; } - public void setPreference(ConfigPreference preference) { + public void setPreference(Preference preference) { this.preference = preference; } @@ -335,7 +335,7 @@ public static class Secret { * {@code spring.cloud.k8s.config.preference} if not * set. */ - private ConfigPreference preference; + private Preference preference; public String getName() { return name; @@ -361,11 +361,11 @@ public void setRefreshable(Boolean refreshable) { this.refreshable = refreshable; } - public ConfigPreference getPreference() { + public Preference getPreference() { return preference; } - public void setPreference(ConfigPreference preference) { + public void setPreference(Preference preference) { this.preference = preference; } diff --git a/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/core/ConfigEnvironmentPostProcessor.java b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/core/ConfigEnvironmentPostProcessor.java index 77979165dd..70856186ee 100644 --- a/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/core/ConfigEnvironmentPostProcessor.java +++ b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/core/ConfigEnvironmentPostProcessor.java @@ -24,9 +24,9 @@ import com.alibaba.cloud.kubernetes.commons.KubernetesClientHolder; import com.alibaba.cloud.kubernetes.config.KubernetesConfigProperties; import com.alibaba.cloud.kubernetes.config.exception.ConfigMissingException; -import com.alibaba.cloud.kubernetes.config.util.ConfigPreference; import com.alibaba.cloud.kubernetes.config.util.Converters; import com.alibaba.cloud.kubernetes.config.util.Pair; +import com.alibaba.cloud.kubernetes.config.util.Preference; import com.alibaba.cloud.kubernetes.config.util.RefreshContext; import io.fabric8.kubernetes.api.model.ConfigMap; import io.fabric8.kubernetes.api.model.Secret; @@ -119,8 +119,8 @@ private void pullConfigMaps(KubernetesConfigProperties properties, .map(ps -> Pair.of(configmap.getPreference(), ps)).orElse(null)) .filter(Objects::nonNull) .collect(groupingBy(Pair::key, mapping(Pair::value, toList()))) - .forEach((configPreference, remotePropertySources) -> { - addPropertySourcesToEnvironment(environment, configPreference, + .forEach((preference, remotePropertySources) -> { + addPropertySourcesToEnvironment(environment, preference, remotePropertySources); }); } @@ -133,17 +133,17 @@ private void pullSecrets(KubernetesConfigProperties properties, .map(ps -> Pair.of(secret.getPreference(), ps)).orElse(null)) .filter(Objects::nonNull) .collect(groupingBy(Pair::key, mapping(Pair::value, toList()))) - .forEach((configPreference, remotePropertySources) -> { - addPropertySourcesToEnvironment(environment, configPreference, + .forEach((preference, remotePropertySources) -> { + addPropertySourcesToEnvironment(environment, preference, remotePropertySources); }); } private static void addPropertySourcesToEnvironment( - ConfigurableEnvironment environment, ConfigPreference configPreference, + ConfigurableEnvironment environment, Preference preference, List> remotePropertySources) { MutablePropertySources propertySources = environment.getPropertySources(); - switch (configPreference) { + switch (preference) { case LOCAL: // The latter config should win the previous config Collections.reverse(remotePropertySources); @@ -156,7 +156,7 @@ private static void addPropertySourcesToEnvironment( break; default: throw new IllegalArgumentException( - "Unknown config preference: " + configPreference.name()); + "Unknown config preference: " + preference.name()); } } diff --git a/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/util/ConfigPreference.java b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/util/Preference.java similarity index 89% rename from spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/util/ConfigPreference.java rename to spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/util/Preference.java index e242151247..20cb036eeb 100644 --- a/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/util/ConfigPreference.java +++ b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/util/Preference.java @@ -19,13 +19,15 @@ /** * @author Freeman */ -public enum ConfigPreference { +public enum Preference { /** - * LOCAL means local configuration has higher priority and will override the remote configuration. + * LOCAL means local configuration has higher priority and will override the remote + * configuration. */ LOCAL, /** - * REMOTE means remote configuration has higher priority and will override the local configuration. + * REMOTE means remote configuration has higher priority and will override the local + * configuration. */ REMOTE } From 4eda9572c9a6271af7f7cd970192f281dbdf99d0 Mon Sep 17 00:00:00 2001 From: Freeman Lau Date: Tue, 31 Jan 2023 21:16:03 +0800 Subject: [PATCH 08/37] continue to optimize the code --- .../KubernetesConfigAutoConfiguration.java | 4 +- .../core/ConfigEnvironmentPostProcessor.java | 13 +++++- .../kubernetes/config/core/ConfigWatcher.java | 9 ++-- .../config/processor/FileProcessor.java | 3 ++ .../config/processor/JsonFileProcessor.java | 2 + .../processor/PropertiesFileProcessor.java | 2 + .../config/processor/YamlFileProcessor.java | 2 + .../kubernetes/config/util/Converters.java | 5 +-- .../kubernetes/config/util/ResourceKey.java | 28 +++++------- .../util/{Util.java => ResourceKeyUtils.java} | 22 +++++++--- .../SinglePairPropertySource.java | 4 +- .../processor/JsonFileProcessorTest.java | 43 ++++++++++++++----- .../PropertiesFileProcessorTest.java | 9 +++- .../processor/YamlFileProcessorTest.java | 28 +++++++++--- 14 files changed, 121 insertions(+), 53 deletions(-) rename spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/util/{Util.java => ResourceKeyUtils.java} (66%) rename spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/{core => util}/SinglePairPropertySource.java (92%) diff --git a/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/KubernetesConfigAutoConfiguration.java b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/KubernetesConfigAutoConfiguration.java index 70f1b5c638..e31a3748bb 100644 --- a/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/KubernetesConfigAutoConfiguration.java +++ b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/KubernetesConfigAutoConfiguration.java @@ -22,6 +22,7 @@ import io.fabric8.kubernetes.client.KubernetesClient; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; @@ -39,7 +40,8 @@ public class KubernetesConfigAutoConfiguration { @Bean - public ConfigWatcher configWatcher(KubernetesConfigProperties properties, + @ConditionalOnMissingBean + public ConfigWatcher KubernetesConfigWatcher(KubernetesConfigProperties properties, KubernetesClient kubernetesClient) { return new ConfigWatcher(properties, kubernetesClient); } diff --git a/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/core/ConfigEnvironmentPostProcessor.java b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/core/ConfigEnvironmentPostProcessor.java index 70856186ee..3120e76b5c 100644 --- a/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/core/ConfigEnvironmentPostProcessor.java +++ b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/core/ConfigEnvironmentPostProcessor.java @@ -178,9 +178,20 @@ private EnumerablePropertySource propertySourceForConfigMap( return Converters.toPropertySource(configMap); } + /** + * Fail the application start up if necessary when the resource is missing. + * + *

+ * NOTE: do nothing if the application is refreshing + * + * @param type the type of the resource + * @param name the name of the resource + * @param namespace the namespace of the resource + * @param properties {@link KubernetesConfigProperties} + */ private static void failApplicationStartUpIfNecessary(Class type, String name, String namespace, KubernetesConfigProperties properties) { - if (properties.isFailOnMissingConfig()) { + if (!isRefreshing() && properties.isFailOnMissingConfig()) { throw new ConfigMissingException(type, name, namespace); } } diff --git a/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/core/ConfigWatcher.java b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/core/ConfigWatcher.java index fb8da05c77..dc6c994eed 100644 --- a/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/core/ConfigWatcher.java +++ b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/core/ConfigWatcher.java @@ -37,7 +37,7 @@ import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; -import static com.alibaba.cloud.kubernetes.config.util.Util.resourceKey; +import static com.alibaba.cloud.kubernetes.config.util.ResourceKeyUtils.resourceKey; /** * Watcher for config resource changes. @@ -82,12 +82,13 @@ private void watchRefreshableResources(KubernetesConfigProperties properties, KubernetesClient client) { properties.getConfigMaps().stream() .filter(KubernetesConfigProperties.ConfigMap::getRefreshable) - .forEach(cm -> { + .forEach(configmap -> { SharedIndexInformer informer = client.configMaps() - .inNamespace(cm.getNamespace()).withName(cm.getName()) + .inNamespace(configmap.getNamespace()) + .withName(configmap.getName()) .inform(new HasMetadataResourceEventHandler<>(context, properties)); - configmapInformers.put(resourceKey(cm), informer); + configmapInformers.put(resourceKey(configmap), informer); }); log(configmapInformers); properties.getSecrets().stream() diff --git a/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/processor/FileProcessor.java b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/processor/FileProcessor.java index 04673d567f..924a16c6de 100644 --- a/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/processor/FileProcessor.java +++ b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/processor/FileProcessor.java @@ -17,8 +17,11 @@ package com.alibaba.cloud.kubernetes.config.processor; import org.springframework.core.env.EnumerablePropertySource; +import org.springframework.core.env.PropertySource; /** + * {@link FileProcessor} use to generate {@link PropertySource} from file content. + * * @author Freeman */ public interface FileProcessor { diff --git a/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/processor/JsonFileProcessor.java b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/processor/JsonFileProcessor.java index 482ce0c99b..16b9310609 100644 --- a/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/processor/JsonFileProcessor.java +++ b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/processor/JsonFileProcessor.java @@ -35,6 +35,8 @@ import org.springframework.core.io.ByteArrayResource; /** + * Convert JSON string to {@link PropertySource}, support JSON array. + * * @author Freeman */ public class JsonFileProcessor implements FileProcessor { diff --git a/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/processor/PropertiesFileProcessor.java b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/processor/PropertiesFileProcessor.java index bbab1194a8..b5a9067b7f 100644 --- a/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/processor/PropertiesFileProcessor.java +++ b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/processor/PropertiesFileProcessor.java @@ -31,6 +31,8 @@ import org.springframework.core.io.ByteArrayResource; /** + * Convert properties file to {@link PropertySource}. + * * @author Freeman */ public class PropertiesFileProcessor implements FileProcessor { diff --git a/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/processor/YamlFileProcessor.java b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/processor/YamlFileProcessor.java index a3f792317f..59ee71c832 100644 --- a/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/processor/YamlFileProcessor.java +++ b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/processor/YamlFileProcessor.java @@ -32,6 +32,8 @@ import org.springframework.core.io.Resource; /** + * Convert yaml file to {@link PropertySource}, support multi-document yaml file. + * * @author Freeman */ public class YamlFileProcessor implements FileProcessor { diff --git a/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/util/Converters.java b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/util/Converters.java index ed931042c0..3e41694821 100644 --- a/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/util/Converters.java +++ b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/util/Converters.java @@ -23,7 +23,6 @@ import java.util.Map; import java.util.stream.Collectors; -import com.alibaba.cloud.kubernetes.config.core.SinglePairPropertySource; import com.alibaba.cloud.kubernetes.config.processor.FileProcessor; import io.fabric8.kubernetes.api.model.ConfigMap; import io.fabric8.kubernetes.api.model.HasMetadata; @@ -101,7 +100,7 @@ public static EnumerablePropertySource toPropertySource(ConfigMap configMap) public static EnumerablePropertySource toPropertySource(Secret secret) { // data is base64 encoded Map data = secret.getData(); - Map encodedValue = new LinkedHashMap<>(data); + Map encodedValue = new LinkedHashMap<>(data); data.replaceAll((key, value) -> stripTrailing( new String(Base64.getDecoder().decode(value), UTF_8))); // secret will add // newlines @@ -111,7 +110,7 @@ public static EnumerablePropertySource toPropertySource(Secret secret) { propertySourceNameForResource(secret)); result.addPropertySource(toPropertySource( propertySourceNameForResource(secret) + "[decoded]", decodedValue)); - result.addPropertySource(new MapPropertySource( + result.addPropertySource(toPropertySource( propertySourceNameForResource(secret) + "[encoded]", encodedValue)); return result; } diff --git a/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/util/ResourceKey.java b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/util/ResourceKey.java index d7427aec6f..b291a2b852 100644 --- a/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/util/ResourceKey.java +++ b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/util/ResourceKey.java @@ -25,13 +25,11 @@ public final class ResourceKey { private final String type; private final String name; private final String namespace; - private final boolean refreshable; - public ResourceKey(String type, String name, String namespace, boolean refreshable) { + public ResourceKey(String type, String name, String namespace) { this.type = type; this.name = name; this.namespace = namespace; - this.refreshable = refreshable; } public String type() { @@ -46,33 +44,27 @@ public String namespace() { return namespace; } - public boolean refreshable() { - return refreshable; - } - @Override - public boolean equals(Object obj) { - if (obj == this) { + public boolean equals(Object o) { + if (this == o) { return true; } - if (obj == null || obj.getClass() != this.getClass()) { + if (o == null || getClass() != o.getClass()) { return false; } - ResourceKey that = (ResourceKey) obj; - return Objects.equals(this.type, that.type) - && Objects.equals(this.name, that.name) - && Objects.equals(this.namespace, that.namespace) - && this.refreshable == that.refreshable; + ResourceKey that = (ResourceKey) o; + return Objects.equals(type, that.type) && Objects.equals(name, that.name) + && Objects.equals(namespace, that.namespace); } @Override public int hashCode() { - return Objects.hash(type, name, namespace, refreshable); + return Objects.hash(type, name, namespace); } @Override public String toString() { - return "ResourceKey[" + "type=" + type + ", " + "name=" + name + ", " - + "namespace=" + namespace + ", " + "refreshEnabled=" + refreshable + ']'; + return "ResourceKey{" + "type='" + type + '\'' + ", name='" + name + '\'' + + ", namespace='" + namespace + '\'' + '}'; } } diff --git a/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/util/Util.java b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/util/ResourceKeyUtils.java similarity index 66% rename from spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/util/Util.java rename to spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/util/ResourceKeyUtils.java index a05aadcab4..93a799a09f 100644 --- a/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/util/Util.java +++ b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/util/ResourceKeyUtils.java @@ -23,20 +23,32 @@ /** * @author Freeman */ -public final class Util { +public final class ResourceKeyUtils { - private Util() { - throw new UnsupportedOperationException("No Util instances for you!"); + private ResourceKeyUtils() { + throw new UnsupportedOperationException("No ResourceKeyUtils instances for you!"); } + /** + * Generate a {@link ResourceKey} from {@link KubernetesConfigProperties.ConfigMap}. + * + * @param configMap {@link KubernetesConfigProperties.ConfigMap} + * @return {@link ResourceKey} + */ public static ResourceKey resourceKey( KubernetesConfigProperties.ConfigMap configMap) { return new ResourceKey(ConfigMap.class.getSimpleName(), configMap.getName(), - configMap.getNamespace(), configMap.getRefreshable()); + configMap.getNamespace()); } + /** + * Generate a {@link ResourceKey} from {@link KubernetesConfigProperties.Secret}. + * + * @param secret {@link KubernetesConfigProperties.Secret} + * @return {@link ResourceKey} + */ public static ResourceKey resourceKey(KubernetesConfigProperties.Secret secret) { return new ResourceKey(Secret.class.getSimpleName(), secret.getName(), - secret.getNamespace(), secret.getRefreshable()); + secret.getNamespace()); } } diff --git a/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/core/SinglePairPropertySource.java b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/util/SinglePairPropertySource.java similarity index 92% rename from spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/core/SinglePairPropertySource.java rename to spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/util/SinglePairPropertySource.java index 22015a6a83..b550fbf20b 100644 --- a/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/core/SinglePairPropertySource.java +++ b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/util/SinglePairPropertySource.java @@ -14,13 +14,11 @@ * limitations under the License. */ -package com.alibaba.cloud.kubernetes.config.core; +package com.alibaba.cloud.kubernetes.config.util; import java.util.Collections; import java.util.Map; -import com.alibaba.cloud.kubernetes.config.util.Pair; - import org.springframework.core.env.MapPropertySource; import org.springframework.core.env.PropertySource; diff --git a/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/test/java/com/alibaba/cloud/kubernetes/config/processor/JsonFileProcessorTest.java b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/test/java/com/alibaba/cloud/kubernetes/config/processor/JsonFileProcessorTest.java index a2f903f7b1..7f6288ef17 100644 --- a/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/test/java/com/alibaba/cloud/kubernetes/config/processor/JsonFileProcessorTest.java +++ b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/test/java/com/alibaba/cloud/kubernetes/config/processor/JsonFileProcessorTest.java @@ -32,9 +32,17 @@ class JsonFileProcessorTest { */ @Test void generate_whenJsonObject() { - String json = "{\n" + " \"username\": \"admin\",\n" - + " \"password\": \"666\",\n" + " \"hobbies\": [\n" - + " \"reading\",\n" + " \"writing\"\n" + " ]\n" + "}\n"; + // @checkstyle:off + String json = "{\n" + + " \"username\": \"admin\",\n" + + " \"password\": \"666\",\n" + + " \"hobbies\": [\n" + + " \"reading\",\n" + + " \"writing\"\n" + + " ]\n" + + "}"; + // @checkstyle:on + EnumerablePropertySource ps = new JsonFileProcessor().generate("test_generate", json); @@ -50,13 +58,28 @@ void generate_whenJsonObject() { */ @Test void generate_whenJsonArray() { - String jsonArray = "[\n" + " {\n" + " \"username\": \"admin\",\n" - + " \"password\": \"666\",\n" + " \"hobbies\": [\n" - + " \"reading\",\n" + " \"writing\"\n" + " ]\n" + " },\n" - + " {\n" + " \"username\": \"root\",\n" - + " \"password\": \"888\",\n" + " \"hobbies\": [\n" - + " \"reading\",\n" + " \"writing\",\n" + " \"coding\"\n" - + " ]\n" + " }\n" + "]\n"; + // @checkstyle:off + String jsonArray = "[\n" + + " {\n" + + " \"username\": \"admin\",\n" + + " \"password\": \"666\",\n" + + " \"hobbies\": [\n" + + " \"reading\",\n" + + " \"writing\"\n" + + " ]\n" + + " },\n" + + " {\n" + + " \"username\": \"root\",\n" + + " \"password\": \"888\",\n" + + " \"hobbies\": [\n" + + " \"reading\",\n" + + " \"writing\",\n" + + " \"coding\"\n" + + " ]\n" + + " }\n" + + "]"; + // @checkstyle:on + EnumerablePropertySource ps = new JsonFileProcessor().generate("test_generate", jsonArray); diff --git a/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/test/java/com/alibaba/cloud/kubernetes/config/processor/PropertiesFileProcessorTest.java b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/test/java/com/alibaba/cloud/kubernetes/config/processor/PropertiesFileProcessorTest.java index f3217a271f..08b42ac66d 100644 --- a/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/test/java/com/alibaba/cloud/kubernetes/config/processor/PropertiesFileProcessorTest.java +++ b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/test/java/com/alibaba/cloud/kubernetes/config/processor/PropertiesFileProcessorTest.java @@ -32,8 +32,13 @@ class PropertiesFileProcessorTest { */ @Test void generate() { - String properties = "username=admin\n" + "password=666\n" + "hobbies[0]=reading\n" - + "hobbies[1]=writing\n"; + // @checkstyle:off + String properties = "username=admin\n" + + "password=666\n" + + "hobbies[0]=reading\n" + + "hobbies[1]=writing\n"; + // @checkstyle:on + EnumerablePropertySource ps = new PropertiesFileProcessor() .generate("test_generate", properties); diff --git a/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/test/java/com/alibaba/cloud/kubernetes/config/processor/YamlFileProcessorTest.java b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/test/java/com/alibaba/cloud/kubernetes/config/processor/YamlFileProcessorTest.java index 5e8596494f..4bc03b09bf 100644 --- a/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/test/java/com/alibaba/cloud/kubernetes/config/processor/YamlFileProcessorTest.java +++ b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/test/java/com/alibaba/cloud/kubernetes/config/processor/YamlFileProcessorTest.java @@ -32,8 +32,14 @@ class YamlFileProcessorTest { */ @Test void generate_whenSingleDocument() { - String yaml = "username: admin\n" + "password: \"666\"\n" + "hobbies:\n" - + " - reading\n" + " - writing\n"; + // @checkstyle:off + String yaml = "username: admin\n" + + "password: \"666\"\n" + + "hobbies:\n" + + " - reading\n" + + " - writing\n"; + // @checkstyle:on + EnumerablePropertySource ps = new YamlFileProcessor().generate("test_generate", yaml); assertThat(ps.getPropertyNames()).hasSize(4); @@ -48,10 +54,20 @@ void generate_whenSingleDocument() { */ @Test void generate_whenMultipleDocuments() { - String yaml = "username: admin\n" + "password: \"666\"\n" + "hobbies:\n" - + " - reading\n" + " - writing\n" + "---\n" + "username: adminn\n" - + "password: \"6666\"\n" + "hobbies:\n" + " - readingg\n" - + " - writingg\n"; + // @checkstyle:off + String yaml = "username: admin\n" + + "password: \"666\"\n" + + "hobbies:\n" + + " - reading\n" + + " - writing\n" + + "---\n" + + "username: adminn\n" + + "password: \"6666\"\n" + + "hobbies:\n" + + " - readingg\n" + + " - writingg\n"; + // @checkstyle:on + EnumerablePropertySource ps = new YamlFileProcessor().generate("test_generate", yaml); assertThat(ps.getPropertyNames()).hasSize(4); From fb27ffd965fae75aa18b6b050f75a358e5e43159 Mon Sep 17 00:00:00 2001 From: Freeman Lau Date: Tue, 31 Jan 2023 21:18:23 +0800 Subject: [PATCH 09/37] fix checkstyle --- .../config/core/ConfigEnvironmentPostProcessor.java | 2 +- .../cloud/kubernetes/config/util/ResourceKeyUtils.java | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/core/ConfigEnvironmentPostProcessor.java b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/core/ConfigEnvironmentPostProcessor.java index 3120e76b5c..f11ffbf02d 100644 --- a/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/core/ConfigEnvironmentPostProcessor.java +++ b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/core/ConfigEnvironmentPostProcessor.java @@ -180,7 +180,7 @@ private EnumerablePropertySource propertySourceForConfigMap( /** * Fail the application start up if necessary when the resource is missing. - * + * *

* NOTE: do nothing if the application is refreshing * diff --git a/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/util/ResourceKeyUtils.java b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/util/ResourceKeyUtils.java index 93a799a09f..1c7699582f 100644 --- a/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/util/ResourceKeyUtils.java +++ b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/util/ResourceKeyUtils.java @@ -31,7 +31,7 @@ private ResourceKeyUtils() { /** * Generate a {@link ResourceKey} from {@link KubernetesConfigProperties.ConfigMap}. - * + * * @param configMap {@link KubernetesConfigProperties.ConfigMap} * @return {@link ResourceKey} */ @@ -43,7 +43,7 @@ public static ResourceKey resourceKey( /** * Generate a {@link ResourceKey} from {@link KubernetesConfigProperties.Secret}. - * + * * @param secret {@link KubernetesConfigProperties.Secret} * @return {@link ResourceKey} */ From 1cd02275c628fbcc6d341455ef00890ed5568228 Mon Sep 17 00:00:00 2001 From: Freeman Lau Date: Tue, 31 Jan 2023 23:06:19 +0800 Subject: [PATCH 10/37] close context --- .../kubernetes/config/MissingConfigIntegrationTests.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/test/java/com/alibaba/cloud/kubernetes/config/MissingConfigIntegrationTests.java b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/test/java/com/alibaba/cloud/kubernetes/config/MissingConfigIntegrationTests.java index 0d68f18120..21f962838f 100644 --- a/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/test/java/com/alibaba/cloud/kubernetes/config/MissingConfigIntegrationTests.java +++ b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/test/java/com/alibaba/cloud/kubernetes/config/MissingConfigIntegrationTests.java @@ -34,7 +34,7 @@ public class MissingConfigIntegrationTests { @Test void testEnabledFailOnMissingConfig() { assertThatCode(() -> new SpringApplicationBuilder(Empty.class) - .web(WebApplicationType.NONE).profiles("missing-config").run()) + .web(WebApplicationType.NONE).profiles("missing-config").run().close()) .isInstanceOf(ConfigMissingException.class); } @@ -44,6 +44,6 @@ void testDisabledFailOnMissingConfig() { .web(WebApplicationType.NONE) .properties(KubernetesConfigProperties.PREFIX + ".fail-on-missing-config=false") - .profiles("missing-config").run()).doesNotThrowAnyException(); + .profiles("missing-config").run().close()).doesNotThrowAnyException(); } } From 4335a724660c259df07c5064be35e0e010497275 Mon Sep 17 00:00:00 2001 From: Freeman Lau Date: Tue, 31 Jan 2023 23:27:48 +0800 Subject: [PATCH 11/37] optimize config properties --- .../config/KubernetesConfigProperties.java | 51 +++++++++---------- 1 file changed, 25 insertions(+), 26 deletions(-) diff --git a/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/KubernetesConfigProperties.java b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/KubernetesConfigProperties.java index 582aef257d..db26db2c6f 100644 --- a/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/KubernetesConfigProperties.java +++ b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/KubernetesConfigProperties.java @@ -75,15 +75,15 @@ public class KubernetesConfigProperties implements InitializingBean { */ private boolean failOnMissingConfig = true; - private List configMaps = new ArrayList<>(); - - private List secrets = new ArrayList<>(); - /** * Whether to enable the auto refresh feature, default value is {@code false}. */ private boolean refreshable = false; + private List configMaps = new ArrayList<>(); + + private List secrets = new ArrayList<>(); + public boolean isEnabled() { return enabled; } @@ -187,29 +187,12 @@ public void afterPropertiesSet() { mergeSecrets(); } - private void mergeSecrets() { - for (Secret secret : secrets) { - if (!StringUtils.hasText(secret.getName())) { - throw new IllegalArgumentException("Secret name must not be empty."); - } - if (!StringUtils.hasText(secret.getNamespace())) { - secret.setNamespace(namespace); - } - if (secret.getRefreshable() == null) { - secret.setRefreshable(refreshable); - } - if (secret.getPreference() == null) { - secret.setPreference(preference); - } - } - } - private void mergeConfigmaps() { for (ConfigMap configMap : configMaps) { if (!StringUtils.hasText(configMap.getName())) { throw new IllegalArgumentException("ConfigMap name must not be empty."); } - if (!StringUtils.hasText(configMap.getNamespace())) { + if (configMap.getNamespace() == null) { configMap.setNamespace(namespace); } if (configMap.getRefreshable() == null) { @@ -221,6 +204,23 @@ private void mergeConfigmaps() { } } + private void mergeSecrets() { + for (Secret secret : secrets) { + if (!StringUtils.hasText(secret.getName())) { + throw new IllegalArgumentException("Secret name must not be empty."); + } + if (secret.getNamespace() == null) { + secret.setNamespace(namespace); + } + if (secret.getRefreshable() == null) { + secret.setRefreshable(refreshable); + } + if (secret.getPreference() == null) { + secret.setPreference(preference); + } + } + } + private static String determineNamespace() { String ns = KubernetesUtils.currentNamespace(); return StringUtils.hasText(ns) ? ns : "default"; @@ -285,8 +285,8 @@ public void setPreference(Preference preference) { @Override public String toString() { return "ConfigMap{" + "name='" + name + '\'' + ", namespace='" + namespace - + '\'' + ", refreshEnabled=" + refreshable + ", preference=" - + preference + '}'; + + '\'' + ", refreshable=" + refreshable + ", preference=" + preference + + '}'; } @Override @@ -372,8 +372,7 @@ public void setPreference(Preference preference) { @Override public String toString() { return "Secret{" + "name='" + name + '\'' + ", namespace='" + namespace + '\'' - + ", refreshEnabled=" + refreshable + ", preference=" + preference - + '}'; + + ", refreshable=" + refreshable + ", preference=" + preference + '}'; } @Override From 632eb66c5333dcf0c0e31b0ffd6666b273c132da Mon Sep 17 00:00:00 2001 From: Freeman Lau Date: Wed, 1 Feb 2023 22:26:58 +0800 Subject: [PATCH 12/37] avoid property sources have same name --- .../cloud/kubernetes/config/processor/JsonFileProcessor.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/processor/JsonFileProcessor.java b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/processor/JsonFileProcessor.java index 16b9310609..4be7de082d 100644 --- a/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/processor/JsonFileProcessor.java +++ b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/processor/JsonFileProcessor.java @@ -96,7 +96,7 @@ private static CompositePropertySource convertJsonObjectStringToPropertySource( CompositePropertySource propertySource = new CompositePropertySource(name); try { String yamlString = yaml.dump(map); - List> pss = loader.load(name, + List> pss = loader.load(name + "[part]", new ByteArrayResource(yamlString.getBytes(StandardCharsets.UTF_8))); propertySource.getPropertySources().addAll(pss); } From 0542d0974bb843f525a05e1207b02c28e2f89e44 Mon Sep 17 00:00:00 2001 From: Freeman Lau Date: Wed, 1 Feb 2023 22:52:12 +0800 Subject: [PATCH 13/37] add kubernetes config README --- .../README.md | 149 ++++++++++++++++++ 1 file changed, 149 insertions(+) create mode 100644 spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/README.md diff --git a/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/README.md b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/README.md new file mode 100644 index 0000000000..8696ecb7ed --- /dev/null +++ b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/README.md @@ -0,0 +1,149 @@ +# spring-cloud-starter-alibaba-kubernetes-config + +The main purpose of this module is to use Kubernetes ConfigMap/Secret as a distributed configuration center to achieve +dynamic configuration updates without restarting the application. + +## Quick Start + +Maven: + +```xml + + + com.alibaba.cloud + spring-cloud-starter-alibaba-kubernetes-config + +``` + +Gradle: + +```groovy +implementation 'com.alibaba.cloud:spring-cloud-starter-alibaba-kubernetes-config' +``` + +First you need a Kubernetes cluster, you can use [docker-desktop](https://www.docker.com/products/docker-desktop/) +or [minikube](https://minikube.sigs.k8s.io/docs/) to create a cluster. + +1. Clone the project + + ```bash + git clone --depth=1 https://github.com/alibaba/spring-cloud-alibaba.git + ``` + +2. Create Role and RoleBinding for ServiceAccount + + ```bash + # Just for the example, we created a ClusterRole, but in fact, you can control resources more finely, only need the get,list,watch permissions of ConfigMap/Secret + kubectl create clusterrole config-cluster-reader --verb=get,list,watch --resource=configmaps,secrets + # Bind ClusterRole to ServiceAccount (namespace: default, name: default) + kubectl create clusterrolebinding config-cluster-reader-default-default --clusterrole config-cluster-reader --serviceaccount default:default + ``` + +3. Build and Start + ```shell + ./mvnw clean package -pl com.alibaba.cloud:kubernetes-config-example -am -DskipTests + + docker build -f spring-cloud-alibaba-examples/kubernetes-config-example/Dockerfile -t kubernetes-config-example:latest . + + kubectl apply -f spring-cloud-alibaba-examples/kubernetes-config-example/deployment.yaml + ``` + ```shell + # Execute the following command after the application startup, the startup process should be very fast (less than 3s) + curl http://localhost:`kubectl get service kubernetes-config-example -o jsonpath='{..nodePort}'`/price + + # You should see a response of `100` + ``` + +4. Add a ConfigMap + ```shell + # This ConfigMap is being monitored by the current application, so when this ConfigMap is added, the application will automatically update the configuration + kubectl apply -f spring-cloud-alibaba-examples/kubernetes-config-example/configmap-example-01.yaml + + # Visit again + curl http://localhost:`kubectl get svc kubernetes-config -o jsonpath='{..nodePort}'`/price + + # You should see a response of `200` + ``` + You can modify the configuration in `configmap-example-01.yaml`, and then re-apply the file to observe the change of + the interface result. + + Through the above operations, you can see that the application can dynamically update the configuration without + restarting. + +5. Delete Resources + ```shell + # Delete all resources created by the above operations + kubectl delete -f spring-cloud-alibaba-examples/kubernetes-config-example/deployment.yaml + kubectl delete -f spring-cloud-alibaba-examples/kubernetes-config-example/configmap-example-01.yaml + kubectl delete clusterrole config-cluster-reader + kubectl delete clusterrolebinding config-cluster-reader-default-default + ``` + +#### Main Features + +- Dynamic update configuration(ConfigMap/Secret) + + You can manually configure whether to monitor configuration file changes. + +- Configuration priority + + Through configuration, choose to use local configuration or remote configuration first. + +- Supports multiple configuration file formats + + Supports configuration files in `yaml`, `properties`, `json` and key-value pair. + +## Core Configurations + +```yaml +spring: + cloud: + k8s: + config: + enabled: true + namespace: default # The namespace where the configuration is located (global configuration). If it is inside the Kubernetes cluster, it defaults to the namespace where the current pod is located; if it is outside the Kubernetes cluster, it defaults to the namespace of the current context + preference: remote # Configuration priority (global configuration), remote is preferred to use remote configuration, local is preferred to use local configuration, and the default is remote + refreshable: true # Whether to enable dynamic update configuration (global configuration), the default is true + refresh-on-delete: false # Whether to automatically refresh when deleting the configuration, enabling this configuration may bring certain risks, if your configuration items only exist on the remote side but not locally, if you delete the configmap by mistake, it may cause abnormalities in the program, so the default value is false + fail-on-missing-config: true + config-maps: + - name: my-configmap # configmap name + namespace: default # The namespace where configmap is located will override the global configuration of the namespace + preference: remote # Configuration priority, which will override the global configuration of preference + refreshable: true # Whether to enable dynamic update configuration, it will override the refresh-enabled global configuration + secrets: + - name: my-secret # secret name + namespace: default # The namespace where the secret is located will override the global configuration of the namespace + preference: remote # Configuration priority, which will override the global configuration of preference + refreshable: false # Whether to enable dynamic update configuration will override the global configuration of refresh-enabled, because secrets generally do not require dynamic refresh, so the default value is false +``` + +## Best Practices + +Spring Cloud provides the capability of dynamically refreshing the Environment at runtime, which mainly dynamically +updates the properties of two types of beans: + +- Beans annotated with `@ConfigurationProperties` +- Beans annotated with `@RefreshScope` + +A good practice is to use `@ConfigurationProperties` to organize your configurations. + +In general, the configuration of an application falls into two categories: + +- Basic configuration + + - public + + It can be managed through the configuration center or the jar package. Generally, there is no need for + dynamic updates, such as Tomcat connection pool parameters, database connection pool parameters, etc. + + - private + + It can be placed in a local configuration file, such as database connection information. This type of + configuration is generally sensitive and can be managed through Kubernetes Secret. + +- Business configuration + + This type of configuration should be strongly related to the business logic, but users need to judge whether they need + to be + placed in the configuration center and whether there is a need for dynamic updates. From f00557c479792a8a7ccc77d49f38ae9798c4be77 Mon Sep 17 00:00:00 2001 From: Freeman Lau Date: Thu, 2 Feb 2023 19:36:31 +0800 Subject: [PATCH 14/37] make log method static --- .../com/alibaba/cloud/kubernetes/config/core/ConfigWatcher.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/core/ConfigWatcher.java b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/core/ConfigWatcher.java index dc6c994eed..37f24617c4 100644 --- a/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/core/ConfigWatcher.java +++ b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/core/ConfigWatcher.java @@ -103,7 +103,7 @@ private void watchRefreshableResources(KubernetesConfigProperties properties, log(secretInformers); } - private void log( + private static void log( Map> informers) { List names = informers.keySet().stream().map(resourceKey -> String .join(".", resourceKey.name(), resourceKey.namespace())) From 2d2cb16baa67252ec85abfb57c9ac0e8fa7cee5b Mon Sep 17 00:00:00 2001 From: Freeman Lau Date: Fri, 3 Feb 2023 00:31:00 +0800 Subject: [PATCH 15/37] add example --- .../src/main/asciidoc/kubernetes-config.adoc | 138 ++++++++++++++++ .../kubernetes-config-example/pom.xml | 56 +++++++ .../config/KubernetesConfigApplication.java | 36 +++++ .../config/controller/EchoController.java | 36 +++++ .../config/filter/BlacklistFilter.java | 62 ++++++++ .../properties/BlacklistProperties.java | 32 ++++ .../src/main/resources/application.yml | 23 +++ spring-cloud-alibaba-examples/pom.xml | 2 + .../config}/RocketMQBusApplication.java | 2 +- .../{rocketmq => kubernetes/config}/User.java | 2 +- .../config}/UserRemoteApplicationEvent.java | 2 +- .../kubernetes/commons/KubernetesUtils.java | 4 + .../README.md | 149 ------------------ .../config/KubernetesConfigProperties.java | 2 +- .../core/ConfigEnvironmentPostProcessor.java | 47 +++++- ...=> AbstractKubernetesConfigException.java} | 7 +- .../KubernetesConfigMissingException.java | 28 ++++ ...bernetesConfigMissingFailureAnalyzer.java} | 6 +- .../KubernetesForbiddenException.java | 28 ++++ .../KubernetesForbiddenFailureAnalyzer.java | 40 +++++ .../KubernetesUnavailableException.java | 28 ++++ .../KubernetesUnavailableFailureAnalyzer.java | 44 ++++++ .../main/resources/META-INF/spring.factories | 4 +- .../config/MissingConfigIntegrationTests.java | 4 +- 24 files changed, 614 insertions(+), 168 deletions(-) create mode 100644 spring-cloud-alibaba-docs/src/main/asciidoc/kubernetes-config.adoc create mode 100644 spring-cloud-alibaba-examples/kubernetes-config-example/pom.xml create mode 100644 spring-cloud-alibaba-examples/kubernetes-config-example/src/main/java/com/alibaba/cloud/examples/kubernetes/config/KubernetesConfigApplication.java create mode 100644 spring-cloud-alibaba-examples/kubernetes-config-example/src/main/java/com/alibaba/cloud/examples/kubernetes/config/controller/EchoController.java create mode 100644 spring-cloud-alibaba-examples/kubernetes-config-example/src/main/java/com/alibaba/cloud/examples/kubernetes/config/filter/BlacklistFilter.java create mode 100644 spring-cloud-alibaba-examples/kubernetes-config-example/src/main/java/com/alibaba/cloud/examples/kubernetes/config/properties/BlacklistProperties.java create mode 100644 spring-cloud-alibaba-examples/kubernetes-config-example/src/main/resources/application.yml rename spring-cloud-alibaba-examples/spring-cloud-bus-rocketmq-example/src/main/java/com/alibaba/cloud/examples/{rocketmq => kubernetes/config}/RocketMQBusApplication.java (98%) rename spring-cloud-alibaba-examples/spring-cloud-bus-rocketmq-example/src/main/java/com/alibaba/cloud/examples/{rocketmq => kubernetes/config}/User.java (95%) rename spring-cloud-alibaba-examples/spring-cloud-bus-rocketmq-example/src/main/java/com/alibaba/cloud/examples/{rocketmq => kubernetes/config}/UserRemoteApplicationEvent.java (96%) delete mode 100644 spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/README.md rename spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/exception/{ConfigMissingException.java => AbstractKubernetesConfigException.java} (83%) create mode 100644 spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/exception/KubernetesConfigMissingException.java rename spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/exception/{ConfigMissingFailureAnalyzer.java => KubernetesConfigMissingFailureAnalyzer.java} (89%) create mode 100644 spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/exception/KubernetesForbiddenException.java create mode 100644 spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/exception/KubernetesForbiddenFailureAnalyzer.java create mode 100644 spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/exception/KubernetesUnavailableException.java create mode 100644 spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/exception/KubernetesUnavailableFailureAnalyzer.java diff --git a/spring-cloud-alibaba-docs/src/main/asciidoc/kubernetes-config.adoc b/spring-cloud-alibaba-docs/src/main/asciidoc/kubernetes-config.adoc new file mode 100644 index 0000000000..149ac39a39 --- /dev/null +++ b/spring-cloud-alibaba-docs/src/main/asciidoc/kubernetes-config.adoc @@ -0,0 +1,138 @@ +== Spring Cloud Alibaba Kubernetes Config + +The purpose of this module is to use Kubernetes ConfigMap/Secret as a distributed configuration center to achieve dynamic configuration updates without restarting the application. + +=== Quick Start + +Maven: + +[source,xml] +---- + + com.alibaba.cloud + spring-cloud-starter-alibaba-kubernetes-config + +---- + +Gradle: + +[source,groovy] +---- +implementation 'com.alibaba.cloud:spring-cloud-starter-alibaba-kubernetes-config' +---- + +First you need a Kubernetes cluster, you can use https://www.docker.com/products/docker-desktop/[docker-desktop] +or https://minikube.sigs.k8s.io/docs/[minikube] to create a cluster. + +- Clone the project + +[source,shell] +---- +git clone --depth=1 https://github.com/alibaba/spring-cloud-alibaba.git +---- + +- Create Role and RoleBinding for ServiceAccount + +[source,shell] +---- +# Created a ClusterRole just for the example, but in fact, you can control resources more finely, only need the get,list,watch permissions of ConfigMap/Secret +kubectl create clusterrole config-cluster-reader --verb=get,list,watch --resource=configmaps,secrets +# Bind ClusterRole to ServiceAccount (namespace: default, name: default) +kubectl create clusterrolebinding config-cluster-reader-default-default --clusterrole config-cluster-reader --serviceaccount default:default +---- + +- Build and Start + +[source,shell] +---- +./mvnw clean package -pl com.alibaba.cloud:kubernetes-config-example -am -DskipTests + +docker build -f spring-cloud-alibaba-examples/kubernetes-config-example/Dockerfile -t kubernetes-config-example:latest . + +kubectl apply -f spring-cloud-alibaba-examples/kubernetes-config-example/deployment.yaml +---- + +[source,shell] +---- +# Execute the following command after the application startup, the startup process should be very fast (less than 3s) +curl http://localhost:`kubectl get service kubernetes-config-example -o jsonpath='{..nodePort}'`/price + +# You should see a response of `100` +---- + +- Add ConfigMap + +[source,shell] +---- +# This ConfigMap is being monitored by the current application, so when this ConfigMap is added, the application will automatically update the configuration +kubectl apply -f spring-cloud-alibaba-examples/kubernetes-config-example/configmap-example-01.yaml + +# Visit again +curl http://localhost:`kubectl get svc kubernetes-config -o jsonpath='{..nodePort}'`/price + +# You should see a response of `200` +---- + +You can modify the configuration in `configmap-example-01.yaml`, and then re-apply the file to observe the change of the interface result. + +Through the above operations, you can see that the application can dynamically update the configuration without restarting. + +- Delete Resources + +[source,shell] +---- +# Delete all resources created by the above operations +kubectl delete -f spring-cloud-alibaba-examples/kubernetes-config-example/deployment.yaml +kubectl delete -f spring-cloud-alibaba-examples/kubernetes-config-example/configmap-example-01.yaml +kubectl delete clusterrole config-cluster-reader +kubectl delete clusterrolebinding config-cluster-reader-default-default +---- + +=== Main Features + +- Dynamic update configuration(ConfigMap/Secret) + + You can manually configure whether to monitor configuration file changes. + +- Configuration priority + + Through configuration, choose to use local configuration or remote configuration first. + +- Supports multiple configuration file formats + + Supports configuration files in `yaml`, `properties`, `json` and key-value pair. + +=== Core Configurations + +[source,yaml] +---- +spring: + cloud: + k8s: + config: + enabled: true + namespace: default # The namespace where the configuration is located (global configuration). If it is inside the Kubernetes cluster, it defaults to the namespace where the current pod is located; if it is outside the Kubernetes cluster, it defaults to the namespace of the current context + preference: remote # Configuration priority (global configuration), remote is preferred to use remote configuration, local is preferred to use local configuration, and the default is remote + refreshable: true # Whether to enable dynamic update configuration (global configuration), the default is true + refresh-on-delete: false # Whether to automatically refresh when deleting the configuration, enabling this configuration may bring certain risks, if your configuration items only exist on the remote side but not locally, if you delete the configmap by mistake, it may cause abnormalities in the program, so the default value is false + fail-on-missing-config: true + config-maps: + - name: my-configmap # configmap name + namespace: default # The namespace where configmap is located will override the global configuration of the namespace + preference: remote # Configuration priority, which will override the global configuration of preference + refreshable: true # Whether to enable dynamic update configuration, it will override the refresh-enabled global configuration + secrets: + - name: my-secret # secret name + namespace: default # The namespace where the secret is located will override the global configuration of the namespace + preference: remote # Configuration priority, which will override the global configuration of preference + refreshable: false # Whether to enable dynamic update configuration will override the global configuration of refresh-enabled, because secrets generally do not require dynamic refresh, so the default value is false +---- + +=== Best Practices + +Spring Cloud provides the capability of dynamically refreshing the Environment at runtime, which mainly dynamically updates the properties of two types of beans: + +- Beans annotated with `@ConfigurationProperties` +- Beans annotated with `@RefreshScope` + +A good practice is to use `@ConfigurationProperties` to organize your configurations. diff --git a/spring-cloud-alibaba-examples/kubernetes-config-example/pom.xml b/spring-cloud-alibaba-examples/kubernetes-config-example/pom.xml new file mode 100644 index 0000000000..a47b439e09 --- /dev/null +++ b/spring-cloud-alibaba-examples/kubernetes-config-example/pom.xml @@ -0,0 +1,56 @@ + + + + spring-cloud-alibaba-examples + com.alibaba.cloud + ${revision} + ../pom.xml + + 4.0.0 + + kubernetes-config-example + Kubernetes Config Example + Example demonstrating how to use Spring Cloud Alibaba Kubernetes Config + jar + + + + com.alibaba.cloud + spring-cloud-starter-alibaba-kubernetes-config + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-configuration-processor + true + + + org.projectlombok + lombok + provided + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + org.apache.maven.plugins + maven-deploy-plugin + ${maven-deploy-plugin.version} + + true + + + + + + diff --git a/spring-cloud-alibaba-examples/kubernetes-config-example/src/main/java/com/alibaba/cloud/examples/kubernetes/config/KubernetesConfigApplication.java b/spring-cloud-alibaba-examples/kubernetes-config-example/src/main/java/com/alibaba/cloud/examples/kubernetes/config/KubernetesConfigApplication.java new file mode 100644 index 0000000000..cda461046e --- /dev/null +++ b/spring-cloud-alibaba-examples/kubernetes-config-example/src/main/java/com/alibaba/cloud/examples/kubernetes/config/KubernetesConfigApplication.java @@ -0,0 +1,36 @@ +/* + * Copyright 2013-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.alibaba.cloud.examples.kubernetes.config; + +import com.alibaba.cloud.examples.kubernetes.config.properties.BlacklistProperties; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.context.properties.EnableConfigurationProperties; + +/** + * @author Freeman + */ +@SpringBootApplication +@EnableConfigurationProperties(BlacklistProperties.class) +public class KubernetesConfigApplication { + + public static void main(String[] args) { + SpringApplication.run(KubernetesConfigApplication.class, args); + } + +} diff --git a/spring-cloud-alibaba-examples/kubernetes-config-example/src/main/java/com/alibaba/cloud/examples/kubernetes/config/controller/EchoController.java b/spring-cloud-alibaba-examples/kubernetes-config-example/src/main/java/com/alibaba/cloud/examples/kubernetes/config/controller/EchoController.java new file mode 100644 index 0000000000..c268bc0966 --- /dev/null +++ b/spring-cloud-alibaba-examples/kubernetes-config-example/src/main/java/com/alibaba/cloud/examples/kubernetes/config/controller/EchoController.java @@ -0,0 +1,36 @@ +/* + * Copyright 2013-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.alibaba.cloud.examples.kubernetes.config.controller; + +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.web.bind.annotation.RestController; + +import static com.alibaba.cloud.examples.kubernetes.config.filter.BlacklistFilter.HEADER_USER_ID; + +/** + * @author Freeman + */ +@RestController +public class EchoController { + + @GetMapping("/echo") + public String echo(@RequestHeader(HEADER_USER_ID) String userId) { + return String.format("Hello, %s", userId); + } + +} diff --git a/spring-cloud-alibaba-examples/kubernetes-config-example/src/main/java/com/alibaba/cloud/examples/kubernetes/config/filter/BlacklistFilter.java b/spring-cloud-alibaba-examples/kubernetes-config-example/src/main/java/com/alibaba/cloud/examples/kubernetes/config/filter/BlacklistFilter.java new file mode 100644 index 0000000000..57e5cbc577 --- /dev/null +++ b/spring-cloud-alibaba-examples/kubernetes-config-example/src/main/java/com/alibaba/cloud/examples/kubernetes/config/filter/BlacklistFilter.java @@ -0,0 +1,62 @@ +/* + * Copyright 2013-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.alibaba.cloud.examples.kubernetes.config.filter; + +import java.io.IOException; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import com.alibaba.cloud.examples.kubernetes.config.properties.BlacklistProperties; + +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; + +/** + * @author Freeman + */ +@Component +public class BlacklistFilter extends OncePerRequestFilter { + /** + * User id header. + */ + public static final String HEADER_USER_ID = "X-User-Id"; + + private final BlacklistProperties blacklistProperties; + + public BlacklistFilter(BlacklistProperties blacklistProperties) { + this.blacklistProperties = blacklistProperties; + } + + @Override + protected void doFilterInternal(HttpServletRequest req, HttpServletResponse resp, + FilterChain chain) throws ServletException, IOException { + String userId = req.getHeader(HEADER_USER_ID); + if (userId == null) { + resp.sendError(HttpServletResponse.SC_BAD_REQUEST, + HEADER_USER_ID + " header is required!"); + return; + } + if (blacklistProperties.getUserIds().contains(userId)) { + resp.sendError(HttpServletResponse.SC_FORBIDDEN, "User is blacklisted!"); + return; + } + chain.doFilter(req, resp); + } +} diff --git a/spring-cloud-alibaba-examples/kubernetes-config-example/src/main/java/com/alibaba/cloud/examples/kubernetes/config/properties/BlacklistProperties.java b/spring-cloud-alibaba-examples/kubernetes-config-example/src/main/java/com/alibaba/cloud/examples/kubernetes/config/properties/BlacklistProperties.java new file mode 100644 index 0000000000..393f59e95c --- /dev/null +++ b/spring-cloud-alibaba-examples/kubernetes-config-example/src/main/java/com/alibaba/cloud/examples/kubernetes/config/properties/BlacklistProperties.java @@ -0,0 +1,32 @@ +/* + * Copyright 2013-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.alibaba.cloud.examples.kubernetes.config.properties; + +import java.util.Set; + +import lombok.Data; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * @author Freeman + */ +@Data +@ConfigurationProperties(prefix = "blacklist") +public class BlacklistProperties { + private Set userIds; +} diff --git a/spring-cloud-alibaba-examples/kubernetes-config-example/src/main/resources/application.yml b/spring-cloud-alibaba-examples/kubernetes-config-example/src/main/resources/application.yml new file mode 100644 index 0000000000..d54782ecae --- /dev/null +++ b/spring-cloud-alibaba-examples/kubernetes-config-example/src/main/resources/application.yml @@ -0,0 +1,23 @@ +server: + port: 8080 +spring: + application: + name: kubernetes-config-example + cloud: + k8s: + config: + config-maps: + - name: configmap-01 + namespace: default + refreshable: true + preference: remote + secrets: + - name: secret-01 + namespace: default + refreshable: false + refresh-on-delete: false + fail-on-missing-config: false +blacklist: + user-ids: + - 1 + - 2 diff --git a/spring-cloud-alibaba-examples/pom.xml b/spring-cloud-alibaba-examples/pom.xml index ec38eba797..2ef228f810 100644 --- a/spring-cloud-alibaba-examples/pom.xml +++ b/spring-cloud-alibaba-examples/pom.xml @@ -53,6 +53,8 @@ integrated-example/integrated-praise-consumer integrated-example/integrated-common integrated-example/integrated-frontend + + kubernetes-config-example diff --git a/spring-cloud-alibaba-examples/spring-cloud-bus-rocketmq-example/src/main/java/com/alibaba/cloud/examples/rocketmq/RocketMQBusApplication.java b/spring-cloud-alibaba-examples/spring-cloud-bus-rocketmq-example/src/main/java/com/alibaba/cloud/examples/kubernetes/config/RocketMQBusApplication.java similarity index 98% rename from spring-cloud-alibaba-examples/spring-cloud-bus-rocketmq-example/src/main/java/com/alibaba/cloud/examples/rocketmq/RocketMQBusApplication.java rename to spring-cloud-alibaba-examples/spring-cloud-bus-rocketmq-example/src/main/java/com/alibaba/cloud/examples/kubernetes/config/RocketMQBusApplication.java index 01f9918a5c..458390653e 100644 --- a/spring-cloud-alibaba-examples/spring-cloud-bus-rocketmq-example/src/main/java/com/alibaba/cloud/examples/rocketmq/RocketMQBusApplication.java +++ b/spring-cloud-alibaba-examples/spring-cloud-bus-rocketmq-example/src/main/java/com/alibaba/cloud/examples/kubernetes/config/RocketMQBusApplication.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.alibaba.cloud.examples.rocketmq; +package com.alibaba.cloud.examples.kubernetes.config; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; diff --git a/spring-cloud-alibaba-examples/spring-cloud-bus-rocketmq-example/src/main/java/com/alibaba/cloud/examples/rocketmq/User.java b/spring-cloud-alibaba-examples/spring-cloud-bus-rocketmq-example/src/main/java/com/alibaba/cloud/examples/kubernetes/config/User.java similarity index 95% rename from spring-cloud-alibaba-examples/spring-cloud-bus-rocketmq-example/src/main/java/com/alibaba/cloud/examples/rocketmq/User.java rename to spring-cloud-alibaba-examples/spring-cloud-bus-rocketmq-example/src/main/java/com/alibaba/cloud/examples/kubernetes/config/User.java index df81d87a68..39d197d94e 100644 --- a/spring-cloud-alibaba-examples/spring-cloud-bus-rocketmq-example/src/main/java/com/alibaba/cloud/examples/rocketmq/User.java +++ b/spring-cloud-alibaba-examples/spring-cloud-bus-rocketmq-example/src/main/java/com/alibaba/cloud/examples/kubernetes/config/User.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.alibaba.cloud.examples.rocketmq; +package com.alibaba.cloud.examples.kubernetes.config; /** * User Domain. diff --git a/spring-cloud-alibaba-examples/spring-cloud-bus-rocketmq-example/src/main/java/com/alibaba/cloud/examples/rocketmq/UserRemoteApplicationEvent.java b/spring-cloud-alibaba-examples/spring-cloud-bus-rocketmq-example/src/main/java/com/alibaba/cloud/examples/kubernetes/config/UserRemoteApplicationEvent.java similarity index 96% rename from spring-cloud-alibaba-examples/spring-cloud-bus-rocketmq-example/src/main/java/com/alibaba/cloud/examples/rocketmq/UserRemoteApplicationEvent.java rename to spring-cloud-alibaba-examples/spring-cloud-bus-rocketmq-example/src/main/java/com/alibaba/cloud/examples/kubernetes/config/UserRemoteApplicationEvent.java index dc4b3c5686..1e087e145e 100644 --- a/spring-cloud-alibaba-examples/spring-cloud-bus-rocketmq-example/src/main/java/com/alibaba/cloud/examples/rocketmq/UserRemoteApplicationEvent.java +++ b/spring-cloud-alibaba-examples/spring-cloud-bus-rocketmq-example/src/main/java/com/alibaba/cloud/examples/kubernetes/config/UserRemoteApplicationEvent.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.alibaba.cloud.examples.rocketmq; +package com.alibaba.cloud.examples.kubernetes.config; import org.springframework.cloud.bus.event.RemoteApplicationEvent; diff --git a/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-alibaba-kubernetes-commons/src/main/java/com/alibaba/cloud/kubernetes/commons/KubernetesUtils.java b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-alibaba-kubernetes-commons/src/main/java/com/alibaba/cloud/kubernetes/commons/KubernetesUtils.java index 71610b8d04..a14a9265d3 100644 --- a/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-alibaba-kubernetes-commons/src/main/java/com/alibaba/cloud/kubernetes/commons/KubernetesUtils.java +++ b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-alibaba-kubernetes-commons/src/main/java/com/alibaba/cloud/kubernetes/commons/KubernetesUtils.java @@ -35,6 +35,10 @@ private KubernetesUtils() { /** * Get the kube config. * + *

+ * NOTE: {@link Config} needs to be a singleton, do NOT modify + * it. + * * @return Config */ public static Config config() { diff --git a/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/README.md b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/README.md deleted file mode 100644 index 8696ecb7ed..0000000000 --- a/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/README.md +++ /dev/null @@ -1,149 +0,0 @@ -# spring-cloud-starter-alibaba-kubernetes-config - -The main purpose of this module is to use Kubernetes ConfigMap/Secret as a distributed configuration center to achieve -dynamic configuration updates without restarting the application. - -## Quick Start - -Maven: - -```xml - - - com.alibaba.cloud - spring-cloud-starter-alibaba-kubernetes-config - -``` - -Gradle: - -```groovy -implementation 'com.alibaba.cloud:spring-cloud-starter-alibaba-kubernetes-config' -``` - -First you need a Kubernetes cluster, you can use [docker-desktop](https://www.docker.com/products/docker-desktop/) -or [minikube](https://minikube.sigs.k8s.io/docs/) to create a cluster. - -1. Clone the project - - ```bash - git clone --depth=1 https://github.com/alibaba/spring-cloud-alibaba.git - ``` - -2. Create Role and RoleBinding for ServiceAccount - - ```bash - # Just for the example, we created a ClusterRole, but in fact, you can control resources more finely, only need the get,list,watch permissions of ConfigMap/Secret - kubectl create clusterrole config-cluster-reader --verb=get,list,watch --resource=configmaps,secrets - # Bind ClusterRole to ServiceAccount (namespace: default, name: default) - kubectl create clusterrolebinding config-cluster-reader-default-default --clusterrole config-cluster-reader --serviceaccount default:default - ``` - -3. Build and Start - ```shell - ./mvnw clean package -pl com.alibaba.cloud:kubernetes-config-example -am -DskipTests - - docker build -f spring-cloud-alibaba-examples/kubernetes-config-example/Dockerfile -t kubernetes-config-example:latest . - - kubectl apply -f spring-cloud-alibaba-examples/kubernetes-config-example/deployment.yaml - ``` - ```shell - # Execute the following command after the application startup, the startup process should be very fast (less than 3s) - curl http://localhost:`kubectl get service kubernetes-config-example -o jsonpath='{..nodePort}'`/price - - # You should see a response of `100` - ``` - -4. Add a ConfigMap - ```shell - # This ConfigMap is being monitored by the current application, so when this ConfigMap is added, the application will automatically update the configuration - kubectl apply -f spring-cloud-alibaba-examples/kubernetes-config-example/configmap-example-01.yaml - - # Visit again - curl http://localhost:`kubectl get svc kubernetes-config -o jsonpath='{..nodePort}'`/price - - # You should see a response of `200` - ``` - You can modify the configuration in `configmap-example-01.yaml`, and then re-apply the file to observe the change of - the interface result. - - Through the above operations, you can see that the application can dynamically update the configuration without - restarting. - -5. Delete Resources - ```shell - # Delete all resources created by the above operations - kubectl delete -f spring-cloud-alibaba-examples/kubernetes-config-example/deployment.yaml - kubectl delete -f spring-cloud-alibaba-examples/kubernetes-config-example/configmap-example-01.yaml - kubectl delete clusterrole config-cluster-reader - kubectl delete clusterrolebinding config-cluster-reader-default-default - ``` - -#### Main Features - -- Dynamic update configuration(ConfigMap/Secret) - - You can manually configure whether to monitor configuration file changes. - -- Configuration priority - - Through configuration, choose to use local configuration or remote configuration first. - -- Supports multiple configuration file formats - - Supports configuration files in `yaml`, `properties`, `json` and key-value pair. - -## Core Configurations - -```yaml -spring: - cloud: - k8s: - config: - enabled: true - namespace: default # The namespace where the configuration is located (global configuration). If it is inside the Kubernetes cluster, it defaults to the namespace where the current pod is located; if it is outside the Kubernetes cluster, it defaults to the namespace of the current context - preference: remote # Configuration priority (global configuration), remote is preferred to use remote configuration, local is preferred to use local configuration, and the default is remote - refreshable: true # Whether to enable dynamic update configuration (global configuration), the default is true - refresh-on-delete: false # Whether to automatically refresh when deleting the configuration, enabling this configuration may bring certain risks, if your configuration items only exist on the remote side but not locally, if you delete the configmap by mistake, it may cause abnormalities in the program, so the default value is false - fail-on-missing-config: true - config-maps: - - name: my-configmap # configmap name - namespace: default # The namespace where configmap is located will override the global configuration of the namespace - preference: remote # Configuration priority, which will override the global configuration of preference - refreshable: true # Whether to enable dynamic update configuration, it will override the refresh-enabled global configuration - secrets: - - name: my-secret # secret name - namespace: default # The namespace where the secret is located will override the global configuration of the namespace - preference: remote # Configuration priority, which will override the global configuration of preference - refreshable: false # Whether to enable dynamic update configuration will override the global configuration of refresh-enabled, because secrets generally do not require dynamic refresh, so the default value is false -``` - -## Best Practices - -Spring Cloud provides the capability of dynamically refreshing the Environment at runtime, which mainly dynamically -updates the properties of two types of beans: - -- Beans annotated with `@ConfigurationProperties` -- Beans annotated with `@RefreshScope` - -A good practice is to use `@ConfigurationProperties` to organize your configurations. - -In general, the configuration of an application falls into two categories: - -- Basic configuration - - - public - - It can be managed through the configuration center or the jar package. Generally, there is no need for - dynamic updates, such as Tomcat connection pool parameters, database connection pool parameters, etc. - - - private - - It can be placed in a local configuration file, such as database connection information. This type of - configuration is generally sensitive and can be managed through Kubernetes Secret. - -- Business configuration - - This type of configuration should be strongly related to the business logic, but users need to judge whether they need - to be - placed in the configuration center and whether there is a need for dynamic updates. diff --git a/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/KubernetesConfigProperties.java b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/KubernetesConfigProperties.java index db26db2c6f..b097d7f201 100644 --- a/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/KubernetesConfigProperties.java +++ b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/KubernetesConfigProperties.java @@ -316,7 +316,7 @@ public static class Secret { */ private String name; /** - * Namespace, + * Namespace, using * {@code spring.cloud.k8s.config.namespace} if not * set. */ diff --git a/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/core/ConfigEnvironmentPostProcessor.java b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/core/ConfigEnvironmentPostProcessor.java index f11ffbf02d..a1a060c8d1 100644 --- a/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/core/ConfigEnvironmentPostProcessor.java +++ b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/core/ConfigEnvironmentPostProcessor.java @@ -16,6 +16,7 @@ package com.alibaba.cloud.kubernetes.config.core; +import java.net.HttpURLConnection; import java.util.Collections; import java.util.List; import java.util.Objects; @@ -23,7 +24,10 @@ import com.alibaba.cloud.kubernetes.commons.KubernetesClientHolder; import com.alibaba.cloud.kubernetes.config.KubernetesConfigProperties; -import com.alibaba.cloud.kubernetes.config.exception.ConfigMissingException; +import com.alibaba.cloud.kubernetes.config.exception.AbstractKubernetesConfigException; +import com.alibaba.cloud.kubernetes.config.exception.KubernetesConfigMissingException; +import com.alibaba.cloud.kubernetes.config.exception.KubernetesForbiddenException; +import com.alibaba.cloud.kubernetes.config.exception.KubernetesUnavailableException; import com.alibaba.cloud.kubernetes.config.util.Converters; import com.alibaba.cloud.kubernetes.config.util.Pair; import com.alibaba.cloud.kubernetes.config.util.Preference; @@ -31,6 +35,7 @@ import io.fabric8.kubernetes.api.model.ConfigMap; import io.fabric8.kubernetes.api.model.Secret; import io.fabric8.kubernetes.client.KubernetesClient; +import io.fabric8.kubernetes.client.KubernetesClientException; import org.apache.commons.logging.Log; import org.springframework.boot.SpringApplication; @@ -74,6 +79,8 @@ public void postProcessEnvironment(ConfigurableEnvironment environment, return; } + failApplicationStartUpIfKubernetesUnavailable(client); + KubernetesConfigProperties properties = getKubernetesConfigProperties( environment); @@ -96,6 +103,10 @@ else if (resource instanceof Secret) { } } + private static void failApplicationStartUpIfKubernetesUnavailable( + KubernetesClient client) { + } + private static KubernetesConfigProperties getKubernetesConfigProperties( ConfigurableEnvironment environment) { return Optional @@ -166,8 +177,15 @@ private EnumerablePropertySource propertySourceForConfigMap( if (isRefreshing() && !cm.getRefreshable()) { return null; } - ConfigMap configMap = client.configMaps().inNamespace(cm.getNamespace()) - .withName(cm.getName()).get(); + ConfigMap configMap; + try { + configMap = client.configMaps().inNamespace(cm.getNamespace()) + .withName(cm.getName()).get(); + } + catch (KubernetesClientException e) { + throw kubernetesConfigException(ConfigMap.class, cm.getName(), + cm.getNamespace(), e); + } if (configMap == null) { log.warn(String.format("ConfigMap '%s' not found in namespace '%s'", cm.getName(), cm.getNamespace())); @@ -178,6 +196,16 @@ private EnumerablePropertySource propertySourceForConfigMap( return Converters.toPropertySource(configMap); } + private static AbstractKubernetesConfigException kubernetesConfigException( + Class type, String name, String namespace, KubernetesClientException e) { + // Usually the Service Account or user does not have enough privileges. + if (e.getCode() == HttpURLConnection.HTTP_FORBIDDEN) { + return new KubernetesForbiddenException(type, name, namespace, e); + } + // Can't connect to the kubernetes cluster. + return new KubernetesUnavailableException(type, name, namespace, e); + } + /** * Fail the application start up if necessary when the resource is missing. * @@ -192,7 +220,7 @@ private EnumerablePropertySource propertySourceForConfigMap( private static void failApplicationStartUpIfNecessary(Class type, String name, String namespace, KubernetesConfigProperties properties) { if (!isRefreshing() && properties.isFailOnMissingConfig()) { - throw new ConfigMissingException(type, name, namespace); + throw new KubernetesConfigMissingException(type, name, namespace, null); } } @@ -202,8 +230,15 @@ private EnumerablePropertySource propertySourceForSecret( if (isRefreshing() && !secret.getRefreshable()) { return null; } - Secret secretInK8s = client.secrets().inNamespace(secret.getNamespace()) - .withName(secret.getName()).get(); + Secret secretInK8s; + try { + secretInK8s = client.secrets().inNamespace(secret.getNamespace()) + .withName(secret.getName()).get(); + } + catch (KubernetesClientException e) { + throw kubernetesConfigException(Secret.class, secret.getName(), + secret.getNamespace(), e); + } if (secretInK8s == null) { log.warn(String.format("Secret '%s' not found in namespace '%s'", secret.getName(), secret.getNamespace())); diff --git a/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/exception/ConfigMissingException.java b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/exception/AbstractKubernetesConfigException.java similarity index 83% rename from spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/exception/ConfigMissingException.java rename to spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/exception/AbstractKubernetesConfigException.java index 9d37e31025..c7b994e78e 100644 --- a/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/exception/ConfigMissingException.java +++ b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/exception/AbstractKubernetesConfigException.java @@ -19,13 +19,14 @@ /** * @author Freeman */ -public class ConfigMissingException extends RuntimeException { - +public abstract class AbstractKubernetesConfigException extends RuntimeException { private final Class type; private final String name; private final String namespace; - public ConfigMissingException(Class type, String name, String namespace) { + public AbstractKubernetesConfigException(Class type, String name, String namespace, + Throwable cause) { + super(cause); this.type = type; this.name = name; this.namespace = namespace; diff --git a/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/exception/KubernetesConfigMissingException.java b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/exception/KubernetesConfigMissingException.java new file mode 100644 index 0000000000..928c447337 --- /dev/null +++ b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/exception/KubernetesConfigMissingException.java @@ -0,0 +1,28 @@ +/* + * Copyright 2013-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.alibaba.cloud.kubernetes.config.exception; + +/** + * @author Freeman + */ +public class KubernetesConfigMissingException extends AbstractKubernetesConfigException { + + public KubernetesConfigMissingException(Class type, String name, String namespace, + Throwable cause) { + super(type, name, namespace, cause); + } +} diff --git a/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/exception/ConfigMissingFailureAnalyzer.java b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/exception/KubernetesConfigMissingFailureAnalyzer.java similarity index 89% rename from spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/exception/ConfigMissingFailureAnalyzer.java rename to spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/exception/KubernetesConfigMissingFailureAnalyzer.java index eaf04ab750..0ace7cafcd 100644 --- a/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/exception/ConfigMissingFailureAnalyzer.java +++ b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/exception/KubernetesConfigMissingFailureAnalyzer.java @@ -24,11 +24,11 @@ /** * @author Freeman */ -public class ConfigMissingFailureAnalyzer - extends AbstractFailureAnalyzer { +public class KubernetesConfigMissingFailureAnalyzer + extends AbstractFailureAnalyzer { @Override protected FailureAnalysis analyze(Throwable rootFailure, - ConfigMissingException cause) { + KubernetesConfigMissingException cause) { String description = String.format("%s name '%s' is missing in namespace '%s'", cause.getType().getSimpleName(), cause.getName(), cause.getNamespace()); String action = String.format( diff --git a/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/exception/KubernetesForbiddenException.java b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/exception/KubernetesForbiddenException.java new file mode 100644 index 0000000000..6275292523 --- /dev/null +++ b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/exception/KubernetesForbiddenException.java @@ -0,0 +1,28 @@ +/* + * Copyright 2013-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.alibaba.cloud.kubernetes.config.exception; + +/** + * @author Freeman + */ +public class KubernetesForbiddenException extends AbstractKubernetesConfigException { + + public KubernetesForbiddenException(Class type, String name, String namespace, + Throwable cause) { + super(type, name, namespace, cause); + } +} diff --git a/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/exception/KubernetesForbiddenFailureAnalyzer.java b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/exception/KubernetesForbiddenFailureAnalyzer.java new file mode 100644 index 0000000000..d760497ba1 --- /dev/null +++ b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/exception/KubernetesForbiddenFailureAnalyzer.java @@ -0,0 +1,40 @@ +/* + * Copyright 2013-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.alibaba.cloud.kubernetes.config.exception; + +import com.alibaba.cloud.kubernetes.commons.KubernetesUtils; + +import org.springframework.boot.diagnostics.AbstractFailureAnalyzer; +import org.springframework.boot.diagnostics.FailureAnalysis; + +/** + * @author Freeman + */ +public class KubernetesForbiddenFailureAnalyzer + extends AbstractFailureAnalyzer { + + @Override + protected FailureAnalysis analyze(Throwable rootFailure, + KubernetesForbiddenException cause) { + String description = String.format( + "It looks like you don't have enough access to the resource (%s '%s.%s') in context '%s'.", + cause.getType().getSimpleName(), cause.getName(), cause.getNamespace(), + KubernetesUtils.config().getCurrentContext().getName()); + String action = "Please ask the administrator to add enough permissions for you, or are you just in the wrong context?"; + return new FailureAnalysis(description, action, cause); + } +} diff --git a/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/exception/KubernetesUnavailableException.java b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/exception/KubernetesUnavailableException.java new file mode 100644 index 0000000000..07fb545a14 --- /dev/null +++ b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/exception/KubernetesUnavailableException.java @@ -0,0 +1,28 @@ +/* + * Copyright 2013-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.alibaba.cloud.kubernetes.config.exception; + +/** + * @author Freeman + */ +public class KubernetesUnavailableException extends AbstractKubernetesConfigException { + + public KubernetesUnavailableException(Class type, String name, String namespace, + Throwable cause) { + super(type, name, namespace, cause); + } +} diff --git a/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/exception/KubernetesUnavailableFailureAnalyzer.java b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/exception/KubernetesUnavailableFailureAnalyzer.java new file mode 100644 index 0000000000..5528477183 --- /dev/null +++ b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/exception/KubernetesUnavailableFailureAnalyzer.java @@ -0,0 +1,44 @@ +/* + * Copyright 2013-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.alibaba.cloud.kubernetes.config.exception; + +import java.util.stream.Collectors; + +import com.alibaba.cloud.kubernetes.commons.KubernetesUtils; +import io.fabric8.kubernetes.api.model.NamedContext; + +import org.springframework.boot.diagnostics.AbstractFailureAnalyzer; +import org.springframework.boot.diagnostics.FailureAnalysis; + +/** + * @author Freeman + */ +public class KubernetesUnavailableFailureAnalyzer + extends AbstractFailureAnalyzer { + @Override + protected FailureAnalysis analyze(Throwable rootFailure, + KubernetesUnavailableException cause) { + String description = String.format( + "Current context '%s' can not connect to Kubernetes cluster.\n\n" + + "Are you sure you've set the right context? Available contexts are: %s.\n", + KubernetesUtils.config().getCurrentContext().getName(), + KubernetesUtils.config().getContexts().stream().map(NamedContext::getName) + .collect(Collectors.toList())); + String action = "Please check your kube config file and Kubernetes cluster status."; + return new FailureAnalysis(description, action, cause); + } +} diff --git a/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/resources/META-INF/spring.factories b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/resources/META-INF/spring.factories index d2fbcf1b7a..3ead7128b5 100644 --- a/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/resources/META-INF/spring.factories +++ b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/resources/META-INF/spring.factories @@ -5,4 +5,6 @@ org.springframework.boot.env.EnvironmentPostProcessor=\ com.alibaba.cloud.kubernetes.config.core.ConfigEnvironmentPostProcessor org.springframework.boot.diagnostics.FailureAnalyzer=\ - com.alibaba.cloud.kubernetes.config.exception.ConfigMissingFailureAnalyzer \ No newline at end of file + com.alibaba.cloud.kubernetes.config.exception.KubernetesConfigMissingFailureAnalyzer,\ + com.alibaba.cloud.kubernetes.config.exception.KubernetesUnavailableFailureAnalyzer,\ + com.alibaba.cloud.kubernetes.config.exception.KubernetesForbiddenFailureAnalyzer \ No newline at end of file diff --git a/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/test/java/com/alibaba/cloud/kubernetes/config/MissingConfigIntegrationTests.java b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/test/java/com/alibaba/cloud/kubernetes/config/MissingConfigIntegrationTests.java index 21f962838f..a1c648e1a5 100644 --- a/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/test/java/com/alibaba/cloud/kubernetes/config/MissingConfigIntegrationTests.java +++ b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/test/java/com/alibaba/cloud/kubernetes/config/MissingConfigIntegrationTests.java @@ -16,7 +16,7 @@ package com.alibaba.cloud.kubernetes.config; -import com.alibaba.cloud.kubernetes.config.exception.ConfigMissingException; +import com.alibaba.cloud.kubernetes.config.exception.KubernetesConfigMissingException; import com.alibaba.cloud.kubernetes.config.testsupport.KubernetesAvailable; import org.junit.jupiter.api.Test; @@ -35,7 +35,7 @@ public class MissingConfigIntegrationTests { void testEnabledFailOnMissingConfig() { assertThatCode(() -> new SpringApplicationBuilder(Empty.class) .web(WebApplicationType.NONE).profiles("missing-config").run().close()) - .isInstanceOf(ConfigMissingException.class); + .isInstanceOf(KubernetesConfigMissingException.class); } @Test From 03a30bb391961da8d084b58ccaf578e4659cee3e Mon Sep 17 00:00:00 2001 From: Freeman Lau Date: Fri, 3 Feb 2023 21:59:08 +0800 Subject: [PATCH 16/37] remove used code --- .../config/core/ConfigEnvironmentPostProcessor.java | 6 ------ 1 file changed, 6 deletions(-) diff --git a/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/core/ConfigEnvironmentPostProcessor.java b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/core/ConfigEnvironmentPostProcessor.java index a1a060c8d1..82b6253f51 100644 --- a/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/core/ConfigEnvironmentPostProcessor.java +++ b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/core/ConfigEnvironmentPostProcessor.java @@ -79,8 +79,6 @@ public void postProcessEnvironment(ConfigurableEnvironment environment, return; } - failApplicationStartUpIfKubernetesUnavailable(client); - KubernetesConfigProperties properties = getKubernetesConfigProperties( environment); @@ -103,10 +101,6 @@ else if (resource instanceof Secret) { } } - private static void failApplicationStartUpIfKubernetesUnavailable( - KubernetesClient client) { - } - private static KubernetesConfigProperties getKubernetesConfigProperties( ConfigurableEnvironment environment) { return Optional From 97065612a78987fe9d052c5e3600123401231711 Mon Sep 17 00:00:00 2001 From: Freeman Lau Date: Fri, 3 Feb 2023 22:38:49 +0800 Subject: [PATCH 17/37] fix config missing when using both configmap and secret --- .../config/KubernetesConfigProperties.java | 7 +++++ .../core/ConfigEnvironmentPostProcessor.java | 26 +++++-------------- 2 files changed, 13 insertions(+), 20 deletions(-) diff --git a/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/KubernetesConfigProperties.java b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/KubernetesConfigProperties.java index b097d7f201..a249387cf2 100644 --- a/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/KubernetesConfigProperties.java +++ b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/KubernetesConfigProperties.java @@ -183,6 +183,13 @@ public String toString() { @Override public void afterPropertiesSet() { + this.merge(); + } + + /** + * Merge the default properties value to the ConfigMaps and Secrets. + */ + public void merge() { mergeConfigmaps(); mergeSecrets(); } diff --git a/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/core/ConfigEnvironmentPostProcessor.java b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/core/ConfigEnvironmentPostProcessor.java index 82b6253f51..2614e54c6e 100644 --- a/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/core/ConfigEnvironmentPostProcessor.java +++ b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/core/ConfigEnvironmentPostProcessor.java @@ -42,7 +42,6 @@ import org.springframework.boot.context.properties.bind.Binder; import org.springframework.boot.env.EnvironmentPostProcessor; import org.springframework.boot.logging.DeferredLogFactory; -import org.springframework.cloud.endpoint.event.RefreshEvent; import org.springframework.core.Ordered; import org.springframework.core.env.ConfigurableEnvironment; import org.springframework.core.env.EnumerablePropertySource; @@ -82,23 +81,10 @@ public void postProcessEnvironment(ConfigurableEnvironment environment, KubernetesConfigProperties properties = getKubernetesConfigProperties( environment); - if (isRefreshing()) { - RefreshEvent event = RefreshContext.get().refreshEvent(); - Object resource = event.getSource(); - if (resource instanceof ConfigMap) { - pullConfigMaps(properties, environment); - } - else if (resource instanceof Secret) { - pullSecrets(properties, environment); - } - else { - log.warn("Refreshed a unknown resource type: " + resource.getClass()); - } - } - else { - pullConfigMaps(properties, environment); - pullSecrets(properties, environment); - } + // NOTE: current environment is brand new, we can't just refresh a single + // resource, all resources must be re-pulled! + pullConfigMaps(properties, environment); + pullSecrets(properties, environment); } private static KubernetesConfigProperties getKubernetesConfigProperties( @@ -111,7 +97,7 @@ private static KubernetesConfigProperties getKubernetesConfigProperties( .bind(KubernetesConfigProperties.PREFIX, KubernetesConfigProperties.class) .orElseGet(KubernetesConfigProperties::new); - prop.afterPropertiesSet(); + prop.merge(); return prop; }); } @@ -192,7 +178,7 @@ private EnumerablePropertySource propertySourceForConfigMap( private static AbstractKubernetesConfigException kubernetesConfigException( Class type, String name, String namespace, KubernetesClientException e) { - // Usually the Service Account or user does not have enough privileges. + // Usually the ServiceAccount or user does not have enough privileges. if (e.getCode() == HttpURLConnection.HTTP_FORBIDDEN) { return new KubernetesForbiddenException(type, name, namespace, e); } From f795234c54690143747066212a068ebc0d6df54b Mon Sep 17 00:00:00 2001 From: Freeman Lau Date: Fri, 3 Feb 2023 22:53:49 +0800 Subject: [PATCH 18/37] Revert "fix config missing when using both configmap and secret" This reverts commit 97065612a78987fe9d052c5e3600123401231711. --- .../config/KubernetesConfigProperties.java | 7 ----- .../core/ConfigEnvironmentPostProcessor.java | 26 ++++++++++++++----- 2 files changed, 20 insertions(+), 13 deletions(-) diff --git a/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/KubernetesConfigProperties.java b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/KubernetesConfigProperties.java index a249387cf2..b097d7f201 100644 --- a/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/KubernetesConfigProperties.java +++ b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/KubernetesConfigProperties.java @@ -183,13 +183,6 @@ public String toString() { @Override public void afterPropertiesSet() { - this.merge(); - } - - /** - * Merge the default properties value to the ConfigMaps and Secrets. - */ - public void merge() { mergeConfigmaps(); mergeSecrets(); } diff --git a/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/core/ConfigEnvironmentPostProcessor.java b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/core/ConfigEnvironmentPostProcessor.java index 2614e54c6e..82b6253f51 100644 --- a/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/core/ConfigEnvironmentPostProcessor.java +++ b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/core/ConfigEnvironmentPostProcessor.java @@ -42,6 +42,7 @@ import org.springframework.boot.context.properties.bind.Binder; import org.springframework.boot.env.EnvironmentPostProcessor; import org.springframework.boot.logging.DeferredLogFactory; +import org.springframework.cloud.endpoint.event.RefreshEvent; import org.springframework.core.Ordered; import org.springframework.core.env.ConfigurableEnvironment; import org.springframework.core.env.EnumerablePropertySource; @@ -81,10 +82,23 @@ public void postProcessEnvironment(ConfigurableEnvironment environment, KubernetesConfigProperties properties = getKubernetesConfigProperties( environment); - // NOTE: current environment is brand new, we can't just refresh a single - // resource, all resources must be re-pulled! - pullConfigMaps(properties, environment); - pullSecrets(properties, environment); + if (isRefreshing()) { + RefreshEvent event = RefreshContext.get().refreshEvent(); + Object resource = event.getSource(); + if (resource instanceof ConfigMap) { + pullConfigMaps(properties, environment); + } + else if (resource instanceof Secret) { + pullSecrets(properties, environment); + } + else { + log.warn("Refreshed a unknown resource type: " + resource.getClass()); + } + } + else { + pullConfigMaps(properties, environment); + pullSecrets(properties, environment); + } } private static KubernetesConfigProperties getKubernetesConfigProperties( @@ -97,7 +111,7 @@ private static KubernetesConfigProperties getKubernetesConfigProperties( .bind(KubernetesConfigProperties.PREFIX, KubernetesConfigProperties.class) .orElseGet(KubernetesConfigProperties::new); - prop.merge(); + prop.afterPropertiesSet(); return prop; }); } @@ -178,7 +192,7 @@ private EnumerablePropertySource propertySourceForConfigMap( private static AbstractKubernetesConfigException kubernetesConfigException( Class type, String name, String namespace, KubernetesClientException e) { - // Usually the ServiceAccount or user does not have enough privileges. + // Usually the Service Account or user does not have enough privileges. if (e.getCode() == HttpURLConnection.HTTP_FORBIDDEN) { return new KubernetesForbiddenException(type, name, namespace, e); } From 36b03fde97a38ab86bbc0e536baed5c695f5afc2 Mon Sep 17 00:00:00 2001 From: Freeman Lau Date: Fri, 3 Feb 2023 23:26:49 +0800 Subject: [PATCH 19/37] only refresh current resource --- .../core/ConfigEnvironmentPostProcessor.java | 79 +++++++++++-------- 1 file changed, 45 insertions(+), 34 deletions(-) diff --git a/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/core/ConfigEnvironmentPostProcessor.java b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/core/ConfigEnvironmentPostProcessor.java index 82b6253f51..9db839939a 100644 --- a/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/core/ConfigEnvironmentPostProcessor.java +++ b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/core/ConfigEnvironmentPostProcessor.java @@ -28,7 +28,6 @@ import com.alibaba.cloud.kubernetes.config.exception.KubernetesConfigMissingException; import com.alibaba.cloud.kubernetes.config.exception.KubernetesForbiddenException; import com.alibaba.cloud.kubernetes.config.exception.KubernetesUnavailableException; -import com.alibaba.cloud.kubernetes.config.util.Converters; import com.alibaba.cloud.kubernetes.config.util.Pair; import com.alibaba.cloud.kubernetes.config.util.Preference; import com.alibaba.cloud.kubernetes.config.util.RefreshContext; @@ -49,6 +48,7 @@ import org.springframework.core.env.MutablePropertySources; import org.springframework.core.env.StandardEnvironment; +import static com.alibaba.cloud.kubernetes.config.util.Converters.toPropertySource; import static java.util.stream.Collectors.groupingBy; import static java.util.stream.Collectors.mapping; import static java.util.stream.Collectors.toList; @@ -85,11 +85,16 @@ public void postProcessEnvironment(ConfigurableEnvironment environment, if (isRefreshing()) { RefreshEvent event = RefreshContext.get().refreshEvent(); Object resource = event.getSource(); + // Just add it, {@link + // org.springframework.cloud.context.refresh.ContextRefresher} will + // replace the PropertySource for you. if (resource instanceof ConfigMap) { - pullConfigMaps(properties, environment); + environment.getPropertySources() + .addLast(toPropertySource((ConfigMap) resource)); } else if (resource instanceof Secret) { - pullSecrets(properties, environment); + environment.getPropertySources() + .addLast(toPropertySource((Secret) resource)); } else { log.warn("Refreshed a unknown resource type: " + resource.getClass()); @@ -168,17 +173,20 @@ private static void addPropertySourcesToEnvironment( private EnumerablePropertySource propertySourceForConfigMap( KubernetesConfigProperties.ConfigMap cm, KubernetesConfigProperties properties) { - if (isRefreshing() && !cm.getRefreshable()) { - return null; - } ConfigMap configMap; try { configMap = client.configMaps().inNamespace(cm.getNamespace()) .withName(cm.getName()).get(); } catch (KubernetesClientException e) { - throw kubernetesConfigException(ConfigMap.class, cm.getName(), - cm.getNamespace(), e); + if (!isRefreshing()) { + throw kubernetesConfigException(ConfigMap.class, cm.getName(), + cm.getNamespace(), e); + } + log.warn( + "Kubernetes client exception while refreshing ConfigMap, so properties value won't change", + e); + return null; } if (configMap == null) { log.warn(String.format("ConfigMap '%s' not found in namespace '%s'", @@ -187,7 +195,35 @@ private EnumerablePropertySource propertySourceForConfigMap( cm.getNamespace(), properties); return null; } - return Converters.toPropertySource(configMap); + return toPropertySource(configMap); + } + + private EnumerablePropertySource propertySourceForSecret( + KubernetesConfigProperties.Secret secret, + KubernetesConfigProperties properties) { + Secret secretInK8s; + try { + secretInK8s = client.secrets().inNamespace(secret.getNamespace()) + .withName(secret.getName()).get(); + } + catch (KubernetesClientException e) { + if (!isRefreshing()) { + throw kubernetesConfigException(Secret.class, secret.getName(), + secret.getNamespace(), e); + } + log.warn( + "Kubernetes client exception while refreshing Secret, so properties value won't change", + e); + return null; + } + if (secretInK8s == null) { + log.warn(String.format("Secret '%s' not found in namespace '%s'", + secret.getName(), secret.getNamespace())); + failApplicationStartUpIfNecessary(Secret.class, secret.getName(), + secret.getNamespace(), properties); + return null; + } + return toPropertySource(secretInK8s); } private static AbstractKubernetesConfigException kubernetesConfigException( @@ -218,31 +254,6 @@ private static void failApplicationStartUpIfNecessary(Class type, String name } } - private EnumerablePropertySource propertySourceForSecret( - KubernetesConfigProperties.Secret secret, - KubernetesConfigProperties properties) { - if (isRefreshing() && !secret.getRefreshable()) { - return null; - } - Secret secretInK8s; - try { - secretInK8s = client.secrets().inNamespace(secret.getNamespace()) - .withName(secret.getName()).get(); - } - catch (KubernetesClientException e) { - throw kubernetesConfigException(Secret.class, secret.getName(), - secret.getNamespace(), e); - } - if (secretInK8s == null) { - log.warn(String.format("Secret '%s' not found in namespace '%s'", - secret.getName(), secret.getNamespace())); - failApplicationStartUpIfNecessary(Secret.class, secret.getName(), - secret.getNamespace(), properties); - return null; - } - return Converters.toPropertySource(secretInK8s); - } - private static boolean isRefreshing() { return RefreshContext.get() != null; } From e630edab5ae906e5e4569e91e1fa7a727bd29999 Mon Sep 17 00:00:00 2001 From: Freeman Lau Date: Fri, 3 Feb 2023 23:32:20 +0800 Subject: [PATCH 20/37] make HasMetadataResourceEventHandler package private --- .../config/core/HasMetadataResourceEventHandler.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/core/HasMetadataResourceEventHandler.java b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/core/HasMetadataResourceEventHandler.java index eb0cce3fbd..2fa55a8de2 100644 --- a/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/core/HasMetadataResourceEventHandler.java +++ b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/core/HasMetadataResourceEventHandler.java @@ -31,7 +31,7 @@ /** * @author Freeman */ -public class HasMetadataResourceEventHandler +class HasMetadataResourceEventHandler implements ResourceEventHandler { private static final Logger log = LoggerFactory .getLogger(HasMetadataResourceEventHandler.class); @@ -39,7 +39,7 @@ public class HasMetadataResourceEventHandler private final ApplicationContext context; private final KubernetesConfigProperties properties; - public HasMetadataResourceEventHandler(ApplicationContext context, + HasMetadataResourceEventHandler(ApplicationContext context, KubernetesConfigProperties properties) { this.context = context; this.properties = properties; From 9b4ff99af0b6629fd0ed1a544ec8630e39b5e41c Mon Sep 17 00:00:00 2001 From: Freeman Lau Date: Fri, 3 Feb 2023 23:42:17 +0800 Subject: [PATCH 21/37] method name lowercase --- .../kubernetes/config/KubernetesConfigAutoConfiguration.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/KubernetesConfigAutoConfiguration.java b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/KubernetesConfigAutoConfiguration.java index e31a3748bb..34900a2778 100644 --- a/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/KubernetesConfigAutoConfiguration.java +++ b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/KubernetesConfigAutoConfiguration.java @@ -41,7 +41,7 @@ public class KubernetesConfigAutoConfiguration { @Bean @ConditionalOnMissingBean - public ConfigWatcher KubernetesConfigWatcher(KubernetesConfigProperties properties, + public ConfigWatcher kubernetesConfigWatcher(KubernetesConfigProperties properties, KubernetesClient kubernetesClient) { return new ConfigWatcher(properties, kubernetesClient); } From 92be6877151a8b3686354ebe8ac7aac96f6bcc4d Mon Sep 17 00:00:00 2001 From: Freeman Lau Date: Sat, 4 Feb 2023 11:37:19 +0800 Subject: [PATCH 22/37] docs and example done --- .../src/main/asciidoc/kubernetes-config.adoc | 176 +++++++++--------- .../kubernetes-config-example/README.md | 61 ++++++ .../deployments.yaml | 20 ++ .../kubernetes-config-example/pom.xml | 2 +- .../config/controller/EchoController.java | 12 +- .../config/filter/BlacklistFilter.java | 14 +- .../properties/BlacklistProperties.java | 1 + .../src/main/resources/application.yml | 8 +- .../kubernetes/commons/KubernetesUtils.java | 3 +- .../config/KubernetesConfigProperties.java | 2 +- .../core/ConfigEnvironmentPostProcessor.java | 59 +++--- .../kubernetes/config/core/ConfigWatcher.java | 6 +- .../core/HasMetadataResourceEventHandler.java | 25 ++- 13 files changed, 240 insertions(+), 149 deletions(-) create mode 100644 spring-cloud-alibaba-examples/kubernetes-config-example/README.md create mode 100644 spring-cloud-alibaba-examples/kubernetes-config-example/deployments.yaml diff --git a/spring-cloud-alibaba-docs/src/main/asciidoc/kubernetes-config.adoc b/spring-cloud-alibaba-docs/src/main/asciidoc/kubernetes-config.adoc index 149ac39a39..03677375ec 100644 --- a/spring-cloud-alibaba-docs/src/main/asciidoc/kubernetes-config.adoc +++ b/spring-cloud-alibaba-docs/src/main/asciidoc/kubernetes-config.adoc @@ -4,103 +4,119 @@ The purpose of this module is to use Kubernetes ConfigMap/Secret as a distribute === Quick Start -Maven: +Go to the [example](./example) and follow the docs to run the example. +You got this! -[source,xml] ----- - - com.alibaba.cloud - spring-cloud-starter-alibaba-kubernetes-config - ----- +=== Main Features -Gradle: +If you have experience with `spring-cloud-starter-alibaba-nacos-config`, you will find out that the usage is very similar. `spring-cloud-starter-alibaba-kubernetes-config` has provided all the core features of `spring-cloud-starter-alibaba-nacos-config`, and provides more easier-to-use configurations, better performance and more in line with cloud-native. -[source,groovy] ----- -implementation 'com.alibaba.cloud:spring-cloud-starter-alibaba-kubernetes-config' ----- +==== Dynamic update configuration(ConfigMap/Secret) -First you need a Kubernetes cluster, you can use https://www.docker.com/products/docker-desktop/[docker-desktop] -or https://minikube.sigs.k8s.io/docs/[minikube] to create a cluster. +You don't need to restart the application when the configuration is updated, and the application can dynamically update the configurations. -- Clone the project +NOTE: Spring Cloud provides the capability of dynamically refreshing the Environment at runtime, which mainly dynamically updates the properties of two types of beans: `@ConfigurationProperties` and `@RefreshScope` beans. -[source,shell] ----- -git clone --depth=1 https://github.com/alibaba/spring-cloud-alibaba.git ----- +*Best Practices: use `@ConfigurationProperties` to organize your configurations!* -- Create Role and RoleBinding for ServiceAccount +Related configuration: -[source,shell] +[source,yaml] ---- -# Created a ClusterRole just for the example, but in fact, you can control resources more finely, only need the get,list,watch permissions of ConfigMap/Secret -kubectl create clusterrole config-cluster-reader --verb=get,list,watch --resource=configmaps,secrets -# Bind ClusterRole to ServiceAccount (namespace: default, name: default) -kubectl create clusterrolebinding config-cluster-reader-default-default --clusterrole config-cluster-reader --serviceaccount default:default +spring: + cloud: + k8s: + config: + refreshable: false # global default config, default value is false + config-maps: + - name: my-configmap + refreshable: true # my-configmap will detect config change and refresh + secrets: + - name: my-secret # my-secret will NOT detect config change and refresh ---- -- Build and Start +==== Configuration priority -[source,shell] ----- -./mvnw clean package -pl com.alibaba.cloud:kubernetes-config-example -am -DskipTests +If there are same configurations in both local `application.yml` and ConfigMaps/Secret, you can choose which configuration to use first. -docker build -f spring-cloud-alibaba-examples/kubernetes-config-example/Dockerfile -t kubernetes-config-example:latest . +Related configuration: -kubectl apply -f spring-cloud-alibaba-examples/kubernetes-config-example/deployment.yaml +[source,yaml] ---- - -[source,shell] +spring: + cloud: + k8s: + config: + preference: remote # Global default config, default value is remote, means remote config will override local config + config-maps: + - name: my-configmap + preference: local ---- -# Execute the following command after the application startup, the startup process should be very fast (less than 3s) -curl http://localhost:`kubectl get service kubernetes-config-example -o jsonpath='{..nodePort}'`/price -# You should see a response of `100` ----- +==== Multiple configuration file formats -- Add ConfigMap +Supports configuration files in `yaml`, `properties`, `json` and key-value pair. -[source,shell] ----- -# This ConfigMap is being monitored by the current application, so when this ConfigMap is added, the application will automatically update the configuration -kubectl apply -f spring-cloud-alibaba-examples/kubernetes-config-example/configmap-example-01.yaml +NOTE: Secret only supports key-value pair! -# Visit again -curl http://localhost:`kubectl get svc kubernetes-config -o jsonpath='{..nodePort}'`/price +Example: -# You should see a response of `200` +ConfigMap: +[source,yaml] +---- +apiVersion: v1 +kind: ConfigMap +metadata: + name: my-configmap +data: + blacklist.yml: | + blacklist: + user-ids: + - 1 + - 2 ---- -You can modify the configuration in `configmap-example-01.yaml`, and then re-apply the file to observe the change of the interface result. - -Through the above operations, you can see that the application can dynamically update the configuration without restarting. - -- Delete Resources - -[source,shell] +Secret: +[source,yaml] ---- -# Delete all resources created by the above operations -kubectl delete -f spring-cloud-alibaba-examples/kubernetes-config-example/deployment.yaml -kubectl delete -f spring-cloud-alibaba-examples/kubernetes-config-example/configmap-example-01.yaml -kubectl delete clusterrole config-cluster-reader -kubectl delete clusterrolebinding config-cluster-reader-default-default +apiVersion: v1 +kind: Secret +metadata: + name: my-secret +stringData: + blacklist.user-ids[0]: 1 + blacklist.user-ids[1]: 1 ---- -=== Main Features +The two configurations above convey the same meaning. -- Dynamic update configuration(ConfigMap/Secret) +==== Abnormal operation protection - You can manually configure whether to monitor configuration file changes. +If config was deleted by mistake, you don't want your application to crash, right? `spring-cloud-starter-alibaba-kubernetes-config` provides a mechanism to protect the application from abnormal operation, such as deleting the configuration by mistake. -- Configuration priority +Related configuration: - Through configuration, choose to use local configuration or remote configuration first. +[source,yaml] +---- +spring: + cloud: + k8s: + config: + refresh-on-deleted: false # when detect the config was deleted, the application will not trigger a refresh. Default value is false. +---- -- Supports multiple configuration file formats +If your configuration is not synchronized across environments, and you have the `import-config` configuration in the `dev` environment but not in the `prod` environment (it is possible that you forgot to synchronize to `prod`), you may want to have a mechanism to help you find this problem and provide fail-fast ability to prevent the application from starting. - Supports configuration files in `yaml`, `properties`, `json` and key-value pair. +Related configuration: + +[source,yaml] +---- +spring: + cloud: + k8s: + config: + fail-on-missing-config: true # application will fail to start if the configs not found. Default value is true. +---- === Core Configurations @@ -111,28 +127,16 @@ spring: k8s: config: enabled: true - namespace: default # The namespace where the configuration is located (global configuration). If it is inside the Kubernetes cluster, it defaults to the namespace where the current pod is located; if it is outside the Kubernetes cluster, it defaults to the namespace of the current context - preference: remote # Configuration priority (global configuration), remote is preferred to use remote configuration, local is preferred to use local configuration, and the default is remote - refreshable: true # Whether to enable dynamic update configuration (global configuration), the default is true - refresh-on-delete: false # Whether to automatically refresh when deleting the configuration, enabling this configuration may bring certain risks, if your configuration items only exist on the remote side but not locally, if you delete the configmap by mistake, it may cause abnormalities in the program, so the default value is false + namespace: default # The namespace where the configuration is located (global configuration). If inside the Kubernetes cluster, it defaults to the namespace where the current pod is located; if outside the Kubernetes cluster, it defaults to the namespace of the current context. + preference: remote + refreshable: false + refresh-on-delete: false fail-on-missing-config: true config-maps: - - name: my-configmap # configmap name - namespace: default # The namespace where configmap is located will override the global configuration of the namespace - preference: remote # Configuration priority, which will override the global configuration of preference - refreshable: true # Whether to enable dynamic update configuration, it will override the refresh-enabled global configuration + - name: my-configmap + preference: remote + refreshable: true secrets: - - name: my-secret # secret name - namespace: default # The namespace where the secret is located will override the global configuration of the namespace - preference: remote # Configuration priority, which will override the global configuration of preference - refreshable: false # Whether to enable dynamic update configuration will override the global configuration of refresh-enabled, because secrets generally do not require dynamic refresh, so the default value is false + - name: my-secret + namespace: secret-namespace ---- - -=== Best Practices - -Spring Cloud provides the capability of dynamically refreshing the Environment at runtime, which mainly dynamically updates the properties of two types of beans: - -- Beans annotated with `@ConfigurationProperties` -- Beans annotated with `@RefreshScope` - -A good practice is to use `@ConfigurationProperties` to organize your configurations. diff --git a/spring-cloud-alibaba-examples/kubernetes-config-example/README.md b/spring-cloud-alibaba-examples/kubernetes-config-example/README.md new file mode 100644 index 0000000000..02c2bb6d50 --- /dev/null +++ b/spring-cloud-alibaba-examples/kubernetes-config-example/README.md @@ -0,0 +1,61 @@ +## Spring Cloud Starter Alibaba Kubernetes Config Example + +This example demonstrates the use of `spring-cloud-starter-alibaba-kubernetes-config` to implement a dynamic blacklist. + +### Procedure + +1. Apply the [deployments.yaml](./deployments.yaml) file + + Make sure you are in the `kubernetes-config-example` directory. + + ```shell + kubectl apply -f deployments.yaml + ``` + +2. Start the application + +3. Access the application + + ```shell + curl localhost:8080/echo -H 'x-user-id:1' + ``` + + You should see the user `1` has been blocked, you can check the [deployments.yaml](./deployments.yaml) to see blocked + userIds. + +4. Modify the `blacklist` configuration in the [deployments.yaml](./deployments.yaml) file + + ```yaml + apiVersion: v1 + kind: ConfigMap + metadata: + name: configmap-01 + namespace: default + data: + blacklist.yml: | + blacklist: + user-ids: + - 2 + ``` + + Then apply the file again + + ```shell + kubectl apply -f deployments.yaml + ``` + +5. Access the application again + + ```shell + curl localhost:8080/echo -H 'x-user-id:1' + ``` + + You should see the user `1` has not been blocked, now only user `2` is blocked. + +6. Delete resources + + Make sure you are in the `kubernetes-config-example` directory. + + ```shell + kubectl delete -f deployments.yaml + ``` \ No newline at end of file diff --git a/spring-cloud-alibaba-examples/kubernetes-config-example/deployments.yaml b/spring-cloud-alibaba-examples/kubernetes-config-example/deployments.yaml new file mode 100644 index 0000000000..4c31b1a3fd --- /dev/null +++ b/spring-cloud-alibaba-examples/kubernetes-config-example/deployments.yaml @@ -0,0 +1,20 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: configmap-01 + namespace: default +data: + blacklist.yml: | + blacklist: + user-ids: + - 1 + - 2 + +--- +apiVersion: v1 +kind: Secret +metadata: + name: secret-01 + namespace: default +stringData: + blacklist.header: X-User-Id diff --git a/spring-cloud-alibaba-examples/kubernetes-config-example/pom.xml b/spring-cloud-alibaba-examples/kubernetes-config-example/pom.xml index a47b439e09..9b2cccc2be 100644 --- a/spring-cloud-alibaba-examples/kubernetes-config-example/pom.xml +++ b/spring-cloud-alibaba-examples/kubernetes-config-example/pom.xml @@ -11,7 +11,7 @@ 4.0.0 kubernetes-config-example - Kubernetes Config Example + Spring Cloud Starter Alibaba Kubernetes Config Example Example demonstrating how to use Spring Cloud Alibaba Kubernetes Config jar diff --git a/spring-cloud-alibaba-examples/kubernetes-config-example/src/main/java/com/alibaba/cloud/examples/kubernetes/config/controller/EchoController.java b/spring-cloud-alibaba-examples/kubernetes-config-example/src/main/java/com/alibaba/cloud/examples/kubernetes/config/controller/EchoController.java index c268bc0966..d22864b7bd 100644 --- a/spring-cloud-alibaba-examples/kubernetes-config-example/src/main/java/com/alibaba/cloud/examples/kubernetes/config/controller/EchoController.java +++ b/spring-cloud-alibaba-examples/kubernetes-config-example/src/main/java/com/alibaba/cloud/examples/kubernetes/config/controller/EchoController.java @@ -16,20 +16,26 @@ package com.alibaba.cloud.examples.kubernetes.config.controller; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.cloud.context.config.annotation.RefreshScope; +import org.springframework.http.HttpHeaders; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RestController; -import static com.alibaba.cloud.examples.kubernetes.config.filter.BlacklistFilter.HEADER_USER_ID; - /** * @author Freeman */ @RestController +@RefreshScope public class EchoController { + @Value("${blacklist.header}") + private String userIdHeader; + @GetMapping("/echo") - public String echo(@RequestHeader(HEADER_USER_ID) String userId) { + public String echo(@RequestHeader HttpHeaders headers) { + String userId = headers.getFirst(userIdHeader); return String.format("Hello, %s", userId); } diff --git a/spring-cloud-alibaba-examples/kubernetes-config-example/src/main/java/com/alibaba/cloud/examples/kubernetes/config/filter/BlacklistFilter.java b/spring-cloud-alibaba-examples/kubernetes-config-example/src/main/java/com/alibaba/cloud/examples/kubernetes/config/filter/BlacklistFilter.java index 57e5cbc577..740a788936 100644 --- a/spring-cloud-alibaba-examples/kubernetes-config-example/src/main/java/com/alibaba/cloud/examples/kubernetes/config/filter/BlacklistFilter.java +++ b/spring-cloud-alibaba-examples/kubernetes-config-example/src/main/java/com/alibaba/cloud/examples/kubernetes/config/filter/BlacklistFilter.java @@ -33,10 +33,6 @@ */ @Component public class BlacklistFilter extends OncePerRequestFilter { - /** - * User id header. - */ - public static final String HEADER_USER_ID = "X-User-Id"; private final BlacklistProperties blacklistProperties; @@ -47,14 +43,16 @@ public BlacklistFilter(BlacklistProperties blacklistProperties) { @Override protected void doFilterInternal(HttpServletRequest req, HttpServletResponse resp, FilterChain chain) throws ServletException, IOException { - String userId = req.getHeader(HEADER_USER_ID); + String userIdHeader = blacklistProperties.getHeader(); + String userId = req.getHeader(userIdHeader); if (userId == null) { - resp.sendError(HttpServletResponse.SC_BAD_REQUEST, - HEADER_USER_ID + " header is required!"); + resp.sendError(HttpServletResponse.SC_UNAUTHORIZED, + userIdHeader + " header is required!"); return; } if (blacklistProperties.getUserIds().contains(userId)) { - resp.sendError(HttpServletResponse.SC_FORBIDDEN, "User is blacklisted!"); + resp.sendError(HttpServletResponse.SC_FORBIDDEN, + String.format("User %s has been blocked!", userId)); return; } chain.doFilter(req, resp); diff --git a/spring-cloud-alibaba-examples/kubernetes-config-example/src/main/java/com/alibaba/cloud/examples/kubernetes/config/properties/BlacklistProperties.java b/spring-cloud-alibaba-examples/kubernetes-config-example/src/main/java/com/alibaba/cloud/examples/kubernetes/config/properties/BlacklistProperties.java index 393f59e95c..1eea5e9d00 100644 --- a/spring-cloud-alibaba-examples/kubernetes-config-example/src/main/java/com/alibaba/cloud/examples/kubernetes/config/properties/BlacklistProperties.java +++ b/spring-cloud-alibaba-examples/kubernetes-config-example/src/main/java/com/alibaba/cloud/examples/kubernetes/config/properties/BlacklistProperties.java @@ -28,5 +28,6 @@ @Data @ConfigurationProperties(prefix = "blacklist") public class BlacklistProperties { + private String header; private Set userIds; } diff --git a/spring-cloud-alibaba-examples/kubernetes-config-example/src/main/resources/application.yml b/spring-cloud-alibaba-examples/kubernetes-config-example/src/main/resources/application.yml index d54782ecae..884bdccc6b 100644 --- a/spring-cloud-alibaba-examples/kubernetes-config-example/src/main/resources/application.yml +++ b/spring-cloud-alibaba-examples/kubernetes-config-example/src/main/resources/application.yml @@ -1,5 +1,7 @@ server: port: 8080 + error: + include-message: always spring: application: name: kubernetes-config-example @@ -16,8 +18,4 @@ spring: namespace: default refreshable: false refresh-on-delete: false - fail-on-missing-config: false -blacklist: - user-ids: - - 1 - - 2 + fail-on-missing-config: true diff --git a/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-alibaba-kubernetes-commons/src/main/java/com/alibaba/cloud/kubernetes/commons/KubernetesUtils.java b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-alibaba-kubernetes-commons/src/main/java/com/alibaba/cloud/kubernetes/commons/KubernetesUtils.java index a14a9265d3..d2d2d6dc53 100644 --- a/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-alibaba-kubernetes-commons/src/main/java/com/alibaba/cloud/kubernetes/commons/KubernetesUtils.java +++ b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-alibaba-kubernetes-commons/src/main/java/com/alibaba/cloud/kubernetes/commons/KubernetesUtils.java @@ -36,8 +36,7 @@ private KubernetesUtils() { * Get the kube config. * *

- * NOTE: {@link Config} needs to be a singleton, do NOT modify - * it. + * NOTE: {@link Config} needs to be a singleton, do NOT modify it. * * @return Config */ diff --git a/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/KubernetesConfigProperties.java b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/KubernetesConfigProperties.java index b097d7f201..3568023602 100644 --- a/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/KubernetesConfigProperties.java +++ b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/KubernetesConfigProperties.java @@ -43,7 +43,7 @@ public class KubernetesConfigProperties implements InitializingBean { private boolean enabled = true; /** - * Default namespace for configmaps and secrets. + * Default namespace for ConfigMaps and Secrets. *

* If in Kubernetes environment, use the namespace of the current pod. *

diff --git a/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/core/ConfigEnvironmentPostProcessor.java b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/core/ConfigEnvironmentPostProcessor.java index 9db839939a..53fbf86c59 100644 --- a/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/core/ConfigEnvironmentPostProcessor.java +++ b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/core/ConfigEnvironmentPostProcessor.java @@ -97,7 +97,8 @@ else if (resource instanceof Secret) { .addLast(toPropertySource((Secret) resource)); } else { - log.warn("Refreshed a unknown resource type: " + resource.getClass()); + log.warn("[Kubernetes Config] Refreshed a unknown resource type: " + + resource.getClass()); } } else { @@ -125,7 +126,8 @@ private void pullConfigMaps(KubernetesConfigProperties properties, ConfigurableEnvironment environment) { properties.getConfigMaps().stream() .map(configmap -> Optional - .ofNullable(propertySourceForConfigMap(configmap, properties)) + .ofNullable(propertySourceForConfigMap(configmap, + properties.isFailOnMissingConfig())) .map(ps -> Pair.of(configmap.getPreference(), ps)).orElse(null)) .filter(Objects::nonNull) .collect(groupingBy(Pair::key, mapping(Pair::value, toList()))) @@ -139,7 +141,8 @@ private void pullSecrets(KubernetesConfigProperties properties, ConfigurableEnvironment environment) { properties.getSecrets().stream() .map(secret -> Optional - .ofNullable(propertySourceForSecret(secret, properties)) + .ofNullable(propertySourceForSecret(secret, + properties.isFailOnMissingConfig())) .map(ps -> Pair.of(secret.getPreference(), ps)).orElse(null)) .filter(Objects::nonNull) .collect(groupingBy(Pair::key, mapping(Pair::value, toList()))) @@ -171,61 +174,53 @@ private static void addPropertySourcesToEnvironment( } private EnumerablePropertySource propertySourceForConfigMap( - KubernetesConfigProperties.ConfigMap cm, - KubernetesConfigProperties properties) { + KubernetesConfigProperties.ConfigMap cm, boolean isFailOnMissingConfig) { ConfigMap configMap; try { configMap = client.configMaps().inNamespace(cm.getNamespace()) .withName(cm.getName()).get(); } catch (KubernetesClientException e) { - if (!isRefreshing()) { - throw kubernetesConfigException(ConfigMap.class, cm.getName(), - cm.getNamespace(), e); - } - log.warn( - "Kubernetes client exception while refreshing ConfigMap, so properties value won't change", - e); + processException(ConfigMap.class, cm.getName(), cm.getNamespace(), e); return null; } if (configMap == null) { - log.warn(String.format("ConfigMap '%s' not found in namespace '%s'", - cm.getName(), cm.getNamespace())); failApplicationStartUpIfNecessary(ConfigMap.class, cm.getName(), - cm.getNamespace(), properties); + cm.getNamespace(), isFailOnMissingConfig); return null; } return toPropertySource(configMap); } private EnumerablePropertySource propertySourceForSecret( - KubernetesConfigProperties.Secret secret, - KubernetesConfigProperties properties) { + KubernetesConfigProperties.Secret secret, boolean isFailOnMissingConfig) { Secret secretInK8s; try { secretInK8s = client.secrets().inNamespace(secret.getNamespace()) .withName(secret.getName()).get(); } catch (KubernetesClientException e) { - if (!isRefreshing()) { - throw kubernetesConfigException(Secret.class, secret.getName(), - secret.getNamespace(), e); - } - log.warn( - "Kubernetes client exception while refreshing Secret, so properties value won't change", - e); + processException(Secret.class, secret.getName(), secret.getNamespace(), e); return null; } if (secretInK8s == null) { - log.warn(String.format("Secret '%s' not found in namespace '%s'", - secret.getName(), secret.getNamespace())); failApplicationStartUpIfNecessary(Secret.class, secret.getName(), - secret.getNamespace(), properties); + secret.getNamespace(), isFailOnMissingConfig); return null; } return toPropertySource(secretInK8s); } + private void processException(Class type, String name, String namespace, + KubernetesClientException e) { + if (!isRefreshing()) { + throw kubernetesConfigException(type, name, namespace, e); + } + log.warn(String.format( + "[Kubernetes Config] Kubernetes client exception while refreshing %s, so properties value won't change", + type.getSimpleName()), e); + } + private static AbstractKubernetesConfigException kubernetesConfigException( Class type, String name, String namespace, KubernetesClientException e) { // Usually the Service Account or user does not have enough privileges. @@ -245,11 +240,13 @@ private static AbstractKubernetesConfigException kubernetesConfigException( * @param type the type of the resource * @param name the name of the resource * @param namespace the namespace of the resource - * @param properties {@link KubernetesConfigProperties} + * @param isFailOnMissingConfig whether to fail the application start up */ - private static void failApplicationStartUpIfNecessary(Class type, String name, - String namespace, KubernetesConfigProperties properties) { - if (!isRefreshing() && properties.isFailOnMissingConfig()) { + private void failApplicationStartUpIfNecessary(Class type, String name, + String namespace, boolean isFailOnMissingConfig) { + log.warn(String.format("[Kubernetes Config] %s '%s' not found in namespace '%s'", + type.getSimpleName(), name, namespace)); + if (!isRefreshing() && isFailOnMissingConfig) { throw new KubernetesConfigMissingException(type, name, namespace, null); } } diff --git a/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/core/ConfigWatcher.java b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/core/ConfigWatcher.java index 37f24617c4..b9b8c9e9d0 100644 --- a/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/core/ConfigWatcher.java +++ b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/core/ConfigWatcher.java @@ -75,7 +75,9 @@ public void afterSingletonsInstantiated() { public void destroy() { configmapInformers.values().forEach(SharedIndexInformer::close); secretInformers.values().forEach(SharedIndexInformer::close); - log.info("ConfigMap and Secret informers closed"); + if (log.isInfoEnabled()) { + log.info("[Kubernetes Config] ConfigMap and Secret informers closed"); + } } private void watchRefreshableResources(KubernetesConfigProperties properties, @@ -109,7 +111,7 @@ private static void log( .join(".", resourceKey.name(), resourceKey.namespace())) .collect(Collectors.toList()); if (!names.isEmpty() && log.isInfoEnabled()) { - log.info("Start watching {}s: {}", + log.info("[Kubernetes Config] Start watching {}s: {}", informers.keySet().iterator().next().type(), names); } } diff --git a/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/core/HasMetadataResourceEventHandler.java b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/core/HasMetadataResourceEventHandler.java index 2fa55a8de2..fd326c01ff 100644 --- a/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/core/HasMetadataResourceEventHandler.java +++ b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/core/HasMetadataResourceEventHandler.java @@ -48,8 +48,9 @@ class HasMetadataResourceEventHandler @Override public void onAdd(HasMetadata obj) { if (log.isDebugEnabled()) { - log.debug("{} '{}' added in namespace '{}'", obj.getKind(), - obj.getMetadata().getName(), obj.getMetadata().getNamespace()); + log.debug("[Kubernetes Config] {} '{}' added in namespace '{}'", + obj.getKind(), obj.getMetadata().getName(), + obj.getMetadata().getNamespace()); } // When application start up, the informer will trigger an onAdd event, but at // this phase application is not @@ -62,8 +63,9 @@ public void onAdd(HasMetadata obj) { @Override public void onUpdate(HasMetadata oldObj, HasMetadata newObj) { if (log.isDebugEnabled()) { - log.debug("{} '{}' updated in namespace '{}'", newObj.getKind(), - newObj.getMetadata().getName(), newObj.getMetadata().getNamespace()); + log.debug("[Kubernetes Config] {} '{}' updated in namespace '{}'", + newObj.getKind(), newObj.getMetadata().getName(), + newObj.getMetadata().getNamespace()); } refresh(newObj); } @@ -71,18 +73,21 @@ public void onUpdate(HasMetadata oldObj, HasMetadata newObj) { @Override public void onDelete(HasMetadata obj, boolean deletedFinalStateUnknown) { if (log.isDebugEnabled()) { - log.debug("{} '{}' deleted in namespace '{}'", obj.getKind(), - obj.getMetadata().getName(), obj.getMetadata().getNamespace()); + log.debug("[Kubernetes Config] {} '{}' deleted in namespace '{}'", + obj.getKind(), obj.getMetadata().getName(), + obj.getMetadata().getNamespace()); } if (properties.isRefreshOnDelete()) { deletePropertySourceOfResource(obj); refresh(obj); } else { - log.info( - "{} '{}' was deleted in namespace '{}', refresh on delete is disabled, ignore the delete event", - obj.getKind(), obj.getMetadata().getName(), - obj.getMetadata().getNamespace()); + if (log.isInfoEnabled()) { + log.info( + "[Kubernetes Config] {} '{}' was deleted in namespace '{}', refresh on delete is disabled, ignore the delete event", + obj.getKind(), obj.getMetadata().getName(), + obj.getMetadata().getNamespace()); + } } } From 0f06277c06bc31bfe9ac1be805426863e46bc61d Mon Sep 17 00:00:00 2001 From: Freeman Lau Date: Sat, 4 Feb 2023 11:54:56 +0800 Subject: [PATCH 23/37] fix docs --- .../src/main/asciidoc/kubernetes-config.adoc | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/spring-cloud-alibaba-docs/src/main/asciidoc/kubernetes-config.adoc b/spring-cloud-alibaba-docs/src/main/asciidoc/kubernetes-config.adoc index 03677375ec..0467b34ca8 100644 --- a/spring-cloud-alibaba-docs/src/main/asciidoc/kubernetes-config.adoc +++ b/spring-cloud-alibaba-docs/src/main/asciidoc/kubernetes-config.adoc @@ -4,7 +4,7 @@ The purpose of this module is to use Kubernetes ConfigMap/Secret as a distribute === Quick Start -Go to the [example](./example) and follow the docs to run the example. +Go to the link:../../../../spring-cloud-alibaba-examples/kubernetes-config-example/README.md[example] and follow the docs to run the example. You got this! === Main Features @@ -62,6 +62,7 @@ NOTE: Secret only supports key-value pair! Example: ConfigMap: + [source,yaml] ---- apiVersion: v1 @@ -77,6 +78,7 @@ data: ---- Secret: + [source,yaml] ---- apiVersion: v1 @@ -85,7 +87,7 @@ metadata: name: my-secret stringData: blacklist.user-ids[0]: 1 - blacklist.user-ids[1]: 1 + blacklist.user-ids[1]: 2 ---- The two configurations above convey the same meaning. From 38bbada62158641526f07d5ae46a7f8cb6ef5f8a Mon Sep 17 00:00:00 2001 From: Freeman Lau Date: Sat, 4 Feb 2023 12:23:32 +0800 Subject: [PATCH 24/37] add zh docs --- .../main/asciidoc-zh/kubernetes-config.adoc | 145 ++++++++++++++++++ .../src/main/asciidoc/kubernetes-config.adoc | 2 + .../kubernetes-config-example/README-zh.md | 62 ++++++++ .../kubernetes-config-example/README.md | 2 + 4 files changed, 211 insertions(+) create mode 100644 spring-cloud-alibaba-docs/src/main/asciidoc-zh/kubernetes-config.adoc create mode 100644 spring-cloud-alibaba-examples/kubernetes-config-example/README-zh.md diff --git a/spring-cloud-alibaba-docs/src/main/asciidoc-zh/kubernetes-config.adoc b/spring-cloud-alibaba-docs/src/main/asciidoc-zh/kubernetes-config.adoc new file mode 100644 index 0000000000..6be15b8e7b --- /dev/null +++ b/spring-cloud-alibaba-docs/src/main/asciidoc-zh/kubernetes-config.adoc @@ -0,0 +1,145 @@ +== Spring Cloud Alibaba Kubernetes Config + +link:kubernetes-config.adoc[English] | link:../asciidoc-zh/kubernetes-config.adoc[中文] + +该模块的目的是利用 Kubernetes ConfigMap/Secret 作为分布式配置中心,实现无需重启应用的动态配置更新。 + +=== 快速开始 + +参考 link:../../../../spring-cloud-alibaba-examples/kubernetes-config-example/README.md[example],按照文档运行一下 demo,你可以的! + +=== 主要特性 + +如果你有使用 `spring-cloud-starter-alibaba-nacos-config` 的经验,你会发现其用法非常相似。`spring-cloud-starter-alibaba-kubernetes-config` 提供了 `spring-cloud-starter-alibaba-nacos-config` 的所有核心功能,并提供了更容易使用的配置,更好的性能和云原生程序契合度。 + +==== 配置动态刷新(ConfigMap/Secret) + +当配置更新时,不需要重新启动应用程序,应用程序可以实时地动态更新配置。 + +NOTE: Spring Cloud 提供了在运行时动态刷新环境的能力,这主要是动态更新两类 Bean 的属性:`@ConfigurationProperties` 和 `@RefreshScope` Bean。 + +*最佳实践:使用 `@ConfigurationProperties` 来组织你的配置!* + +相关配置: + +[source,yaml] +---- +spring: + cloud: + k8s: + config: + refreshable: false # 全局默认配置,默认值为 false + config-maps: + - name: my-configmap + refreshable: true # my-configmap 将检测配置更改并刷新 + secrets: + - name: my-secret # my-secret 不会检测配置更改和刷新 +---- + +==== 配置优先级 + +如果本地的 `application.yml` 和 ConfigMap/Secret 中有相同的配置,可以选择优先使用哪个配置。 + +相关配置: + +[source,yaml] +---- +spring: + cloud: + k8s: + config: + preference: remote # 全局默认配置,默认值为remote,表示远程配置会覆盖本地配置 + config-maps: + - name: my-configmap + preference: local +---- + +==== 多种配置文件格式 + +支持 `yaml`、`properties`、`json` 和键值对配置。 + +NOTE: Secret 只支持键值对! + +Example: + +ConfigMap: + +[source,yaml] +---- +apiVersion: v1 +kind: ConfigMap +metadata: + name: my-configmap +data: + blacklist.yml: | + blacklist: + user-ids: + - 1 + - 2 +---- + +Secret: + +[source,yaml] +---- +apiVersion: v1 +kind: Secret +metadata: + name: my-secret +stringData: + blacklist.user-ids[0]: 1 + blacklist.user-ids[1]: 2 +---- + +上面的两个配置是等价的。 + +==== 异常操作保护 + +如果配置被错误地删除,你不希望你的应用程序崩溃,对吗?`spring-cloud-starter-alibaba-kubernetes-config` 提供了一个机制来保护应用程序免受异常操作的影响,比如误删配置。 + +相关配置: + +[source,yaml] +---- +spring: + cloud: + k8s: + config: + refresh-on-deleted: false # 当检测到配置被删除时,应用程序不会触发刷新。默认值为 false。 +---- + +如果你的配置没有跨环境同步,你在 `dev` 环境中有 `import-config` 配置,但在 `prod` 环境中没有(有可能是你忘记同步到 `prod`),你可能希望有一个机制来帮助你发现这个问题,并提供快速失败的能力来阻止应用程序启动。 + +相关配置: + +[source,yaml] +---- +spring: + cloud: + k8s: + config: + fail-on-missing-config: true # 如果找不到配置,应用程序将无法启动。默认值为 true。 +---- + +=== 核心配置 + +[source,yaml] +---- +spring: + cloud: + k8s: + config: + enabled: true + namespace: default # 配置所在的命名空间(全局配置)。如果在 Kubernetes 集群内部,则默认为当前 pod 所在的命名空间;如果在 Kubernetes 集群之外,则默认为当前 context 的命名空间。 + preference: remote + refreshable: false + refresh-on-delete: false + fail-on-missing-config: true + config-maps: + - name: my-configmap + preference: remote + refreshable: true + secrets: + - name: my-secret + namespace: secret-namespace +---- diff --git a/spring-cloud-alibaba-docs/src/main/asciidoc/kubernetes-config.adoc b/spring-cloud-alibaba-docs/src/main/asciidoc/kubernetes-config.adoc index 0467b34ca8..6af807e8f9 100644 --- a/spring-cloud-alibaba-docs/src/main/asciidoc/kubernetes-config.adoc +++ b/spring-cloud-alibaba-docs/src/main/asciidoc/kubernetes-config.adoc @@ -1,5 +1,7 @@ == Spring Cloud Alibaba Kubernetes Config +link:kubernetes-config.adoc[English] | link:../asciidoc-zh/kubernetes-config.adoc[中文] + The purpose of this module is to use Kubernetes ConfigMap/Secret as a distributed configuration center to achieve dynamic configuration updates without restarting the application. === Quick Start diff --git a/spring-cloud-alibaba-examples/kubernetes-config-example/README-zh.md b/spring-cloud-alibaba-examples/kubernetes-config-example/README-zh.md new file mode 100644 index 0000000000..352920feec --- /dev/null +++ b/spring-cloud-alibaba-examples/kubernetes-config-example/README-zh.md @@ -0,0 +1,62 @@ +## Spring Cloud Starter Alibaba Kubernetes Config Example + +[English](README.md) | [中文](README-zh.md) + +这个例子演示了使用 `spring-cloud-starter-alibaba-kubernetes-config` 来实现一个动态黑名单功能。 + +### 步骤 + +1. Apply [deployments.yaml](./deployments.yaml) 文件 + + 确保在 `kubernetes-config-example` 目录。 + + ```shell + kubectl apply -f deployments.yaml + ``` + +2. 启动程序 + +3. 访问应用 + + ```shell + curl localhost:8080/echo -H 'x-user-id:1' + ``` + + 可以看到用户 `1` 已经被 block 了,可以查看 [deployments.yaml](./deployments.yaml) 得知 blocked 的用户 id。 + +4. 修改 [deployments.yaml](./deployments.yaml) 文件中的 `blacklist` 配置。 + + ```yaml + apiVersion: v1 + kind: ConfigMap + metadata: + name: configmap-01 + namespace: default + data: + blacklist.yml: | + blacklist: + user-ids: + - 2 + ``` + + 再次 apply + + ```shell + kubectl apply -f deployments.yaml + ``` + +5. 再次访问应用 + + ```shell + curl localhost:8080/echo -H 'x-user-id:1' + ``` + + 可以看到用户 `1` 不再被 block,现在只有用户 `2` 被 block。 + +6. 删除资源 + + 确保在 `kubernetes-config-example` 目录。 + + ```shell + kubectl delete -f deployments.yaml + ``` \ No newline at end of file diff --git a/spring-cloud-alibaba-examples/kubernetes-config-example/README.md b/spring-cloud-alibaba-examples/kubernetes-config-example/README.md index 02c2bb6d50..ad33668889 100644 --- a/spring-cloud-alibaba-examples/kubernetes-config-example/README.md +++ b/spring-cloud-alibaba-examples/kubernetes-config-example/README.md @@ -1,5 +1,7 @@ ## Spring Cloud Starter Alibaba Kubernetes Config Example +[English](README.md) | [中文](README-zh.md) + This example demonstrates the use of `spring-cloud-starter-alibaba-kubernetes-config` to implement a dynamic blacklist. ### Procedure From 4ac940da2260c7c2bd52bd4176969894d008da82 Mon Sep 17 00:00:00 2001 From: Freeman Lau Date: Sat, 4 Feb 2023 12:27:24 +0800 Subject: [PATCH 25/37] fix docs --- .../src/main/asciidoc-zh/kubernetes-config.adoc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spring-cloud-alibaba-docs/src/main/asciidoc-zh/kubernetes-config.adoc b/spring-cloud-alibaba-docs/src/main/asciidoc-zh/kubernetes-config.adoc index 6be15b8e7b..23784015d2 100644 --- a/spring-cloud-alibaba-docs/src/main/asciidoc-zh/kubernetes-config.adoc +++ b/spring-cloud-alibaba-docs/src/main/asciidoc-zh/kubernetes-config.adoc @@ -1,12 +1,12 @@ == Spring Cloud Alibaba Kubernetes Config -link:kubernetes-config.adoc[English] | link:../asciidoc-zh/kubernetes-config.adoc[中文] +link:../asciidoc/kubernetes-config.adoc[English] | link:kubernetes-config.adoc[中文] 该模块的目的是利用 Kubernetes ConfigMap/Secret 作为分布式配置中心,实现无需重启应用的动态配置更新。 === 快速开始 -参考 link:../../../../spring-cloud-alibaba-examples/kubernetes-config-example/README.md[example],按照文档运行一下 demo,你可以的! +参考 link:../../../../spring-cloud-alibaba-examples/kubernetes-config-example/README-zh.md[example],按照文档运行一下 demo,你可以的! === 主要特性 From 565d54525421c42cc8c3056403d8e044c7ea6558 Mon Sep 17 00:00:00 2001 From: Freeman Lau Date: Sat, 4 Feb 2023 12:31:40 +0800 Subject: [PATCH 26/37] revert rocketmq package --- .../{kubernetes/config => rocketmq}/RocketMQBusApplication.java | 2 +- .../cloud/examples/{kubernetes/config => rocketmq}/User.java | 2 +- .../config => rocketmq}/UserRemoteApplicationEvent.java | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) rename spring-cloud-alibaba-examples/spring-cloud-bus-rocketmq-example/src/main/java/com/alibaba/cloud/examples/{kubernetes/config => rocketmq}/RocketMQBusApplication.java (98%) rename spring-cloud-alibaba-examples/spring-cloud-bus-rocketmq-example/src/main/java/com/alibaba/cloud/examples/{kubernetes/config => rocketmq}/User.java (95%) rename spring-cloud-alibaba-examples/spring-cloud-bus-rocketmq-example/src/main/java/com/alibaba/cloud/examples/{kubernetes/config => rocketmq}/UserRemoteApplicationEvent.java (96%) diff --git a/spring-cloud-alibaba-examples/spring-cloud-bus-rocketmq-example/src/main/java/com/alibaba/cloud/examples/kubernetes/config/RocketMQBusApplication.java b/spring-cloud-alibaba-examples/spring-cloud-bus-rocketmq-example/src/main/java/com/alibaba/cloud/examples/rocketmq/RocketMQBusApplication.java similarity index 98% rename from spring-cloud-alibaba-examples/spring-cloud-bus-rocketmq-example/src/main/java/com/alibaba/cloud/examples/kubernetes/config/RocketMQBusApplication.java rename to spring-cloud-alibaba-examples/spring-cloud-bus-rocketmq-example/src/main/java/com/alibaba/cloud/examples/rocketmq/RocketMQBusApplication.java index 458390653e..01f9918a5c 100644 --- a/spring-cloud-alibaba-examples/spring-cloud-bus-rocketmq-example/src/main/java/com/alibaba/cloud/examples/kubernetes/config/RocketMQBusApplication.java +++ b/spring-cloud-alibaba-examples/spring-cloud-bus-rocketmq-example/src/main/java/com/alibaba/cloud/examples/rocketmq/RocketMQBusApplication.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.alibaba.cloud.examples.kubernetes.config; +package com.alibaba.cloud.examples.rocketmq; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; diff --git a/spring-cloud-alibaba-examples/spring-cloud-bus-rocketmq-example/src/main/java/com/alibaba/cloud/examples/kubernetes/config/User.java b/spring-cloud-alibaba-examples/spring-cloud-bus-rocketmq-example/src/main/java/com/alibaba/cloud/examples/rocketmq/User.java similarity index 95% rename from spring-cloud-alibaba-examples/spring-cloud-bus-rocketmq-example/src/main/java/com/alibaba/cloud/examples/kubernetes/config/User.java rename to spring-cloud-alibaba-examples/spring-cloud-bus-rocketmq-example/src/main/java/com/alibaba/cloud/examples/rocketmq/User.java index 39d197d94e..df81d87a68 100644 --- a/spring-cloud-alibaba-examples/spring-cloud-bus-rocketmq-example/src/main/java/com/alibaba/cloud/examples/kubernetes/config/User.java +++ b/spring-cloud-alibaba-examples/spring-cloud-bus-rocketmq-example/src/main/java/com/alibaba/cloud/examples/rocketmq/User.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.alibaba.cloud.examples.kubernetes.config; +package com.alibaba.cloud.examples.rocketmq; /** * User Domain. diff --git a/spring-cloud-alibaba-examples/spring-cloud-bus-rocketmq-example/src/main/java/com/alibaba/cloud/examples/kubernetes/config/UserRemoteApplicationEvent.java b/spring-cloud-alibaba-examples/spring-cloud-bus-rocketmq-example/src/main/java/com/alibaba/cloud/examples/rocketmq/UserRemoteApplicationEvent.java similarity index 96% rename from spring-cloud-alibaba-examples/spring-cloud-bus-rocketmq-example/src/main/java/com/alibaba/cloud/examples/kubernetes/config/UserRemoteApplicationEvent.java rename to spring-cloud-alibaba-examples/spring-cloud-bus-rocketmq-example/src/main/java/com/alibaba/cloud/examples/rocketmq/UserRemoteApplicationEvent.java index 1e087e145e..dc4b3c5686 100644 --- a/spring-cloud-alibaba-examples/spring-cloud-bus-rocketmq-example/src/main/java/com/alibaba/cloud/examples/kubernetes/config/UserRemoteApplicationEvent.java +++ b/spring-cloud-alibaba-examples/spring-cloud-bus-rocketmq-example/src/main/java/com/alibaba/cloud/examples/rocketmq/UserRemoteApplicationEvent.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.alibaba.cloud.examples.kubernetes.config; +package com.alibaba.cloud.examples.rocketmq; import org.springframework.cloud.bus.event.RemoteApplicationEvent; From dee3443b795520cad33dc63cedff8827ad6f72a6 Mon Sep 17 00:00:00 2001 From: Freeman Lau Date: Sat, 4 Feb 2023 13:44:02 +0800 Subject: [PATCH 27/37] add comments --- .../kubernetes/commons/KubernetesUtils.java | 7 +++++- .../KubernetesConfigAutoConfiguration.java | 2 ++ .../config/KubernetesConfigProperties.java | 2 ++ .../core/ConfigEnvironmentPostProcessor.java | 23 +++++++++++++++++++ .../kubernetes/config/core/ConfigWatcher.java | 2 +- .../core/HasMetadataResourceEventHandler.java | 3 +++ .../kubernetes/config/util/Converters.java | 2 ++ .../cloud/kubernetes/config/util/Pair.java | 2 ++ .../kubernetes/config/util/Preference.java | 2 ++ .../kubernetes/config/util/Processors.java | 2 ++ .../kubernetes/config/util/ResourceKey.java | 2 ++ .../config/util/ResourceKeyUtils.java | 2 ++ 12 files changed, 49 insertions(+), 2 deletions(-) diff --git a/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-alibaba-kubernetes-commons/src/main/java/com/alibaba/cloud/kubernetes/commons/KubernetesUtils.java b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-alibaba-kubernetes-commons/src/main/java/com/alibaba/cloud/kubernetes/commons/KubernetesUtils.java index d2d2d6dc53..469b07b4f4 100644 --- a/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-alibaba-kubernetes-commons/src/main/java/com/alibaba/cloud/kubernetes/commons/KubernetesUtils.java +++ b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-alibaba-kubernetes-commons/src/main/java/com/alibaba/cloud/kubernetes/commons/KubernetesUtils.java @@ -22,6 +22,11 @@ import io.fabric8.kubernetes.client.KubernetesClient; /** + * Kubernetes utils. + * + *

+ * Usually used to get kube config and create {@link KubernetesClient} instance. + * * @author Freeman */ public final class KubernetesUtils { @@ -59,7 +64,7 @@ public static String currentNamespace() { } /** - * New a KubernetesClient instance. + * Create a KubernetesClient instance. * * @return new KubernetesClient instance */ diff --git a/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/KubernetesConfigAutoConfiguration.java b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/KubernetesConfigAutoConfiguration.java index 34900a2778..1780f29f3c 100644 --- a/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/KubernetesConfigAutoConfiguration.java +++ b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/KubernetesConfigAutoConfiguration.java @@ -30,6 +30,8 @@ import org.springframework.context.annotation.Import; /** + * Spring Cloud Alibaba Kubernetes Config autoconfiguration. + * * @author Freeman */ @Configuration(proxyBeanMethods = false) diff --git a/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/KubernetesConfigProperties.java b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/KubernetesConfigProperties.java index 3568023602..63315cef92 100644 --- a/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/KubernetesConfigProperties.java +++ b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/KubernetesConfigProperties.java @@ -28,6 +28,8 @@ import org.springframework.util.StringUtils; /** + * Spring Cloud Alibaba Kubernetes Config properties. + * * @author Freeman */ @ConfigurationProperties(KubernetesConfigProperties.PREFIX) diff --git a/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/core/ConfigEnvironmentPostProcessor.java b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/core/ConfigEnvironmentPostProcessor.java index 53fbf86c59..9e1d740928 100644 --- a/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/core/ConfigEnvironmentPostProcessor.java +++ b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/core/ConfigEnvironmentPostProcessor.java @@ -45,7 +45,9 @@ import org.springframework.core.Ordered; import org.springframework.core.env.ConfigurableEnvironment; import org.springframework.core.env.EnumerablePropertySource; +import org.springframework.core.env.Environment; import org.springframework.core.env.MutablePropertySources; +import org.springframework.core.env.PropertySource; import org.springframework.core.env.StandardEnvironment; import static com.alibaba.cloud.kubernetes.config.util.Converters.toPropertySource; @@ -54,7 +56,28 @@ import static java.util.stream.Collectors.toList; /** + * Kubernetes config {@link EnvironmentPostProcessor}. + * + *

+ * There are two ways to use this post processor: + *

    + *
  • when application starts up
  • + *
  • when application triggers a {@link RefreshEvent}
  • + *
+ * + *

+ * When application starts up, this processor will load all the ConfigMaps/Secrets. + *

+ * When application triggers a {@link RefreshEvent}, Spring will copy a + * {@link Environment} and invoke all {@link EnvironmentPostProcessor}s, then replace the + * {@link PropertySource} if copied {@link Environment} has the same name + * {@link PropertySource}. So this processor only converts the refreshed resource to + * {@link PropertySource} and add it to the {@link Environment} when refresh event is + * triggered. + * * @author Freeman + * @see org.springframework.cloud.context.refresh.ContextRefresher + * @see org.springframework.cloud.context.refresh.ConfigDataContextRefresher */ public class ConfigEnvironmentPostProcessor implements EnvironmentPostProcessor, Ordered { /** diff --git a/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/core/ConfigWatcher.java b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/core/ConfigWatcher.java index b9b8c9e9d0..eb31639958 100644 --- a/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/core/ConfigWatcher.java +++ b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/core/ConfigWatcher.java @@ -40,7 +40,7 @@ import static com.alibaba.cloud.kubernetes.config.util.ResourceKeyUtils.resourceKey; /** - * Watcher for config resource changes. + * Watcher for config resources change. * * @author Freeman */ diff --git a/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/core/HasMetadataResourceEventHandler.java b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/core/HasMetadataResourceEventHandler.java index fd326c01ff..90a694e619 100644 --- a/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/core/HasMetadataResourceEventHandler.java +++ b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/core/HasMetadataResourceEventHandler.java @@ -29,6 +29,9 @@ import org.springframework.core.env.ConfigurableEnvironment; /** + * {@link HasMetadataResourceEventHandler} process {@link HasMetadata}(ConfigMap/Secret) + * events, trigger {@link RefreshEvent} when necessary. + * * @author Freeman */ class HasMetadataResourceEventHandler diff --git a/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/util/Converters.java b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/util/Converters.java index 3e41694821..675cf1eab8 100644 --- a/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/util/Converters.java +++ b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/util/Converters.java @@ -36,6 +36,8 @@ import static java.nio.charset.StandardCharsets.UTF_8; /** + * {@link Converters} use to convert ConfigMap/Secret to {@link EnumerablePropertySource}. + * * @author Freeman */ public final class Converters { diff --git a/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/util/Pair.java b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/util/Pair.java index fa8921db7e..71d5250643 100644 --- a/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/util/Pair.java +++ b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/util/Pair.java @@ -17,6 +17,8 @@ package com.alibaba.cloud.kubernetes.config.util; /** + * Utility class for holding a pair of values. + * * @author Freeman */ public final class Pair { diff --git a/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/util/Preference.java b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/util/Preference.java index 20cb036eeb..030ca419a0 100644 --- a/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/util/Preference.java +++ b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/util/Preference.java @@ -17,6 +17,8 @@ package com.alibaba.cloud.kubernetes.config.util; /** + * Configuration preference. + * * @author Freeman */ public enum Preference { diff --git a/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/util/Processors.java b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/util/Processors.java index a797c2b657..f0c420e8f9 100644 --- a/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/util/Processors.java +++ b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/util/Processors.java @@ -25,6 +25,8 @@ import com.alibaba.cloud.kubernetes.config.processor.YamlFileProcessor; /** + * {@link Processors} holds all the {@link FileProcessor} instances. + * * @author Freeman */ public final class Processors { diff --git a/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/util/ResourceKey.java b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/util/ResourceKey.java index b291a2b852..3bd7f7def0 100644 --- a/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/util/ResourceKey.java +++ b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/util/ResourceKey.java @@ -19,6 +19,8 @@ import java.util.Objects; /** + * {@link ResourceKey} represents identity of a resource. + * * @author Freeman */ public final class ResourceKey { diff --git a/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/util/ResourceKeyUtils.java b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/util/ResourceKeyUtils.java index 1c7699582f..b126d75a95 100644 --- a/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/util/ResourceKeyUtils.java +++ b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/util/ResourceKeyUtils.java @@ -21,6 +21,8 @@ import io.fabric8.kubernetes.api.model.Secret; /** + * {@link ResourceKeyUtils} used to generate {@link ResourceKey}. + * * @author Freeman */ public final class ResourceKeyUtils { From 837c41d8c76a7f9e9a8f58d59a65fe66c23e680f Mon Sep 17 00:00:00 2001 From: Freeman Lau Date: Wed, 8 Feb 2023 21:08:31 +0800 Subject: [PATCH 28/37] add exmaple docs note --- .../kubernetes-config-example/README-zh.md | 3 +++ .../kubernetes-config-example/README.md | 3 +++ 2 files changed, 6 insertions(+) diff --git a/spring-cloud-alibaba-examples/kubernetes-config-example/README-zh.md b/spring-cloud-alibaba-examples/kubernetes-config-example/README-zh.md index 352920feec..54fb0d79af 100644 --- a/spring-cloud-alibaba-examples/kubernetes-config-example/README-zh.md +++ b/spring-cloud-alibaba-examples/kubernetes-config-example/README-zh.md @@ -6,6 +6,9 @@ ### 步骤 +*NOTE:在阅读之前你需要了解一些基本的 [Kubernetes](https://kubernetes.io/docs/home/) 知识,并准备一个有访问权限(ConfigMap/Secret)的 +Kubernetes 集群。* + 1. Apply [deployments.yaml](./deployments.yaml) 文件 确保在 `kubernetes-config-example` 目录。 diff --git a/spring-cloud-alibaba-examples/kubernetes-config-example/README.md b/spring-cloud-alibaba-examples/kubernetes-config-example/README.md index ad33668889..e11e81a857 100644 --- a/spring-cloud-alibaba-examples/kubernetes-config-example/README.md +++ b/spring-cloud-alibaba-examples/kubernetes-config-example/README.md @@ -6,6 +6,9 @@ This example demonstrates the use of `spring-cloud-starter-alibaba-kubernetes-co ### Procedure +*NOTE: Before reading this you need to know some basic [Kubernetes](https://kubernetes.io/docs/home/) knowledge and +prepare a Kubernetes cluster with access rights (ConfigMap/Secret).* + 1. Apply the [deployments.yaml](./deployments.yaml) file Make sure you are in the `kubernetes-config-example` directory. From 9043a5a10f17bcb2f06c9ab4b7a078f4e429b242 Mon Sep 17 00:00:00 2001 From: Freeman Lau Date: Wed, 8 Feb 2023 21:41:24 +0800 Subject: [PATCH 29/37] add it package for integration tests --- .../ConfigMapIntegrationTests.java} | 17 +++++----- .../kubernetes/config/{ => it}/Empty.java | 2 +- .../MissingConfigIntegrationTests.java | 8 +++-- .../NotRefreshableIntegrationTests.java | 2 +- .../{ => it}/SecretIntegrationTests.java | 24 +++++++------ .../kubernetes/config/it/package-info.java | 34 +++++++++++++++++++ ...n-normal.yml => application-configmap.yml} | 2 +- .../configmap-changed.yaml | 0 .../{normal => configmap}/configmap.yaml | 0 9 files changed, 64 insertions(+), 25 deletions(-) rename spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/test/java/com/alibaba/cloud/kubernetes/config/{NormalIntegrationTests.java => it/ConfigMapIntegrationTests.java} (82%) rename spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/test/java/com/alibaba/cloud/kubernetes/config/{ => it}/Empty.java (94%) rename spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/test/java/com/alibaba/cloud/kubernetes/config/{ => it}/MissingConfigIntegrationTests.java (83%) rename spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/test/java/com/alibaba/cloud/kubernetes/config/{ => it}/NotRefreshableIntegrationTests.java (98%) rename spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/test/java/com/alibaba/cloud/kubernetes/config/{ => it}/SecretIntegrationTests.java (74%) create mode 100644 spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/test/java/com/alibaba/cloud/kubernetes/config/it/package-info.java rename spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/test/resources/{application-normal.yml => application-configmap.yml} (92%) rename spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/test/resources/{normal => configmap}/configmap-changed.yaml (100%) rename spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/test/resources/{normal => configmap}/configmap.yaml (100%) diff --git a/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/test/java/com/alibaba/cloud/kubernetes/config/NormalIntegrationTests.java b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/test/java/com/alibaba/cloud/kubernetes/config/it/ConfigMapIntegrationTests.java similarity index 82% rename from spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/test/java/com/alibaba/cloud/kubernetes/config/NormalIntegrationTests.java rename to spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/test/java/com/alibaba/cloud/kubernetes/config/it/ConfigMapIntegrationTests.java index 8ea4a8fcbb..2243af2f0a 100644 --- a/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/test/java/com/alibaba/cloud/kubernetes/config/NormalIntegrationTests.java +++ b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/test/java/com/alibaba/cloud/kubernetes/config/it/ConfigMapIntegrationTests.java @@ -14,10 +14,9 @@ * limitations under the License. */ -package com.alibaba.cloud.kubernetes.config; +package com.alibaba.cloud.kubernetes.config.it; import com.alibaba.cloud.kubernetes.config.testsupport.KubernetesAvailable; -import com.alibaba.cloud.kubernetes.config.testsupport.KubernetesTestUtil; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; @@ -27,6 +26,8 @@ import org.springframework.core.env.Environment; import org.springframework.test.context.ActiveProfiles; +import static com.alibaba.cloud.kubernetes.config.testsupport.KubernetesTestUtil.createOrReplaceConfigMap; +import static com.alibaba.cloud.kubernetes.config.testsupport.KubernetesTestUtil.deleteConfigMap; import static org.assertj.core.api.Assertions.assertThat; import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.NONE; @@ -35,17 +36,17 @@ */ @KubernetesAvailable @SpringBootTest(classes = Empty.class, webEnvironment = NONE) -@ActiveProfiles("normal") -public class NormalIntegrationTests { +@ActiveProfiles("configmap") +public class ConfigMapIntegrationTests { @BeforeAll static void init() { - KubernetesTestUtil.createOrReplaceConfigMap("normal/configmap.yaml"); + createOrReplaceConfigMap("configmap/configmap.yaml"); } @AfterAll static void recover() { - KubernetesTestUtil.deleteConfigMap("normal/configmap-changed.yaml"); + deleteConfigMap("configmap/configmap-changed.yaml"); } @Autowired @@ -60,7 +61,7 @@ void testNormal() throws InterruptedException { assertThat(env.getProperty("hobbies[2]")).isNull(); // update configmap - KubernetesTestUtil.createOrReplaceConfigMap("normal/configmap-changed.yaml"); + createOrReplaceConfigMap("configmap/configmap-changed.yaml"); // context is refreshing Thread.sleep(1000); @@ -72,7 +73,7 @@ void testNormal() throws InterruptedException { assertThat(env.getProperty("hobbies[2]")).isEqualTo("coding"); // delete configmap, refresh on delete is disabled by default - KubernetesTestUtil.deleteConfigMap("normal/configmap-changed.yaml"); + deleteConfigMap("configmap/configmap-changed.yaml"); // context is refreshing Thread.sleep(1000); diff --git a/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/test/java/com/alibaba/cloud/kubernetes/config/Empty.java b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/test/java/com/alibaba/cloud/kubernetes/config/it/Empty.java similarity index 94% rename from spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/test/java/com/alibaba/cloud/kubernetes/config/Empty.java rename to spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/test/java/com/alibaba/cloud/kubernetes/config/it/Empty.java index 92b0d715f5..5eafa4da6f 100644 --- a/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/test/java/com/alibaba/cloud/kubernetes/config/Empty.java +++ b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/test/java/com/alibaba/cloud/kubernetes/config/it/Empty.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.alibaba.cloud.kubernetes.config; +package com.alibaba.cloud.kubernetes.config.it; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.context.annotation.Configuration; diff --git a/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/test/java/com/alibaba/cloud/kubernetes/config/MissingConfigIntegrationTests.java b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/test/java/com/alibaba/cloud/kubernetes/config/it/MissingConfigIntegrationTests.java similarity index 83% rename from spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/test/java/com/alibaba/cloud/kubernetes/config/MissingConfigIntegrationTests.java rename to spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/test/java/com/alibaba/cloud/kubernetes/config/it/MissingConfigIntegrationTests.java index a1c648e1a5..7f2df86028 100644 --- a/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/test/java/com/alibaba/cloud/kubernetes/config/MissingConfigIntegrationTests.java +++ b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/test/java/com/alibaba/cloud/kubernetes/config/it/MissingConfigIntegrationTests.java @@ -14,8 +14,9 @@ * limitations under the License. */ -package com.alibaba.cloud.kubernetes.config; +package com.alibaba.cloud.kubernetes.config.it; +import com.alibaba.cloud.kubernetes.config.KubernetesConfigProperties; import com.alibaba.cloud.kubernetes.config.exception.KubernetesConfigMissingException; import com.alibaba.cloud.kubernetes.config.testsupport.KubernetesAvailable; import org.junit.jupiter.api.Test; @@ -30,11 +31,12 @@ */ @KubernetesAvailable public class MissingConfigIntegrationTests { + private static final String PROFILE = "missing-config"; @Test void testEnabledFailOnMissingConfig() { assertThatCode(() -> new SpringApplicationBuilder(Empty.class) - .web(WebApplicationType.NONE).profiles("missing-config").run().close()) + .web(WebApplicationType.NONE).profiles(PROFILE).run().close()) .isInstanceOf(KubernetesConfigMissingException.class); } @@ -44,6 +46,6 @@ void testDisabledFailOnMissingConfig() { .web(WebApplicationType.NONE) .properties(KubernetesConfigProperties.PREFIX + ".fail-on-missing-config=false") - .profiles("missing-config").run().close()).doesNotThrowAnyException(); + .profiles(PROFILE).run().close()).doesNotThrowAnyException(); } } diff --git a/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/test/java/com/alibaba/cloud/kubernetes/config/NotRefreshableIntegrationTests.java b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/test/java/com/alibaba/cloud/kubernetes/config/it/NotRefreshableIntegrationTests.java similarity index 98% rename from spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/test/java/com/alibaba/cloud/kubernetes/config/NotRefreshableIntegrationTests.java rename to spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/test/java/com/alibaba/cloud/kubernetes/config/it/NotRefreshableIntegrationTests.java index f00dcf2906..9ad7702925 100644 --- a/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/test/java/com/alibaba/cloud/kubernetes/config/NotRefreshableIntegrationTests.java +++ b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/test/java/com/alibaba/cloud/kubernetes/config/it/NotRefreshableIntegrationTests.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.alibaba.cloud.kubernetes.config; +package com.alibaba.cloud.kubernetes.config.it; import com.alibaba.cloud.kubernetes.config.testsupport.KubernetesAvailable; import org.junit.jupiter.api.AfterAll; diff --git a/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/test/java/com/alibaba/cloud/kubernetes/config/SecretIntegrationTests.java b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/test/java/com/alibaba/cloud/kubernetes/config/it/SecretIntegrationTests.java similarity index 74% rename from spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/test/java/com/alibaba/cloud/kubernetes/config/SecretIntegrationTests.java rename to spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/test/java/com/alibaba/cloud/kubernetes/config/it/SecretIntegrationTests.java index 7fc26579dc..672ae6df19 100644 --- a/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/test/java/com/alibaba/cloud/kubernetes/config/SecretIntegrationTests.java +++ b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/test/java/com/alibaba/cloud/kubernetes/config/it/SecretIntegrationTests.java @@ -14,10 +14,9 @@ * limitations under the License. */ -package com.alibaba.cloud.kubernetes.config; +package com.alibaba.cloud.kubernetes.config.it; import com.alibaba.cloud.kubernetes.config.testsupport.KubernetesAvailable; -import com.alibaba.cloud.kubernetes.config.testsupport.KubernetesTestUtil; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; @@ -27,6 +26,10 @@ import org.springframework.core.env.Environment; import org.springframework.test.context.ActiveProfiles; +import static com.alibaba.cloud.kubernetes.config.testsupport.KubernetesTestUtil.createOrReplaceConfigMap; +import static com.alibaba.cloud.kubernetes.config.testsupport.KubernetesTestUtil.createOrReplaceSecret; +import static com.alibaba.cloud.kubernetes.config.testsupport.KubernetesTestUtil.deleteConfigMap; +import static com.alibaba.cloud.kubernetes.config.testsupport.KubernetesTestUtil.deleteSecret; import static org.assertj.core.api.Assertions.assertThat; import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.NONE; @@ -40,16 +43,16 @@ public class SecretIntegrationTests { @BeforeAll static void init() { - KubernetesTestUtil.createOrReplaceConfigMap("secret/configmap.yaml"); - KubernetesTestUtil.createOrReplaceSecret("secret/secret.yaml"); - KubernetesTestUtil.createOrReplaceSecret("secret/secret-refreshable.yaml"); + createOrReplaceConfigMap("secret/configmap.yaml"); + createOrReplaceSecret("secret/secret.yaml"); + createOrReplaceSecret("secret/secret-refreshable.yaml"); } @AfterAll static void recover() { - KubernetesTestUtil.deleteConfigMap("secret/configmap.yaml"); - KubernetesTestUtil.deleteSecret("secret/secret.yaml"); - KubernetesTestUtil.deleteSecret("secret/secret-refreshable.yaml"); + deleteConfigMap("secret/configmap.yaml"); + deleteSecret("secret/secret.yaml"); + deleteSecret("secret/secret-refreshable.yaml"); } @Autowired @@ -71,9 +74,8 @@ void testSecret() throws InterruptedException { assertThat(env.getProperty("hobbies[1]")).isEqualTo("writing"); assertThat(env.getProperty("hobbies[2]")).isNull(); - KubernetesTestUtil.createOrReplaceSecret("secret/secret-changed.yaml"); - KubernetesTestUtil - .createOrReplaceSecret("secret/secret-refreshable-changed.yaml"); + createOrReplaceSecret("secret/secret-changed.yaml"); + createOrReplaceSecret("secret/secret-refreshable-changed.yaml"); // make sure context is refreshed Thread.sleep(1000); diff --git a/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/test/java/com/alibaba/cloud/kubernetes/config/it/package-info.java b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/test/java/com/alibaba/cloud/kubernetes/config/it/package-info.java new file mode 100644 index 0000000000..2592dbe178 --- /dev/null +++ b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/test/java/com/alibaba/cloud/kubernetes/config/it/package-info.java @@ -0,0 +1,34 @@ +/* + * Copyright 2013-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Package `it` is shorthand for integration testing. + * + *

+ * The tests under this package will detect whether there is a Kubernetes environment, if + * there is, the tests will be executed, and if not, the tests will be skipped. + * + *

+ * How to run integration tests: + *

+ * You must have a Kubernetes cluster, and the current-context in ~/.kube/config has + * access rights to ConfigMap and Secret. Then you can run the following command: + *

+ * ./mvnw clean test -pl com.alibaba.cloud:spring-cloud-starter-alibaba-kubernetes-config + * -am + * + */ +package com.alibaba.cloud.kubernetes.config.it; diff --git a/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/test/resources/application-normal.yml b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/test/resources/application-configmap.yml similarity index 92% rename from spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/test/resources/application-normal.yml rename to spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/test/resources/application-configmap.yml index d03d1cd7d3..0e219b311b 100644 --- a/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/test/resources/application-normal.yml +++ b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/test/resources/application-configmap.yml @@ -7,7 +7,7 @@ spring: - name: my-configmap refreshable: true application: - name: normal + name: configmap logging: level: com.alibaba.kubernetes.config: debug diff --git a/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/test/resources/normal/configmap-changed.yaml b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/test/resources/configmap/configmap-changed.yaml similarity index 100% rename from spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/test/resources/normal/configmap-changed.yaml rename to spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/test/resources/configmap/configmap-changed.yaml diff --git a/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/test/resources/normal/configmap.yaml b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/test/resources/configmap/configmap.yaml similarity index 100% rename from spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/test/resources/normal/configmap.yaml rename to spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/test/resources/configmap/configmap.yaml From 89c9ce278e07953f6a51595a783261ca45b16dae Mon Sep 17 00:00:00 2001 From: Freeman Lau Date: Sat, 18 Feb 2023 12:04:26 +0800 Subject: [PATCH 30/37] rename to KubernetesConfigWatcher --- .../config/KubernetesConfigAutoConfiguration.java | 8 ++++---- ...java => KubernetesConfigEnvironmentPostProcessor.java} | 4 ++-- .../{ConfigWatcher.java => KubernetesConfigWatcher.java} | 6 +++--- .../com/alibaba/cloud/kubernetes/config/util/Pair.java | 8 ++++---- .../src/main/resources/META-INF/spring.factories | 2 +- 5 files changed, 14 insertions(+), 14 deletions(-) rename spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/core/{ConfigEnvironmentPostProcessor.java => KubernetesConfigEnvironmentPostProcessor.java} (98%) rename spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/core/{ConfigWatcher.java => KubernetesConfigWatcher.java} (94%) diff --git a/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/KubernetesConfigAutoConfiguration.java b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/KubernetesConfigAutoConfiguration.java index 1780f29f3c..2daf11a79d 100644 --- a/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/KubernetesConfigAutoConfiguration.java +++ b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/KubernetesConfigAutoConfiguration.java @@ -17,7 +17,7 @@ package com.alibaba.cloud.kubernetes.config; import com.alibaba.cloud.kubernetes.commons.KubernetesClientConfiguration; -import com.alibaba.cloud.kubernetes.config.core.ConfigWatcher; +import com.alibaba.cloud.kubernetes.config.core.KubernetesConfigWatcher; import io.fabric8.kubernetes.api.model.ConfigMap; import io.fabric8.kubernetes.client.KubernetesClient; @@ -43,8 +43,8 @@ public class KubernetesConfigAutoConfiguration { @Bean @ConditionalOnMissingBean - public ConfigWatcher kubernetesConfigWatcher(KubernetesConfigProperties properties, - KubernetesClient kubernetesClient) { - return new ConfigWatcher(properties, kubernetesClient); + public KubernetesConfigWatcher kubernetesConfigWatcher( + KubernetesConfigProperties properties, KubernetesClient kubernetesClient) { + return new KubernetesConfigWatcher(properties, kubernetesClient); } } diff --git a/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/core/ConfigEnvironmentPostProcessor.java b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/core/KubernetesConfigEnvironmentPostProcessor.java similarity index 98% rename from spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/core/ConfigEnvironmentPostProcessor.java rename to spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/core/KubernetesConfigEnvironmentPostProcessor.java index 9e1d740928..226b58b0c1 100644 --- a/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/core/ConfigEnvironmentPostProcessor.java +++ b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/core/KubernetesConfigEnvironmentPostProcessor.java @@ -79,7 +79,7 @@ * @see org.springframework.cloud.context.refresh.ContextRefresher * @see org.springframework.cloud.context.refresh.ConfigDataContextRefresher */ -public class ConfigEnvironmentPostProcessor implements EnvironmentPostProcessor, Ordered { +public class KubernetesConfigEnvironmentPostProcessor implements EnvironmentPostProcessor, Ordered { /** * Order of the post processor. */ @@ -88,7 +88,7 @@ public class ConfigEnvironmentPostProcessor implements EnvironmentPostProcessor, private final Log log; private final KubernetesClient client; - public ConfigEnvironmentPostProcessor(DeferredLogFactory logFactory) { + public KubernetesConfigEnvironmentPostProcessor(DeferredLogFactory logFactory) { this.log = logFactory.getLog(getClass()); this.client = KubernetesClientHolder.getKubernetesClient(); } diff --git a/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/core/ConfigWatcher.java b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/core/KubernetesConfigWatcher.java similarity index 94% rename from spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/core/ConfigWatcher.java rename to spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/core/KubernetesConfigWatcher.java index eb31639958..2798bc89d5 100644 --- a/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/core/ConfigWatcher.java +++ b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/core/KubernetesConfigWatcher.java @@ -44,9 +44,9 @@ * * @author Freeman */ -public class ConfigWatcher +public class KubernetesConfigWatcher implements SmartInitializingSingleton, ApplicationContextAware, DisposableBean { - private static final Logger log = LoggerFactory.getLogger(ConfigWatcher.class); + private static final Logger log = LoggerFactory.getLogger(KubernetesConfigWatcher.class); private final Map> configmapInformers = new LinkedHashMap<>(); private final Map> secretInformers = new LinkedHashMap<>(); @@ -55,7 +55,7 @@ public class ConfigWatcher private ApplicationContext context; - public ConfigWatcher(KubernetesConfigProperties properties, KubernetesClient client) { + public KubernetesConfigWatcher(KubernetesConfigProperties properties, KubernetesClient client) { this.properties = properties; this.client = client; } diff --git a/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/util/Pair.java b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/util/Pair.java index 71d5250643..f7923e7fea 100644 --- a/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/util/Pair.java +++ b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/util/Pair.java @@ -23,11 +23,11 @@ */ public final class Pair { private final K key; - private final V right; + private final V value; - private Pair(K key, V right) { + private Pair(K key, V value) { this.key = key; - this.right = right; + this.value = value; } public K key() { @@ -35,7 +35,7 @@ public K key() { } public V value() { - return right; + return value; } public static Pair of(K key, V value) { diff --git a/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/resources/META-INF/spring.factories b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/resources/META-INF/spring.factories index 3ead7128b5..760ed5026c 100644 --- a/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/resources/META-INF/spring.factories +++ b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/resources/META-INF/spring.factories @@ -2,7 +2,7 @@ org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ com.alibaba.cloud.kubernetes.config.KubernetesConfigAutoConfiguration org.springframework.boot.env.EnvironmentPostProcessor=\ - com.alibaba.cloud.kubernetes.config.core.ConfigEnvironmentPostProcessor + com.alibaba.cloud.kubernetes.config.core.KubernetesConfigEnvironmentPostProcessor org.springframework.boot.diagnostics.FailureAnalyzer=\ com.alibaba.cloud.kubernetes.config.exception.KubernetesConfigMissingFailureAnalyzer,\ From e5e5fbe016f8959c65e658fef65fa999a8064a1b Mon Sep 17 00:00:00 2001 From: Freeman Lau Date: Sat, 18 Feb 2023 12:09:15 +0800 Subject: [PATCH 31/37] fix docs --- .../src/main/asciidoc-zh/kubernetes-config.adoc | 2 +- .../src/main/asciidoc/kubernetes-config.adoc | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/spring-cloud-alibaba-docs/src/main/asciidoc-zh/kubernetes-config.adoc b/spring-cloud-alibaba-docs/src/main/asciidoc-zh/kubernetes-config.adoc index 23784015d2..27b6ed86b4 100644 --- a/spring-cloud-alibaba-docs/src/main/asciidoc-zh/kubernetes-config.adoc +++ b/spring-cloud-alibaba-docs/src/main/asciidoc-zh/kubernetes-config.adoc @@ -108,7 +108,7 @@ spring: refresh-on-deleted: false # 当检测到配置被删除时,应用程序不会触发刷新。默认值为 false。 ---- -如果你的配置没有跨环境同步,你在 `dev` 环境中有 `import-config` 配置,但在 `prod` 环境中没有(有可能是你忘记同步到 `prod`),你可能希望有一个机制来帮助你发现这个问题,并提供快速失败的能力来阻止应用程序启动。 +如果你的配置没有跨环境同步,你在 `dev` 环境中有 `important-config` 配置,但在 `prod` 环境中没有(有可能是你忘记同步到 `prod`),你可能希望有一个机制来帮助你发现这个问题,并提供快速失败的能力来阻止应用程序启动。 相关配置: diff --git a/spring-cloud-alibaba-docs/src/main/asciidoc/kubernetes-config.adoc b/spring-cloud-alibaba-docs/src/main/asciidoc/kubernetes-config.adoc index 6af807e8f9..9f4ebf8814 100644 --- a/spring-cloud-alibaba-docs/src/main/asciidoc/kubernetes-config.adoc +++ b/spring-cloud-alibaba-docs/src/main/asciidoc/kubernetes-config.adoc @@ -109,7 +109,7 @@ spring: refresh-on-deleted: false # when detect the config was deleted, the application will not trigger a refresh. Default value is false. ---- -If your configuration is not synchronized across environments, and you have the `import-config` configuration in the `dev` environment but not in the `prod` environment (it is possible that you forgot to synchronize to `prod`), you may want to have a mechanism to help you find this problem and provide fail-fast ability to prevent the application from starting. +If your configuration is not synchronized across environments, and you have the `important-config` configuration in the `dev` environment but not in the `prod` environment (it is possible that you forgot to synchronize to `prod`), you may want to have a mechanism to help you find this problem and provide fail-fast ability to prevent the application from starting. Related configuration: From a554d7c3ea05556b54bbeaff82b1e4eb0176edd8 Mon Sep 17 00:00:00 2001 From: Freeman Lau Date: Wed, 22 Feb 2023 22:02:01 +0800 Subject: [PATCH 32/37] update docs --- .../src/main/asciidoc-zh/kubernetes-config.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-cloud-alibaba-docs/src/main/asciidoc-zh/kubernetes-config.adoc b/spring-cloud-alibaba-docs/src/main/asciidoc-zh/kubernetes-config.adoc index 27b6ed86b4..1f3b304b4e 100644 --- a/spring-cloud-alibaba-docs/src/main/asciidoc-zh/kubernetes-config.adoc +++ b/spring-cloud-alibaba-docs/src/main/asciidoc-zh/kubernetes-config.adoc @@ -95,7 +95,7 @@ stringData: ==== 异常操作保护 -如果配置被错误地删除,你不希望你的应用程序崩溃,对吗?`spring-cloud-starter-alibaba-kubernetes-config` 提供了一个机制来保护应用程序免受异常操作的影响,比如误删配置。 +如果配置被错误地删除,你应该不希望应用程序触发配置刷新从而导致程序出现预期外的异常,`spring-cloud-starter-alibaba-kubernetes-config` 提供了一个机制来保护应用程序免受异常操作的影响,比如误删配置。 相关配置: From c22d21ed83bf5f32ab2849e50bf8194051b5f72c Mon Sep 17 00:00:00 2001 From: Freeman Lau Date: Wed, 22 Feb 2023 22:51:00 +0800 Subject: [PATCH 33/37] optimize KubernetesClientHolder --- .../commons/KubernetesClientHolder.java | 20 ++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-alibaba-kubernetes-commons/src/main/java/com/alibaba/cloud/kubernetes/commons/KubernetesClientHolder.java b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-alibaba-kubernetes-commons/src/main/java/com/alibaba/cloud/kubernetes/commons/KubernetesClientHolder.java index e30d1770c6..793897dc6b 100644 --- a/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-alibaba-kubernetes-commons/src/main/java/com/alibaba/cloud/kubernetes/commons/KubernetesClientHolder.java +++ b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-alibaba-kubernetes-commons/src/main/java/com/alibaba/cloud/kubernetes/commons/KubernetesClientHolder.java @@ -39,15 +39,21 @@ private KubernetesClientHolder() { private static final AtomicReference kubernetesClient = new AtomicReference<>(); - public static synchronized KubernetesClient getKubernetesClient() { - KubernetesClient client = kubernetesClient.get(); - if (client == null) { - kubernetesClient.set(KubernetesUtils.newKubernetesClient()); - } - return kubernetesClient.get(); + /** + * Get or create a {@link KubernetesClient}. + * + * @return Kubernetes client + */ + public static KubernetesClient getKubernetesClient() { + return kubernetesClient.updateAndGet(cli -> { + return cli != null ? cli : KubernetesUtils.newKubernetesClient(); + }); } - public static synchronized void remove() { + /** + * Remove the {@link KubernetesClient} instance. + */ + public static void remove() { kubernetesClient.set(null); } } From 9e704302d9fa5bb48b9f65d442eb73967d4f5b69 Mon Sep 17 00:00:00 2001 From: Freeman Lau Date: Sun, 5 Mar 2023 09:28:56 +0800 Subject: [PATCH 34/37] change prefix to alibaba-kubernetes --- .../src/main/asciidoc-zh/kubernetes-config.adoc | 10 +++++----- .../src/main/asciidoc/kubernetes-config.adoc | 10 +++++----- .../src/main/resources/application.yml | 2 +- .../config/KubernetesConfigProperties.java | 12 ++++++------ .../src/test/resources/application-configmap.yml | 2 +- .../test/resources/application-missing-config.yml | 2 +- .../test/resources/application-not-refreshable.yml | 2 +- .../src/test/resources/application-secret.yml | 2 +- 8 files changed, 21 insertions(+), 21 deletions(-) diff --git a/spring-cloud-alibaba-docs/src/main/asciidoc-zh/kubernetes-config.adoc b/spring-cloud-alibaba-docs/src/main/asciidoc-zh/kubernetes-config.adoc index 1f3b304b4e..1627365242 100644 --- a/spring-cloud-alibaba-docs/src/main/asciidoc-zh/kubernetes-config.adoc +++ b/spring-cloud-alibaba-docs/src/main/asciidoc-zh/kubernetes-config.adoc @@ -26,7 +26,7 @@ NOTE: Spring Cloud 提供了在运行时动态刷新环境的能力,这主要 ---- spring: cloud: - k8s: + alibaba-kubernetes: config: refreshable: false # 全局默认配置,默认值为 false config-maps: @@ -46,7 +46,7 @@ spring: ---- spring: cloud: - k8s: + alibaba-kubernetes: config: preference: remote # 全局默认配置,默认值为remote,表示远程配置会覆盖本地配置 config-maps: @@ -103,7 +103,7 @@ stringData: ---- spring: cloud: - k8s: + alibaba-kubernetes: config: refresh-on-deleted: false # 当检测到配置被删除时,应用程序不会触发刷新。默认值为 false。 ---- @@ -116,7 +116,7 @@ spring: ---- spring: cloud: - k8s: + alibaba-kubernetes: config: fail-on-missing-config: true # 如果找不到配置,应用程序将无法启动。默认值为 true。 ---- @@ -127,7 +127,7 @@ spring: ---- spring: cloud: - k8s: + alibaba-kubernetes: config: enabled: true namespace: default # 配置所在的命名空间(全局配置)。如果在 Kubernetes 集群内部,则默认为当前 pod 所在的命名空间;如果在 Kubernetes 集群之外,则默认为当前 context 的命名空间。 diff --git a/spring-cloud-alibaba-docs/src/main/asciidoc/kubernetes-config.adoc b/spring-cloud-alibaba-docs/src/main/asciidoc/kubernetes-config.adoc index 9f4ebf8814..c8daa0e741 100644 --- a/spring-cloud-alibaba-docs/src/main/asciidoc/kubernetes-config.adoc +++ b/spring-cloud-alibaba-docs/src/main/asciidoc/kubernetes-config.adoc @@ -27,7 +27,7 @@ Related configuration: ---- spring: cloud: - k8s: + alibaba-kubernetes: config: refreshable: false # global default config, default value is false config-maps: @@ -47,7 +47,7 @@ Related configuration: ---- spring: cloud: - k8s: + alibaba-kubernetes config: preference: remote # Global default config, default value is remote, means remote config will override local config config-maps: @@ -104,7 +104,7 @@ Related configuration: ---- spring: cloud: - k8s: + alibaba-kubernetes config: refresh-on-deleted: false # when detect the config was deleted, the application will not trigger a refresh. Default value is false. ---- @@ -117,7 +117,7 @@ Related configuration: ---- spring: cloud: - k8s: + alibaba-kubernetes config: fail-on-missing-config: true # application will fail to start if the configs not found. Default value is true. ---- @@ -128,7 +128,7 @@ spring: ---- spring: cloud: - k8s: + alibaba-kubernetes config: enabled: true namespace: default # The namespace where the configuration is located (global configuration). If inside the Kubernetes cluster, it defaults to the namespace where the current pod is located; if outside the Kubernetes cluster, it defaults to the namespace of the current context. diff --git a/spring-cloud-alibaba-examples/kubernetes-config-example/src/main/resources/application.yml b/spring-cloud-alibaba-examples/kubernetes-config-example/src/main/resources/application.yml index 884bdccc6b..8dadceea8c 100644 --- a/spring-cloud-alibaba-examples/kubernetes-config-example/src/main/resources/application.yml +++ b/spring-cloud-alibaba-examples/kubernetes-config-example/src/main/resources/application.yml @@ -6,7 +6,7 @@ spring: application: name: kubernetes-config-example cloud: - k8s: + alibaba-kubernetes: config: config-maps: - name: configmap-01 diff --git a/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/KubernetesConfigProperties.java b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/KubernetesConfigProperties.java index 63315cef92..28cb5daccd 100644 --- a/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/KubernetesConfigProperties.java +++ b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/main/java/com/alibaba/cloud/kubernetes/config/KubernetesConfigProperties.java @@ -37,7 +37,7 @@ public class KubernetesConfigProperties implements InitializingBean { /** * Prefix of {@link KubernetesConfigProperties}. */ - public static final String PREFIX = "spring.cloud.k8s.config"; + public static final String PREFIX = "spring.cloud.alibaba-kubernetes.config"; /** * Whether to enable the kubernetes config feature. @@ -235,19 +235,19 @@ public static class ConfigMap { private String name; /** * Namespace, using - * {@code spring.cloud.k8s.config.namespace} if not + * {@code spring.cloud.alibaba-kubernetes.config.namespace} if not * set. */ private String namespace; /** * Whether to enable the auto refresh on current ConfigMap, using - * {@code spring.cloud.k8s.config.refreshable} if not + * {@code spring.cloud.alibaba-kubernetes.config.refreshable} if not * set. */ private Boolean refreshable; /** * Config preference, using - * {@code spring.cloud.k8s.config.preference} if not + * {@code spring.cloud.alibaba-kubernetes.config.preference} if not * set. */ private Preference preference; @@ -319,7 +319,7 @@ public static class Secret { private String name; /** * Namespace, using - * {@code spring.cloud.k8s.config.namespace} if not + * {@code spring.cloud.alibaba-kubernetes.config.namespace} if not * set. */ private String namespace; @@ -334,7 +334,7 @@ public static class Secret { private Boolean refreshable = false; /** * Config preference, using - * {@code spring.cloud.k8s.config.preference} if not + * {@code spring.cloud.alibaba-kubernetes.config.preference} if not * set. */ private Preference preference; diff --git a/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/test/resources/application-configmap.yml b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/test/resources/application-configmap.yml index 0e219b311b..5a82eebeed 100644 --- a/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/test/resources/application-configmap.yml +++ b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/test/resources/application-configmap.yml @@ -1,6 +1,6 @@ spring: cloud: - k8s: + alibaba-kubernetes: config: namespace: default configmaps: diff --git a/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/test/resources/application-missing-config.yml b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/test/resources/application-missing-config.yml index cd02470adf..61f7b44025 100644 --- a/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/test/resources/application-missing-config.yml +++ b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/test/resources/application-missing-config.yml @@ -2,7 +2,7 @@ spring: application: name: missing-config cloud: - k8s: + alibaba-kubernetes: config: namespace: default configmaps: diff --git a/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/test/resources/application-not-refreshable.yml b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/test/resources/application-not-refreshable.yml index 749bf74f1c..1b47931ec1 100644 --- a/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/test/resources/application-not-refreshable.yml +++ b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/test/resources/application-not-refreshable.yml @@ -1,6 +1,6 @@ spring: cloud: - k8s: + alibaba-kubernetes: config: namespace: default configmaps: diff --git a/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/test/resources/application-secret.yml b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/test/resources/application-secret.yml index 6994a84acc..47fdf70bb4 100644 --- a/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/test/resources/application-secret.yml +++ b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/test/resources/application-secret.yml @@ -1,6 +1,6 @@ spring: cloud: - k8s: + alibaba-kubernetes: config: config-maps: - name: secret-configmap-01 From 3ea493ac48c433845fbe39de3e6440788b9f48ad Mon Sep 17 00:00:00 2001 From: Freeman Lau Date: Sun, 5 Mar 2023 09:36:43 +0800 Subject: [PATCH 35/37] optimize getKubernetesClient method --- .../cloud/kubernetes/commons/KubernetesClientHolder.java | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-alibaba-kubernetes-commons/src/main/java/com/alibaba/cloud/kubernetes/commons/KubernetesClientHolder.java b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-alibaba-kubernetes-commons/src/main/java/com/alibaba/cloud/kubernetes/commons/KubernetesClientHolder.java index 793897dc6b..5ad3ecd084 100644 --- a/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-alibaba-kubernetes-commons/src/main/java/com/alibaba/cloud/kubernetes/commons/KubernetesClientHolder.java +++ b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-alibaba-kubernetes-commons/src/main/java/com/alibaba/cloud/kubernetes/commons/KubernetesClientHolder.java @@ -45,9 +45,10 @@ private KubernetesClientHolder() { * @return Kubernetes client */ public static KubernetesClient getKubernetesClient() { - return kubernetesClient.updateAndGet(cli -> { - return cli != null ? cli : KubernetesUtils.newKubernetesClient(); - }); + if (kubernetesClient.get() == null) { + kubernetesClient.compareAndSet(null, KubernetesUtils.newKubernetesClient()); + } + return kubernetesClient.get(); } /** From daa62f6509fb84af470fe8da4b39534246695e9c Mon Sep 17 00:00:00 2001 From: Freeman Lau Date: Sun, 5 Mar 2023 10:59:48 +0800 Subject: [PATCH 36/37] ci oom, ignore integration tests --- .github/workflows/integration-test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/integration-test.yml b/.github/workflows/integration-test.yml index 225a7c6e1e..7139b3ea18 100644 --- a/.github/workflows/integration-test.yml +++ b/.github/workflows/integration-test.yml @@ -42,6 +42,6 @@ jobs: - name: install dependencies run: mvn clean install -U package -pl '!spring-cloud-alibaba-coverage' -DskipTests - name: Testing - run: ./mvnw verify -B -Dmaven.test.skip=false + run: ./mvnw verify -B -Dmaven.test.skip=false -pl '!*test' # run: mvn clean -Dit.enabled=true test From 1059ca1dec0e292e39c09509e3586d01e299eb0e Mon Sep 17 00:00:00 2001 From: Freeman Lau Date: Sun, 5 Mar 2023 11:39:20 +0800 Subject: [PATCH 37/37] update maven 3.9.0 --- .github/workflows/integration-test.yml | 2 +- .mvn/wrapper/maven-wrapper.jar | Bin 48934 -> 59925 bytes .mvn/wrapper/maven-wrapper.properties | 19 ++++++++++++++++++- 3 files changed, 19 insertions(+), 2 deletions(-) mode change 100755 => 100644 .mvn/wrapper/maven-wrapper.jar diff --git a/.github/workflows/integration-test.yml b/.github/workflows/integration-test.yml index 7139b3ea18..225a7c6e1e 100644 --- a/.github/workflows/integration-test.yml +++ b/.github/workflows/integration-test.yml @@ -42,6 +42,6 @@ jobs: - name: install dependencies run: mvn clean install -U package -pl '!spring-cloud-alibaba-coverage' -DskipTests - name: Testing - run: ./mvnw verify -B -Dmaven.test.skip=false -pl '!*test' + run: ./mvnw verify -B -Dmaven.test.skip=false # run: mvn clean -Dit.enabled=true test diff --git a/.mvn/wrapper/maven-wrapper.jar b/.mvn/wrapper/maven-wrapper.jar old mode 100755 new mode 100644 index 279ff4cdc0b283ba14a7b6a8dfc2499afeccc85a..bf82ff01c6cdae4a1bb754a6e062954d77ac5c11 GIT binary patch literal 59925 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

dy!05UMhSKsK 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 VCNKd3JGtOvEm(5t0lFyE{{i--k}m)N literal 48934 zcmbTe1C(XUvNqb)W!rX_ZQHhMm2IP|%eHOXwr$&Xbs2x}bI*V8efPby$31V2F~^!? ztQnCRnK3iIjQnEBO96wRe1m|1`1b7^;h*cjK9GNIWki$(Xe4As>43kB9 zUlABpCL(B3smO9{?MbrjI0xJJD%t?}#=lc`t4M1yGQVS3&Au19EX7w?Q_Pf?aLJ6U zM)8Z)KzC{|DsbiNDpEZ)r7znkyNrf=UFuZtUO@h3X8=dAWYHfhsDQrxvHM>Q5d43) z(;DDnZ1Zmo6XyS0+}xJd31A6uF#op{k^h~dfAovS)d65y>dWcvT=$^TU(Xlv`_==5i%1t+Cz-BM#1DOlUJA$}AOq7{$s7~|tR$4MQe?sJK*(()cOb-9x^s3Ko^~^z`>F!Z>HEQkMAad@by=TI7_I6mDT2GPtrI{ z0wA(xVK*@iZw8U;VLt7dru%OI=@H-jV7QptddaB@ahj{jTZv3VjHG2~rpl6Nhq7j` zQJm5RM&GLo6k&k0oG{^rj~TI7C)ty1s0%F6HkD~lMY51y0_kWiJ1$O&08%0J7ppfK z*^vH#>Im73{D)&_;%o&fFj6BO1)88OH>riA+<*@lovP8DCDOg9CS&tS~HZ~i@Afk-s*!u(v`+mrL=-~huQ4FNt z`=!NoA_W6h802<}eU35~?YcEBY00@mP>;IjR>%ts}{$vjc6OMJf~Pqj}^JMmmJw?>I1z?R`Uj zDAKc0_QyV(*xL{>q4tFr$g4e$`f2P5;v^e&1dNji=l%AYVhu+PmMfLbMQuW7j-VP5 zcWVvpT+6T~~2({1_Oww<*BLPH(HOZ6)+e256xS>`rlCbw?`bq4uFN5uD zkR4$IX?k^utEDT3^ZTlpAvwUjHER+R!?*SOwbdGJ6;7QF@mQi8&u3*()UFE8s95@f z)K9AP8Y2PqD@|4~1Z+q=v4)vhJK;`R*?$#m^tA1kIy{l#X}_F`W_Davngdpry`rlV z&Rt)&mTFAFhQ#R_a{0uPTjYgBhLEm8v1YBsa*$RBkR2z*2ggt`ZJ|D-oQmQtLW6Hu z4^_$scfnp@$-%Ek&{fKMTyMJqp3-Z{)@ti@RJM|Cez~lqq*P%< z>F%9R3qjyBMMS438t|H=$X19mvvVJzxLFBiNLBo#8mP+1jyer8okUiYnpP1GQU5}0 z=c7>kP#!XU&S+q=)l5(ecUe1WjC5I|+Tra&Nqk&H8yUFgt`0|D6SyvRaTTO#CnwwG zqHJhYO$;=h2rImmsuFA1yiZ5LV;7z-gx@>P$K!c@NHi;uA1{H0sUUiY;lzcQXFI`a zug&aoum)=6Dj_X2B(Gc(@#c$k38%jxa1XJ2?3tl_l;#ywTlxae)7Z8{l~c1fGlBh3 zfvMhGRrtb+68lieh``+uOsMV-{p7t;b4u*ltqCRGND+arJ;Ag zx_T7>mM%+a6_CzQucSPu6`AQ$u-x8ER zGZRh369U$V2Bw{pY>B{~lgAd#f!+}biUQ$;Kzg!puTVfiy+^BH_uZ62F%)9XObwW< zlG3wb@n|cj74BAUaQ~g{6|7VSy2c-I9Q52s3^-cu)!VR}01NgZLK=a5Jhr*a?U)=S z=%@FNAy4LKcJ})R#8Z82^chISZoG=u?J+0ln;DgU3O{*lPCstJCuBs+jX~A7x$~Pj z+8?5p*?r5j9kwHDpxej>v_E_$21xU-oB0d;(SBZ!?Fah+{e>TTo8k2>K)-#kc z|GCsx#_QN337`aR12;%DuP7?l{A_$2*HK=I#zu~S%w=E@u&#(>?o8{0uuQ^a8@M*gHf9M=KS z3Dwf21FF?MdLtNmy(dm^?M_2*MDhcRy52larCi)- za-}xhp#ZidH*rEBnT=~;X?b=nwh0GtuxJ# zCk8dzs=OTnxe+)uG3f?BLM9>k;t;H3z?b3&kmNoZ z5bJ4Jq8=%CCXrhe3hh$OuKowf%S>`FSrbfa(ju!8YGDeYocrg`MG>^D%2v4_(xV-E zjm!I$uDzg-=d=)xAg*eU%KqIDm)2w1bCencf=7i2+A743X520}G4kAe@PJDzq6(}m zl8a>FzAhjpmx&+rOUe*TgTw{hDYAXa&_ZkofUSm9gyfKN zhvAmE_N;dIh;=>tV+dEiFLHA711RFw3RK#tL)J){2#qTlQwf-_xBf>U$r{&j8-gC< zmhOM}p>z?QDEJ@1ZubW+{TG({pMFUHZ~5Ur@q}8q56U9yC)w&|s!mW0b&Sz0TEzGC zW7R@owvreGeTLNm9VGR>Qk>Sm>zGmjxR)-<3s@FVuIF`R1ZGXtXG++q`@< zR8V|*g5-Xc6chG|Z@=wpqskP3ag}__0Xb>I9CS6HI$GlP4PFvrwM~hoH%+y^R(2(D z_^2V@cpBDn&|)${dyyQHiwApHF_g0F&g4S8E2lkO$REAVLqWeU>9!`Il8U3sUht%! zm=R7~zVl6mJ8T%j>_c$N#R@&KIPioJ^#dbEFE1}m#59nZ3g24KS`PxgjD61{l_z;z ztBL=H0TuG1_}u{qvVWFFh)O)BQG0668p~207vna5-kRK2d3UpBX$~SQhM^5dz&Sk( zWe({*iX#c@j$Iu9kEm;CRbVXhQCLc`Ip;`BTCDC6!mpE_Xs;+(zMPbd-N(-OU8<2h+X-dPB)Y3Bx@59VlZio2Hg> z+?u8dO@$kp!3Q!bKmj_Te!?FPK7ShiNzXwohWMZ(j6<7XVFKx6Q+J(SQZ1S7hg;Nq z2YCS9juc~83(V=FBBgFmvc^jufx^7#eo$$5z&K-9--MMi4l3Pl`2vIKA#R^mlEGeGquZAppjwEO7g!(Himn6_grO%^*DDYX+E4E-n_{Xu9Ecu^Y} z<`mD?s9z*4;H7@s^O}K-?Sk*XnGBbB{=s#A=ODCv78CH4(vxFTKU#Wg+t#Bln_NX) z*S)X4T&5WVni;D1$UaB(i={19iKX$jzL6p0DITKL%?gOfxmhjH0ud6@Uj?bcHVj#4 z0xCB!rQ01QA<~+X-9lpJHP)y#G&&iL(w8%(cmxUSlL^2oeEm$;Igd@(zj!O{;K4vFopY0ohCQu#2~vMXy0)7w+nW>+U| zzN$YjTdpXdVX`L?I=2K2GnLmxST;8{{HNpE%Ik2sTK(FQj!0ZdVU})#m$S-R5tps? zwVt!mTb|Ic&}On?k7nJGdHQraEvtJM-TC_6erZ0ox-5)!m1TNYRWu|HOdImaJjCtn zFAG&+`b+2L6_!UlpWo;lzv*KJ%egqYOzxkA6-pX)O799apqU!L#XN?P*ClBh(XdAi z>(FV^2LP}tJ#y_*)vrZ1!5N%nm~Ge)TOxrNwdCk=@(Aro$$Hio^9 z_Z$Y$vqDFVGa{eI==VR1IwQj+wILsastA{G*6_R}&S6=Cn|iZgyI#SXx}p`I4KSzM zNjljUa23v)!N=Vg_M(3nd(qVMsXr~v@m1y0R6(uUCf;U-e4!st81e5+IDTY5`lHFmtBjRx?i*K-G_lHw@u-%~4Bz%G&TqVAKIhjB~yR9{PwZ_eB zYM@yfYg8l^!r|Z2dqjRV&F;b95-%N4dRJ7uFFbP6yqvcBXYUk4%v{GmV*<$M&;1{E zWc&az{668=1X;&bV`bShEn(U94jKzg&=$!z`{p!U?JJ4!L*_ zC{R9Gu$7u9Yk0a#-VNzDhNH%Z~_;iy8X?d#w+=>k6iV>uox`Y3MIeK`#xG}g!_5%dW-&! zx(Ac|eC!|4P56NS7k*8{6;WFQ|MqPI?(c8|#oywF{{*i80T@K0df|Hc5rV#Q6XIgr zzJa6DKJKhgA%zMHwp6MoSss%L-1Y zgo&e(VKgO)B3eX&&9E3K*#NWJVh6FQexyjEh&y0`?-+xKwTn*p5Io*o&5peOyYH(+ zJls8@hHnkAW0hrxIlO_h-3-a&%ZG1R5!;&qC|NCFim$Jw;2cB{)i_p0S!mN@nR0k4 z$a?w;wL$lV&gHjYjJE35yThRM``bW>h-{){#$V8h~J zg#_vY8^Z8KKw`wGiKQ1M02o0N5(iNG`wHCu@}2Ftoxl#b=^Av&81TdiSx+E=lei$^ zzd#gjnrF41OKXs6X4aW&tCE_haef+NQ2n|$SrjyQ12(y8f7+otMxczL_1-EPD zw#^Zz-;TPrg&6Yl2CoSE+yPLn(IdbQ8K`#T(xln>#fG4=?Vw8YU~uDDg@EM0VK-gy znEmGPD@$66oZP%zcWG{Lwq9Rr>hjcDPm0>ioLpyx99zGC(2?cF+1w^cY3NnWQZV(T zk8AMyaj2@Q`6I&)>AT(tUWn+0zPSA6Pl-iMMqyR$)*-(s-y`9^vu!D3P?qGbWexGs za#G8_*_wLQnHOkUREW2n40Ky`H?|eRad`MK8EMhL9;JFWPYRGkviBeJi_*wsWROJ- z!TG9Ubw1Uo+h`=f&e@yj5mxX+{jV9T51inAPP2Yema)*7*7r|?l{touGu3|QKEB>O zr?x{TdTu_9cusvT4d(WoKLPCkLv{m-_9bF(Rq1IYu%dtl3DVUbt2ykI!uh6eG#`jm9 z=S2%hBQsi!fU9h!sc1~QJq~Yvv#ZZ>GpELIO=uY(V0D@|`)!k8uFN-EX|IK8sN>E< zW}{%~9hJ$ejJ{DOd4ZsnaiGNXh zrjfPUQHyI_s!)<@P4_6c$HYVuSRjb8lkrxc&5^A$#P7c<9ysC7rD6s0L;wrgaFwBs}{X=glXfen*Pjy!sN;w zgWDXEi3!vdvpxD1SvJIUi`cDit=C+12dcRlxr!3z)!+x%6>&iXFT?Gxpa}?a%{RlT zoH-t&JwvyY_x>mm?wZv&lLZt9tMQBY^L)37p5fB7XC>t99&*T6h4T=mCO1aJ`(68> z0+D(VU>vC7R?A^<;Ik@n8SX-g(vr0fHh@@MRAEmpt=rH*{n^-@Q_(DB*_b04?>?3z zLCQ&vGJX`YNL#*Ge{IU$)2{m&vfah6yJr>T?EdQ7s5r?O_}vrg>EM~3W6=70Rx26N zOuOzxOFUjLg&NQLBMAa!gvOq{vw=MRktQVHl1q@+ixOlQG2q@4{i&UrM0G4P9;AJL zRK<_S!2H}_&Lk_x!eGGTcTpN;cK4*nR zmKEZhS_;M1erBcfU4T6lS!fed>f+xai^)z6jjo!vpLRsKoz8w4TUFn?%=e5t1fR(J z#Aupl8(-*2R@^Cqjo%Y;xGwm=#O~1qx~prdA+dOkh>c!R%a(A-gQW3v&Nd*T%=aCr za;em3T22#3R%pr1-cNLr-gSanKiSYqci2gk9tCSYte$j`-}9+;e;{2t(Y^!JHuah7PzOu)fnMg)Hvrye8sh?14Eq&c z4E7y3oMO6}$>vETRJh3g6mwiWi?Z?w}r|ArCQdSAsyG%mN0}A=g+(tC_vgJRPQ$TbcBz;IHCecA=rgoHXHd z{J`KGOQgiz$JRR9(Dv(>p?0s8x*b2TXhFah0%Nuc6JhA6TFnNp`=21?AiWGd99{bUWs z^_Vh0h9YaJs;?A>RNdK9cP3(#jcF;zgr8OVHo7ia34(j07`05>Z2@$`jo24PBnOWr z19XrDa!1ff&Y~6ieFmn`C!U!GPI1>Z{=PR|Jvo^$FgH9Jvm6pw=o=8jg;xggmHQ2+ zRctFC9V2&)rHQ!HX0$)TE{&UUvlCZiqfYy>bQlEI?y9Z)ywL$@I8$RDasG9@*=u|2dp_4F>&r??M4+k zOO;in;t3;HH8^1-!lsfaEDt!bHa8RB7V~zL_2dscl$IozM7Uhqd3x}MTMk9g73B!l z$gvw{=64D=G-XzQu$@d`z-ov+44U*cqY!z8W}etp+9qJjv`Vb$ER)TJFadEg?7qR# zcj3ffu0uiw^moTJ#)?0hx4@p#8)0J}$?P&O$C$hPJdL{y3odqc4yWi`OM~qKK0^`c z!DLx(5{>^*ExJt}m0dL+vCd~lM@XsDDqd&Iw9z6~<Hfx1gV50QMdSewYFD*pu@@FS0@V7@9SR_VhgZJ?A5kS`>I3H zReVut4#A%%${XmlEE+sl5m&GpX*^7Hh|EEcW-eo?99z+;qb2J7d?`X9w^@(l{&x&Q zczaso!fWYwHQa)%!1i4P3PzJrzpbsEqP?_v0V9TiCpa`M4-zjZu=4!JI$IhWMQl-J zHThv0vkLl3JA#@0WDi8hX4Tksy4$r`ykY7JLRrS}Cw1|+yi`n>M?aOd%CtiotY?2P z5Fj0Rl`_q?oFb{!pnHD%EEi;ud^Wl%h2Tv%m-Eq1K8N>tf4sF~EV^-|%=VDTe=1sl z1882xsnK^fU7=2}U+QKZ>fYZuQt)BRhn2fIV~Tw9ePEdwGXB-B$go$XERVRHYzWtV z>@YoE>`pnOBswU~f29%EhnoIUYevJDRB^Y|6@jTuV@+K}DZ6_%gfvZQIAx!pl2}ml z%I*S_U#qrk89aMI%bGC=E3~4$mD;z2(gdH|Jgasn1u&h)yim<^2p@K)xHvY40;kaT z&`G?;vEm#p#>v3DrW7i~HGf!CeF?G?Gc+%>$2m;g+D}nd@ql}Rh}sp=;ThTOz96Y8 zqvQfFnyb#Qoy|_Xrx+`}E4~rdL{3yW#P6Huk$gAry^Xy#m58P=mq=@PuYZ@1guYHZ z8LBpF96hDC#31(=@80o-&C-?AS=Q*F52*HPZz@}Ggwvx@bPl?4F68Zx_|OjGW(n%} z^_qZ<0(%1hgH-RW&=^IiGKi@%md8$2kWv4|0`ua0GNgIY^ScH2+zvhM7GY^0v0=~N zYQWK}+Z)mP8O~-P+8%l9=(Tud!n{Q2NIzUTUkS^uz9C1dZHA`3MhB^7P$n<`RC$C9 zQO(pos#K9$?3M^R8I1T3iE0ZKB8}@fE|xeA^H&g1+LIQ@EoaHv`*WUfyh4KYX=fZS4yi z+ifuTPH_A1%_A##gAU(c_qT@Pmeo)_Ro&(G>dN9IukWH>Q9ds)#?H>Bcj>}r?sL>A zI}-=VMYpQP#oVgKZOK3Eu)`hE%SzR7AL)_3IW8-b7kIx3jD5UrHfIyw-lN3%JW<94L(rnLML?_qr^TF8|P3r^)c8lAdHjL@G z>XEq0GGNUF@Mac9>kWE86J8a6;ZLYxZA}b5$+XTz+JbK1q1zRVY)U(P)$Z*?-&A{ z^GVXd-rGleK>cki-0;0-JIur}pSA`Y5G@XA3icIEHqAzG3<*&W$K8wEaSxObyXmT9 z4o!^*F1`|b?g)f#)M+jFE8=Ia+4o=A?Cn+KKY^%<+8t$vCVFVrn-eMDBt!He91Ag$r)hf?61wK`;hOE!#%cF;~qwqNKEOwh>}e~sSg4loNfjZNfb$A+KKmAn*9Xcq`Xv}cn6!NW+Y zUB?lg${xGG&vPsL$hy@0v7vd&=$c_dnfUx6H`=6#WWf<>IdZFCbF-@Q?riE4z_G$b z9lNE#EQExM#6@Zk#a_X4mlB4doZ+X(Q ztbssBe=EnZD%U=xna)ajTmgO{yR6aHHZJqKRQ#rK&t~k_Q2r{`^E9m0O6qC!!zs4S znv)w{0Wcyf)V9?xDD>D7zUYKbep)+acz^%r%s1<3u$)&UsLex;)Y@HN!dnvWQs>b7 z=K*ff$i6d$JCUs}R-#@}ozOsceZ&Vsyj_a_V);dTHEN1tCR3qiRzk71QrRrIoKhmv zM7-&N#vOH_9PcE|&0SHdMy6|Q+X;~*th?-1;OEnuDT>cw3eIu!=SHJn;)lk_7w}(M zgB(3f+?_w%paT5A%No%AUDiP6FEWwZe{gpGqH_H6%tamC3;PiD%Lk7`gTr)$JqkHE zL|O~a599|)Z|^NZ6uE9Mh{e?1sw&axz^M^AZbM;u(v`#$)F$kVBrR>{1C!eoBU#Gj|SDpAhy0uaQvHY0;*_Xx=w3MX}fVF7! zv#AUX)}oA0v=`?Y_k&q)h#!)#^Kc{PB>o(c(r4H0zDEyIiG}608Riup*10*R`86&! zo)ta@*T(WZ?at|xIJ~9_J!7L@4w;-)g-=d(x3|8TQ$@MAa!$u$$N)8sL8Mu3%axy& ze&wZ_5_eI$%)F?2@u(N>Whb#i8}uQH-a+s=KLJVvNTrGI-dY=?Z=9;3pxRDukrzDt zpnopprqk|s-?|0~Hdg(7zcQR}U~+@hJQaCZ=^(0dr>foN2vp|@JymOiQq@}A2codF z%717sl^?Q$s&1eOn2;CnNybxFiSmSb&Q2 zg!-Wu8!OpuKYw1X%)C!U4UMVH5JJfK1FKn6kWjRa)%NFRh?BYSpn|qo zOk-%MS1^P+FFUcZ_SIBW{dhU^7k z`*C^|GYoRS_#k&?KH0f8exEEZ4T8p%P@)%!990YZ?(auSO{9Zd#=oHhos@v$x0(yI z(|Jn=g;diSupu$ibZU!8r%j3Kk@oi~Wk-~?7Y%}yLNe(A0c(G z)TiRzCNj+f1|BKwDRayMk)t>i&4)o6g7E983;f!eT~m?YOb$0Cur~|UQgqio3j-Z0 z)~9$Q6D=ifg(iwp2EjGZ!DuEM3Z{$gN)M)(R5tuf8IGN}RAGMB$SYD`N^`o5kD%Q3 z&IWd@^yQZq)hk{s|eM;D(Pgfp58?UwbVaixz-+*uvIA2zXLWQ3WiZWOE%Q~kgO zSM94eAsdoXXH9HEWfd+V$v5T^H+N0e4-Uv@LRjkbmh`5-*a~BtZ>eaVc^2yKqM@G%g7Yfm%C&P z%&*ZbeTbyI>PZKYyiA*#%(FvCTC^%pADQdcqy3AXU5KUul^(v>nkehs?RD4UYTFyaeMb z;Gj)(oe~A(CD`~wn4tK%Qc@lX-BbNek%z*AIxXoqdtKdb-5Z)MsUvIka{Kv|ye7#; zs~l!Uc(ny3Uh&Ed57-*;_`5`*RT4fTdf~M93IzOW!S@ zgZ!%NE7c`M2ib0EjwcAxbY3XcF8l%PqDHSw&6y3>g%0aJG0iUKt2fOW{cPmfIGPpnQh9;G3>~U zXlt(LYLvSY$F6bsWu{U387xAVJx!6P>vhDXP2ACE!QS*U^rQlBMXaqF+Rl3IcRUoL z4GydL@Xz)zA8L6$R`j^gb(D80eMeRV|6-yf7sg16Y3^Jng5`rDA^sRft+wP;*;I$e z@4m>=>Tf;!dnDRd*XH;Z<`);2;)$U-vqt9RZuAaW*yv-T3lPT#`~G?2sDpU}qcK1{ zCh=9IM-QXi_jKNMhN!0zH9m5U3$(q&Yel;Kme>y_5%j{hBf|0A2zJvK=*DGs;fn=+ z7b}b%V@ktJ#XpjR*8*)C@Ue9aaFZpGO9gBPcwsyY6?hObV9t=Y^ar?T z2FvU7$eM3LpivZR?exc4io+z`^{HV4A}|mScfG~R5sS05*Ju!EjI%8{$;(pl{fa%d z??9=!3{ygnhzh8n`)Y3DMO`qfClu3n1Bp5~f@d29vcLyfRjiWvcq$BHeviP&70No> zWA%-_8`{My`-h8H82rk%aQ}9!!-Aqas=tMv8!=s8pQX~`-VA2RQ5QuFe!1Fn0!9v$E44lFkBW@JmzSC{mxsu^W9LO-tDH$;zh*Lb4W-M!uAyoJ73?;Nv; zonw1Jb`biq@%?Jfmf-60g%Wty2C>E6Ehq6xhuFl!HqtU4Q$TOp%XCY|&7|zWzRm?} z=B_B-bB;(eVn2hp}@WAnCFez2H?x#}0kt{cp7*$#2C5_oR-p`VAyOP`SCE<;)zDlEZ zRxx*;d{|OQ)=v_Db@34Xo-MNj4e7<=YwNlh>HxO4Vdjm*$0>!(_xqjES8v-l(qZQQ zE$&U8Tk`@tqaQ#1`Fu5j&PQr5+tfB%6Gkms%IL~pFUjt|KaOpK9Z(_b1f3M-OLeY)_osh?OnJKIRS_f%GzydM199h80 z9Zvwu+%%NJncFg78j7TACmo^oZ&X2<5EdR-^VCMgB7h#`c2zYk$2?Vaq-lFbGPajQ z?JafZy5!8-C6YbFrjZ6$_u-<(J|XsmLYp(sfgGnwODxk+4KN!+CbM>g{lzedv$Ud% zU%m630g@QK2+h1OxtZbazM{4na=Hj?c2k~$6#xqItOV_Fpf)6qaw;79MKw<`I_|zi zG2qzVN^Pb(=SU(?bpVK0;D^C^JnO4qZn@11uqfqK^Nc-ai4?bSg|ki0^~%s^-ea_r-t+@UiDpNa)5^;ueVA28>PyiR^MBzL_PRKo@lr{??jseq*qzA z2(^m5;7-1uieTxMS*q+}F!-{Os~2?-X$7+iC0|sqb!j(L5@EnnM$*jKj(M$Bmbe?w zapf}X5qU6=#=BnK7U=IG6+fWCAiZWmPZ^EuNi9Jp~*qnWAS`Ye8ha659q>^-DK zcgxjI)h58CZ6aBHh@tFc7f)@eQWf^87Hnn0jDwZwO!OQGPqxt@YQUcGHS*^8&|w)Z z*d^D8fnG=4&55|xB~g2W-{ofIXZ8-oa9CmWKIRF|Uw+~^X`0vZrzXP*`0xCL`hV>w z|FN&iRD1EnRzu}6lt`+Gm=~78xfl@8h$UUOFatKvX!)I_K3EsoJirhWRS?P(%B^TB zDhmUuNkC2W2X-dZU8h4;8d+3i3|ivxBf-d8g%7AvZXk|hzVlAV?&9ipRzh+GUT#vP|ee?w`2JCt6Zl{S?f zM|=3DiNakYuY@cY0W+zr0-8g4qjp@<2g+f2zGP;jQS3Ar6&tQcU&m?9oXsTN zf2y_<>NH7l19!;ifq=(dvIlgL+sn|uFbq9Tv6(h|lEyz-*m#j)f_bNt`M@a^R7t?t z#Prk5&DMx2p{f<_$BF!G*d}v(30HrR44ES6@JK3@O-JCBR3h7Hn+Y`J+Ki(fs|wBlA{g(S%I)Mr=JSQJ-zy4^l06PN%0-z6Yn23dlxUNr z%Vs8c-3u-y(FqYvQdfr7te6c)n|XUfM#M(vKAxljW!tO}h$^iZgeqvS6)@=22#d2VPQiz#VyEA} zpx--U;v{wRP|Cgx5q%(r@Zq)*J>0ZlFXA5t1XDMrT0l)+WeqJ@xP%S93lmgWrQY<0 z-J1>ltCK%8Twiby*$p0A8G?;TdJtEn?JU!hs?i3uI3osREy z04^UuD$iJj+W#i4`mn+{5_~Wp)~~ZBpM*HiFR^mPEzFvS!;2FF8f_S?Ni(ZAkNVAX5bue;`;q<4VYNvAotjEY<*F1l#A|;!ZpHAsF-^p;+APV@gDDh> zb34v3E~#v0t)WM>AZGBx5EfCzmnRU>URCHV6R>v#)cFKYkIBV8c zg4}W?35n}UmhRL3la|(V z+2zSN*@*Srt z{X%DRmJSqabE!^)s7L}pn$-`Il_d4c&hq13h<5+rWhwZzT_L9K_je1yU)#Mh+wIT$ zogw?*j@=mc9jiTQNPE5B@{9v??M0q$F%PhnuLcz^j>Rh>a$&)r(Lim>DO>m@VqwGl z%r00OmK!fza&e23kj;7})ReSD+R7wEzf1As4CIzgeId2>#P82$ zsbkL8Z#h3qjn4@gbJs;N-Q#5G^uI6$38G74SU=q<^r>San|fo{DLJpMUe;Zew0D)d z<0c>HUv@nnF;<~iUO6G4xdB@Uz(bxC0^7sv5^nTSEH}Ky*P(s58`f8?);PoaHWO z<*~KS)ER_5Xr0Ri+j<94W@hXPy9hw{-HQUzLnaj(X)Q@RNGVMez*KNExjH5#@WmNHOl^-sB5&lafie#!Z|0ef-`w;Qa)hy<=Z>x1$F(4h*UbAUe&~IPCr|pX znN_TR7s&r-IfOXC#^}FzRw^25N@^&dQ2`kURmc!n;KG$qfRHey(6zEbNm%xQJb5nd zK8Dt`4QyK*5KXS>%FLy+aNdX7szvhkVEJ*)-<>{1J~`zNToMG31F}!MS&olRkDrfr zKi}??^?+b@utQb8zu~Z9GSr(86%t|IH`Ni(SEWu;vLR(px>Gv#I=kIaCvsV?DVZNi zK7%Z6(%YuTHkgwWACudb1d#9WnXD$c3g+aNdrJ4XuB9ppFz1Z&6Tg4AG)$@#y-$@m z)MX`~m&_qOBAIwdIn{jCCVTBjNJM5wc&q*?0;(B5rZt7SvV}_JVaXB8q^c>>E;Za? zT1ZT6lH?&{Pi3C2yLI>>ORuvRN! zkpD};JKh(86BX>d?0e2<0c3VO& z37nZX)Qyni-TmIn7$+Fm6ZE>J)?ygS#Dj{ z;R=#>c$CVn1EpNw)Ij7CBljLgVt4AGIz0|C;ZgKLk|H(^g9u!wU_Su{LVea zN=4Xe&39w6C7gihgaorIEZtbQR=6r-MBwT(*?XPl3kn)*tQ29t#!RP7FV*EWyUhbC z64I(=m~NzLeke2nSt_p6!mvuNirH>+f=HNY3D}P;pOSILQcO4-2_wt9-c>*kXt4$` zm(q?IuGZJwtH=TD#?rTugj8%}T@n*^u4Ip|cSUUIiqZ!obIQt6T$1mT#-SQ*CBg4^ zt5_4Nk$aDnF2+eB&WWpq_QVKft+^gZkPsd-XyyA9?u=SxZ=}%9nttf1HjLQ{O~u*S zs3!~aIu%rc(`02TRLMbb6-N~5leE01!Jo_by&Jf@vg{a z37M60ZrE>)lrFy*_93z~_wC=G;M$&M_w$~?qu3SL!p#oTkpCR2&+i1ya1THS0mH`MF)F!5q_w*~ zhkk*=pxPPB;X9z)DfC>yy^DWX%YhmFUeqO|B`5x9QHqV?1=0?r*1&4H^V#P$Ei_aU zAI~FwEyV68Al3(*^H<6qV{`QQ&;{oCwl9#=2!$)TK zty#R&-~ahn^}G(wP;9>%)#=N{##}!6Tj#pQ44sdty70GsJ zHITfaE;gi1;$#})UWn>BkwK(&MKZx?Gf+N>ClZ?75IDu7)nTV!um9M9|0(dUjE398HV1n>sGnOkfbfl&ZZ) zSmN3=HNNgreT*2VP^dnI@iD_|Hj36h?Byt~8;h$|-=$Sqr(BA`C@oCcx1L_z$;w3= z#sL8BEn20x7ndm5kz-T6@_*QE`pM^>jTW8iXr24=7&a2)ujVjN*BSTAvf~A=Z-Ks- zOww;vb})IX&;hJ%ERf_ZLjP@Y0%HW&XynGjYMjO~LcJO`E*&pw2=#gkWo+?TGC zHUQQYSbs}eVS7kIg&~ixy;sf-*_PCfkQYjSWp2Awmy7m?a<8?Q^l$6U#Yv_h&$Hi_ z6S}UXcHr3qcFCglsOt?CQByLY*?V}MR>|v8c+VI|Mv+hCy*ZRAeGM}XP;n_y`@BMP z-B+sdS03eEI*7Z*pytB+Zv2jywvjD(Vjz5KF#^hFmQW_%m(ZY&A&T{7fV0M}(_EYG zgJ4`8>Ob`nTc zj&wZC33oF?1q&B9Qz6aNRN{akTlx&%d^po^N2c7OPqN9fI6|d5VyUDk#kn=TNjv!M z^qh^4PdxGrf&OR^^QoNK29L+s&jp13wB$(x4Ld+At#QVuBN;<9r0{51EK3mkYADbf z*oWCYHxH(Gn}pn4o;xG*$SVJoJEH{8NqgS$o6(2pPtm%VH|+aXr*~MiE&bc?Qmqe> zU0IWbeC_cV~iI@fiz1ZB7yleND#VWOd|bA5=sFjxCW%Z za;b?Yh&b7|)g-@)AOS4Jm^2(C7MQy@3r%8v6X2LOlmY~>7hR~i7=M&L0zSn_Ib!~! z?=jaMX_@~4#5S-W(o_CqQ-1vUw~v1f#AyExi2d`$7jZK-bpFpG)I_yVACyDPpJc1- z>BbN>B>aLk!+rW{3^r1HMO)nH7`jUl9qIH z0|U}1d1@Pn~llTlenSPnmSKpVy1aKpo&;Dhfq8y>-QF z4+(hLtZOC@hN5-WOG`=={We@Xyq;0mw-JV`l;5FL@n)k>ts!|h7Fn5p4uo()S+u_y zmzKOmCfx%y6pYm~YGcs>5F>)3WY-A@#b2O!9Sg6%>j7t{b@|<2QBV!VWNP<$4 zO)%?4o8rt%UezN*ok3N&xD0qWnE20Ngy`if$zek+;ayeOTbpsP|#-YM;<(9xBJw7%nsZM?bDRzvG1&Dq4hqdoH3Kbq-IB=>p3B8 zrfe-%)VkE})U&}aL1f=Whg+NZo$QezHNR4Ac(*9>z`4|F0PucsE9VZL2{$*Y7Zds} zviFGyS+Lxr)2l~Dhk-A>gS{DbF|olZ?^j|r0HJnHntvx0ZJHR%Wpa-jheM^bN9ayv`DiPCYbZ`` z#a3@;1tTpE|>gXF;eP?|b6-H#2W+2LO>;3bnxrYRC$ z4zLGujSI{~RtNntZW4WzOM1vu z!boEA2kG{B)*l&at!PY!?+B)SSlD(O4PUVQ^rU0b=3<<*MQ7>M4VE0$DVOSe>DI6q=dGa4vcv{C z8U{_@eMPutRDGg6!2FGUrTjWh`erPIJ;An+CSL5j^BPW~{OkuhLtUQvv4|{3YK8Pi zEX2ohhXW&FJ_&DuvC#*gs)%#z1~4HrrJi_)(}}&QQ=!W;3h5w}lsY2mz4ZFyMB&tL z^?Hlc9Buj^u0RJl5EtXCN?00p6eHZ*E4uid;$%r-->{i;QV$rt_}G5JppFnn!o2&% zj#K;P8_qvW+IWKWm|hJ)rIJ9j1@gghq#vQ#`lIwH-JIF@PDl^d?6f@N$z-AHfaypR ztBcakNwt)68mgxul7E(DQC4@MRB`DH$$+{~c36y3J1R?->)V7jEp=+~aFmT-1T&Re zAYTeVO+2N)P51Sny25HBwM9*2PPy)vF&*g}&s`QVk`tTP?|+-t_s|A7bYx>+l2*`T z*Hi6pMlM2wI@qYp{nJTSjJ&737YhkkpLT>J?b-^#i>T=SmSfMNGHF#M>QXND*3_BHV?OuF;5dYgsB{z|mB z`ay2-zSb&VrTcX3XhT~=*%nqjqR&XkPbon~9L<6Lv?-?DYENW^y`r>t`>JgR9$?D@ zYm75543*8vv5S0Dh=OBMR=r~pj#^hf&v-=Mh>jarh-0gDlHT@>N&L7yD5yVJP+7zp zfh1dm3Iu@6=*Bk98G+CuBV(ws)p*HLDVCHXp5#CMf{lT&y@zPr1T)u! zulfHdhzi_*Ta)EXnOFkl?Tj8tRf72#byFGaFgAgL(J+c?Z$`<5zf^P%!gK}(zcP34 zs&PRc(Ag@#;L)LYgwYUVrP&y=@F4^T!FZl9tJCdpA@j6`*V6CW+&Mqo0u^({zY)?e zy0zwQy!?O_b&>lX;7M9(5_AiEb@vW;^76eK)tsGnCxvPE`XZp@5X|R_RyfG#^XEb0 zGarSwDcV@5aonEr{<#~s@dzxUsU(Q3Nx(OFL&AJPRbA{=-{rb7Ldp3=N?TD5 z^clj%{d!oJow3m9?k3;ua^sv+yRs=X@fHJyHS31Crt%DB@^+W~pxFk*IHTK3oZ*+e z;RjEzoJBR5YyBP7=wRalB2QQ(!xwB;Km$O;K$NR6CQ$7Q*Bsi8_|1imAm9h2 z*((m|3u}YcQFceo0Ux4&INUds|7eZphNp#Ob>jCxvVsbp4o_b=-2)Y}=8&t2xg-8vI2~83V6pED5 zqr-p^oU<2_Xoxwj4y!-rvX*>juIL0mi7mqIebzn)J3Bs7y6AuG;DC@ddN79*jK|yw zDZ-oE<>(NmOe(>h&}B_!r@ZCQCc4N(qfQrm>{j|myvGXT%*=?Z`U5uT&3txOext-o z*$fO3U>`V|U}@Tu_GM z7TJ+MI?Qtbv1*L6|It7D9Qu+Nzfq~Ec!I(xdXK8V!=}jX@{x}-WKFC+7D>R@^+@l- z^(D>z6l4Q1fgfx1aM(0sCBuQP! z>(YK0MJoz;Q8A12WdwFKG6OiLa$Sh4Kc`A@HDIZkp81IoWor1BPLgzc%JyoO+DCMq| zZYje>Ec4_@R?lDk(-xwk+YqoRVmDh_D`+FT^fEVJ}AoAlIK!Z4N3H#zF!uX=*F zg4x+f3&(FM?s0j$VOR6{`u4Q+P5e4496i*ke~ipjMV);?RA^KEVIY<)tHhctxid9g zy9rN5JzH#?FtM=wu~CU~v(|x^qo@$IMK9XgL$y%DRmFWD^=8&jeIn?oP;-`vlC3}msnFPLuaU+U zL+;Fc7S~Xc6S?m?^803dX(ODeB}d^+9RY6QZl-j#HB9%cTn%V4m6rE3q1`nHk?35E z$e#|!!$0W9QspQ~w2+8GAH8IO7`+zvizo!k#3Cz0=E)rF0Io2_45?o%$L u$^9`glA4L_$;; zl_@l;sD)gFWv6AEE@av@Xw>npN{T;9a#1&{=Ca6jEx|2>x^l11J}Nd6t}a zJ`Jzs_HrRFj^lM*GD9{Jlk7ku5y8|us0hTh=NU`1cwHVgibA#d>|~WmAkobcX9vRE z18wo)4?2_k9GI)@gVC(&0aospf@f*;pBH1?-=O^HOy89YS%2*NzCrc(H!L{>Rag)E zL}DS$!xkPTq%lUlsRHI9%ny8&iksp7pz3QkO9 zFQ}2^RLn^A^P-E<#=j1!KpAW!!NKTlxScZGhb+peIVJM*nj4}3xf0VDK^Z7f*}P`9 zchpTGTBA=TBPd|bgrfP7?AnCFuw|rP)8)>3vP7BQ+PBUclN3YI##*Qa0%ubDb&MQg zVjU?-7H3A2O?6MLxZt|7z~fWM^^rG^2(>>RZp69e|&UmgRlwO8eK&l4|StkbuL%svOd@YAGBm{DDJ z11%k_fQ~4Wcz=R~1AW+p#@|W8XA(6vMi_&P7o5&p1ejWWAvz3aogID?JO*MMdsgH& z9##x9Yh95;q)Q_;Z2=(a#_NU1Lo(^B!xpe6eK4}_FmLH5o2X+o4Z@3pX zz1z3^xn6bmz#BJ03L`p!yqLGp)6$CL5W22GJ4-=JH)%CyvA5Luzp@DwV(AsLT0;0>OOLOzS1Ulc z63@(09D2uKGWu)9To?-Uu_-^J4Zk+u9w z

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

a4Fj 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 b@62=lH1&Xbc%zse^rHy+Ms6*?`S1S#s9cW^ diff --git a/.mvn/wrapper/maven-wrapper.properties b/.mvn/wrapper/maven-wrapper.properties index a447c9fa81..6686a643d7 100644 --- a/.mvn/wrapper/maven-wrapper.properties +++ b/.mvn/wrapper/maven-wrapper.properties @@ -1 +1,18 @@ -distributionUrl=https://repo1.maven.org/maven2/org/apache/maven/apache-maven/3.5.2/apache-maven-3.5.2-bin.zip \ No newline at end of file +# 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 +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.0/apache-maven-3.9.0-bin.zip +wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.1/maven-wrapper-3.1.1.jar