diff --git a/.mvn/wrapper/maven-wrapper.jar b/.mvn/wrapper/maven-wrapper.jar old mode 100755 new mode 100644 index 279ff4cdc0..bf82ff01c6 Binary files a/.mvn/wrapper/maven-wrapper.jar and b/.mvn/wrapper/maven-wrapper.jar differ 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 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-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..1627365242 --- /dev/null +++ b/spring-cloud-alibaba-docs/src/main/asciidoc-zh/kubernetes-config.adoc @@ -0,0 +1,145 @@ +== Spring Cloud Alibaba Kubernetes Config + +link:../asciidoc/kubernetes-config.adoc[English] | link:kubernetes-config.adoc[中文] + +该模块的目的是利用 Kubernetes ConfigMap/Secret 作为分布式配置中心,实现无需重启应用的动态配置更新。 + +=== 快速开始 + +参考 link:../../../../spring-cloud-alibaba-examples/kubernetes-config-example/README-zh.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: + alibaba-kubernetes: + 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: + alibaba-kubernetes: + 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: + alibaba-kubernetes: + config: + refresh-on-deleted: false # 当检测到配置被删除时,应用程序不会触发刷新。默认值为 false。 +---- + +如果你的配置没有跨环境同步,你在 `dev` 环境中有 `important-config` 配置,但在 `prod` 环境中没有(有可能是你忘记同步到 `prod`),你可能希望有一个机制来帮助你发现这个问题,并提供快速失败的能力来阻止应用程序启动。 + +相关配置: + +[source,yaml] +---- +spring: + cloud: + alibaba-kubernetes: + config: + fail-on-missing-config: true # 如果找不到配置,应用程序将无法启动。默认值为 true。 +---- + +=== 核心配置 + +[source,yaml] +---- +spring: + cloud: + alibaba-kubernetes: + 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 new file mode 100644 index 0000000000..c8daa0e741 --- /dev/null +++ b/spring-cloud-alibaba-docs/src/main/asciidoc/kubernetes-config.adoc @@ -0,0 +1,146 @@ +== 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 + +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 + +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. + +==== Dynamic update configuration(ConfigMap/Secret) + +You don't need to restart the application when the configuration is updated, and the application can dynamically update the configurations. + +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. + +*Best Practices: use `@ConfigurationProperties` to organize your configurations!* + +Related configuration: + +[source,yaml] +---- +spring: + cloud: + alibaba-kubernetes: + 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 +---- + +==== Configuration priority + +If there are same configurations in both local `application.yml` and ConfigMaps/Secret, you can choose which configuration to use first. + +Related configuration: + +[source,yaml] +---- +spring: + cloud: + alibaba-kubernetes + config: + preference: remote # Global default config, default value is remote, means remote config will override local config + config-maps: + - name: my-configmap + preference: local +---- + +==== Multiple configuration file formats + +Supports configuration files in `yaml`, `properties`, `json` and key-value pair. + +NOTE: Secret only supports key-value pair! + +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 +---- + +The two configurations above convey the same meaning. + +==== Abnormal operation protection + +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. + +Related configuration: + +[source,yaml] +---- +spring: + cloud: + alibaba-kubernetes + config: + 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 `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: + +[source,yaml] +---- +spring: + cloud: + alibaba-kubernetes + config: + fail-on-missing-config: true # application will fail to start if the configs not found. Default value is true. +---- + +=== Core Configurations + +[source,yaml] +---- +spring: + cloud: + 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. + 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-examples/kubernetes-config-example/README-zh.md b/spring-cloud-alibaba-examples/kubernetes-config-example/README-zh.md new file mode 100644 index 0000000000..54fb0d79af --- /dev/null +++ b/spring-cloud-alibaba-examples/kubernetes-config-example/README-zh.md @@ -0,0 +1,65 @@ +## Spring Cloud Starter Alibaba Kubernetes Config Example + +[English](README.md) | [中文](README-zh.md) + +这个例子演示了使用 `spring-cloud-starter-alibaba-kubernetes-config` 来实现一个动态黑名单功能。 + +### 步骤 + +*NOTE:在阅读之前你需要了解一些基本的 [Kubernetes](https://kubernetes.io/docs/home/) 知识,并准备一个有访问权限(ConfigMap/Secret)的 +Kubernetes 集群。* + +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 new file mode 100644 index 0000000000..e11e81a857 --- /dev/null +++ b/spring-cloud-alibaba-examples/kubernetes-config-example/README.md @@ -0,0 +1,66 @@ +## 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 + +*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. + + ```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 new file mode 100644 index 0000000000..9b2cccc2be --- /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 + Spring Cloud Starter Alibaba 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..d22864b7bd --- /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,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.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; + +/** + * @author Freeman + */ +@RestController +@RefreshScope +public class EchoController { + + @Value("${blacklist.header}") + private String userIdHeader; + + @GetMapping("/echo") + 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 new file mode 100644 index 0000000000..740a788936 --- /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,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.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 { + + 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 userIdHeader = blacklistProperties.getHeader(); + String userId = req.getHeader(userIdHeader); + if (userId == null) { + resp.sendError(HttpServletResponse.SC_UNAUTHORIZED, + userIdHeader + " header is required!"); + return; + } + if (blacklistProperties.getUserIds().contains(userId)) { + 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 new file mode 100644 index 0000000000..1eea5e9d00 --- /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,33 @@ +/* + * 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 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 new file mode 100644 index 0000000000..8dadceea8c --- /dev/null +++ b/spring-cloud-alibaba-examples/kubernetes-config-example/src/main/resources/application.yml @@ -0,0 +1,21 @@ +server: + port: 8080 + error: + include-message: always +spring: + application: + name: kubernetes-config-example + cloud: + alibaba-kubernetes: + 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: true 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-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..3c2983bf6f --- /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.boot + spring-boot-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-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..5ad3ecd084 --- /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,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.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<>(); + + /** + * Get or create a {@link KubernetesClient}. + * + * @return Kubernetes client + */ + public static KubernetesClient getKubernetesClient() { + if (kubernetesClient.get() == null) { + kubernetesClient.compareAndSet(null, KubernetesUtils.newKubernetesClient()); + } + return kubernetesClient.get(); + } + + /** + * Remove the {@link KubernetesClient} instance. + */ + public static 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..469b07b4f4 --- /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,74 @@ +/* + * 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; + +/** + * Kubernetes utils. + * + *

+ * Usually used to get kube config and create {@link KubernetesClient} instance. + * + * @author Freeman + */ +public final class KubernetesUtils { + + private KubernetesUtils() { + throw new UnsupportedOperationException("No KubernetesUtil instances for you!"); + } + + private static final Config config = new ConfigBuilder().build(); + + /** + * Get the kube config. + * + *

+ * NOTE: {@link Config} needs to be a singleton, do NOT modify it. + * + * @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(); + } + + /** + * Create 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 new file mode 100644 index 0000000000..e5de64bc8b --- /dev/null +++ b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/pom.xml @@ -0,0 +1,37 @@ + + 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 + + + + org.springframework.cloud + spring-cloud-starter + + + com.alibaba.cloud + spring-cloud-alibaba-kubernetes-commons + + + org.springframework.boot + spring-boot-configuration-processor + true + + + org.springframework.boot + spring-boot-starter-test + test + + + + \ No newline at end of file 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..2daf11a79d --- /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,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; + +import com.alibaba.cloud.kubernetes.commons.KubernetesClientConfiguration; +import com.alibaba.cloud.kubernetes.config.core.KubernetesConfigWatcher; +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.ConditionalOnMissingBean; +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; + +/** + * Spring Cloud Alibaba Kubernetes Config autoconfiguration. + * + * @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 + @ConditionalOnMissingBean + 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/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..28cb5daccd --- /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,400 @@ +/* + * 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.Preference; + +import org.springframework.beans.factory.InitializingBean; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.util.StringUtils; + +/** + * Spring Cloud Alibaba Kubernetes Config properties. + * + * @author Freeman + */ +@ConfigurationProperties(KubernetesConfigProperties.PREFIX) +public class KubernetesConfigProperties implements InitializingBean { + /** + * Prefix of {@link KubernetesConfigProperties}. + */ + public static final String PREFIX = "spring.cloud.alibaba-kubernetes.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 Preference#REMOTE}, means remote + * configurations 'win', will override the local configurations. + */ + private Preference preference = Preference.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; + + /** + * 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; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + public String getNamespace() { + return namespace; + } + + public void setNamespace(String namespace) { + this.namespace = namespace; + } + + public Preference getPreference() { + return preference; + } + + public void setPreference(Preference 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 mergeConfigmaps() { + for (ConfigMap configMap : configMaps) { + if (!StringUtils.hasText(configMap.getName())) { + throw new IllegalArgumentException("ConfigMap name must not be empty."); + } + if (configMap.getNamespace() == null) { + configMap.setNamespace(namespace); + } + if (configMap.getRefreshable() == null) { + configMap.setRefreshable(refreshable); + } + if (configMap.getPreference() == null) { + configMap.setPreference(preference); + } + } + } + + 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"; + } + + public static class ConfigMap { + /** + * ConfigMap name. + */ + private String name; + /** + * Namespace, using + * {@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.alibaba-kubernetes.config.refreshable} if not + * set. + */ + private Boolean refreshable; + /** + * Config preference, using + * {@code spring.cloud.alibaba-kubernetes.config.preference} if not + * set. + */ + private Preference 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 Preference getPreference() { + return preference; + } + + public void setPreference(Preference preference) { + this.preference = preference; + } + + @Override + public String toString() { + return "ConfigMap{" + "name='" + name + '\'' + ", namespace='" + namespace + + '\'' + ", refreshable=" + 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, using + * {@code spring.cloud.alibaba-kubernetes.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.alibaba-kubernetes.config.preference} if not + * set. + */ + private Preference 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 Preference getPreference() { + return preference; + } + + public void setPreference(Preference preference) { + this.preference = preference; + } + + @Override + public String toString() { + return "Secret{" + "name='" + name + '\'' + ", namespace='" + namespace + '\'' + + ", refreshable=" + 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/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..90a694e619 --- /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,115 @@ +/* + * 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; + +/** + * {@link HasMetadataResourceEventHandler} process {@link HasMetadata}(ConfigMap/Secret) + * events, trigger {@link RefreshEvent} when necessary. + * + * @author Freeman + */ +class HasMetadataResourceEventHandler + implements ResourceEventHandler { + private static final Logger log = LoggerFactory + .getLogger(HasMetadataResourceEventHandler.class); + + private final ApplicationContext context; + private final KubernetesConfigProperties properties; + + HasMetadataResourceEventHandler(ApplicationContext context, + KubernetesConfigProperties properties) { + this.context = context; + this.properties = properties; + } + + @Override + public void onAdd(HasMetadata obj) { + if (log.isDebugEnabled()) { + 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 + // 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("[Kubernetes Config] {} '{}' 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("[Kubernetes Config] {} '{}' deleted in namespace '{}'", + obj.getKind(), obj.getMetadata().getName(), + obj.getMetadata().getNamespace()); + } + if (properties.isRefreshOnDelete()) { + deletePropertySourceOfResource(obj); + refresh(obj); + } + else { + 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()); + } + } + } + + private void deletePropertySourceOfResource(HasMetadata resource) { + String propertySourceName = Converters.propertySourceNameForResource(resource); + ((ConfigurableEnvironment) context.getEnvironment()).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/KubernetesConfigEnvironmentPostProcessor.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 new file mode 100644 index 0000000000..226b58b0c1 --- /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/KubernetesConfigEnvironmentPostProcessor.java @@ -0,0 +1,285 @@ +/* + * 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.net.HttpURLConnection; +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.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.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; +import io.fabric8.kubernetes.client.KubernetesClient; +import io.fabric8.kubernetes.client.KubernetesClientException; +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.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; +import static java.util.stream.Collectors.groupingBy; +import static java.util.stream.Collectors.mapping; +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 KubernetesConfigEnvironmentPostProcessor 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 KubernetesConfigEnvironmentPostProcessor(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(); + // Just add it, {@link + // org.springframework.cloud.context.refresh.ContextRefresher} will + // replace the PropertySource for you. + if (resource instanceof ConfigMap) { + environment.getPropertySources() + .addLast(toPropertySource((ConfigMap) resource)); + } + else if (resource instanceof Secret) { + environment.getPropertySources() + .addLast(toPropertySource((Secret) resource)); + } + else { + log.warn("[Kubernetes Config] 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)) + .orElseGet(() -> { + KubernetesConfigProperties prop = Binder.get(environment) + .bind(KubernetesConfigProperties.PREFIX, + KubernetesConfigProperties.class) + .orElseGet(KubernetesConfigProperties::new); + prop.afterPropertiesSet(); + return prop; + }); + } + + private void pullConfigMaps(KubernetesConfigProperties properties, + ConfigurableEnvironment environment) { + properties.getConfigMaps().stream() + .map(configmap -> Optional + .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()))) + .forEach((preference, remotePropertySources) -> { + addPropertySourcesToEnvironment(environment, preference, + remotePropertySources); + }); + } + + private void pullSecrets(KubernetesConfigProperties properties, + ConfigurableEnvironment environment) { + properties.getSecrets().stream() + .map(secret -> Optional + .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()))) + .forEach((preference, remotePropertySources) -> { + addPropertySourcesToEnvironment(environment, preference, + remotePropertySources); + }); + } + + private static void addPropertySourcesToEnvironment( + ConfigurableEnvironment environment, Preference preference, + List> remotePropertySources) { + MutablePropertySources propertySources = environment.getPropertySources(); + switch (preference) { + 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: " + preference.name()); + } + } + + private EnumerablePropertySource propertySourceForConfigMap( + KubernetesConfigProperties.ConfigMap cm, boolean isFailOnMissingConfig) { + ConfigMap configMap; + try { + configMap = client.configMaps().inNamespace(cm.getNamespace()) + .withName(cm.getName()).get(); + } + catch (KubernetesClientException e) { + processException(ConfigMap.class, cm.getName(), cm.getNamespace(), e); + return null; + } + if (configMap == null) { + failApplicationStartUpIfNecessary(ConfigMap.class, cm.getName(), + cm.getNamespace(), isFailOnMissingConfig); + return null; + } + return toPropertySource(configMap); + } + + private EnumerablePropertySource propertySourceForSecret( + KubernetesConfigProperties.Secret secret, boolean isFailOnMissingConfig) { + Secret secretInK8s; + try { + secretInK8s = client.secrets().inNamespace(secret.getNamespace()) + .withName(secret.getName()).get(); + } + catch (KubernetesClientException e) { + processException(Secret.class, secret.getName(), secret.getNamespace(), e); + return null; + } + if (secretInK8s == null) { + failApplicationStartUpIfNecessary(Secret.class, secret.getName(), + 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. + 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. + * + *

+ * 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 isFailOnMissingConfig whether to fail the application start up + */ + 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); + } + } + + 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/KubernetesConfigWatcher.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 new file mode 100644 index 0000000000..2798bc89d5 --- /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/KubernetesConfigWatcher.java @@ -0,0 +1,118 @@ +/* + * 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 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 static com.alibaba.cloud.kubernetes.config.util.ResourceKeyUtils.resourceKey; + +/** + * Watcher for config resources change. + * + * @author Freeman + */ +public class KubernetesConfigWatcher + implements SmartInitializingSingleton, ApplicationContextAware, DisposableBean { + private static final Logger log = LoggerFactory.getLogger(KubernetesConfigWatcher.class); + + private final Map> configmapInformers = new LinkedHashMap<>(); + private final Map> secretInformers = new LinkedHashMap<>(); + private final KubernetesConfigProperties properties; + private final KubernetesClient client; + + private ApplicationContext context; + + public KubernetesConfigWatcher(KubernetesConfigProperties properties, KubernetesClient client) { + this.properties = properties; + this.client = client; + } + + @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); + if (log.isInfoEnabled()) { + log.info("[Kubernetes Config] ConfigMap and Secret informers closed"); + } + } + + private void watchRefreshableResources(KubernetesConfigProperties properties, + KubernetesClient client) { + properties.getConfigMaps().stream() + .filter(KubernetesConfigProperties.ConfigMap::getRefreshable) + .forEach(configmap -> { + SharedIndexInformer informer = client.configMaps() + .inNamespace(configmap.getNamespace()) + .withName(configmap.getName()) + .inform(new HasMetadataResourceEventHandler<>(context, + properties)); + configmapInformers.put(resourceKey(configmap), informer); + }); + log(configmapInformers); + properties.getSecrets().stream() + .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); + } + + private static 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("[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/exception/AbstractKubernetesConfigException.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 new file mode 100644 index 0000000000..c7b994e78e --- /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/AbstractKubernetesConfigException.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.exception; + +/** + * @author Freeman + */ +public abstract class AbstractKubernetesConfigException extends RuntimeException { + private final Class type; + private final String name; + private final String namespace; + + public AbstractKubernetesConfigException(Class type, String name, String namespace, + Throwable cause) { + super(cause); + 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/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/KubernetesConfigMissingFailureAnalyzer.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 new file mode 100644 index 0000000000..0ace7cafcd --- /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/KubernetesConfigMissingFailureAnalyzer.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 KubernetesConfigMissingFailureAnalyzer + extends AbstractFailureAnalyzer { + @Override + protected FailureAnalysis analyze(Throwable rootFailure, + 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( + "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/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/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..924a16c6de --- /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,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.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 { + + /** + * 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..4be7de082d --- /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; + +/** + * Convert JSON string to {@link PropertySource}, support JSON array. + * + * @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 + 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; + } + + 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 + "[part]", + 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..b5a9067b7f --- /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,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.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; + +/** + * Convert properties file to {@link PropertySource}. + * + * @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..59ee71c832 --- /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,63 @@ +/* + * 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; + +/** + * Convert yaml file to {@link PropertySource}, support multi-document yaml file. + * + * @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/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..675cf1eab8 --- /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,141 @@ +/* + * 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.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; + +/** + * {@link Converters} use to convert ConfigMap/Secret to {@link EnumerablePropertySource}. + * + * @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(toPropertySource( + 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..f7923e7fea --- /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,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.util; + +/** + * Utility class for holding a pair of values. + * + * @author Freeman + */ +public final class Pair { + private final K key; + private final V value; + + private Pair(K key, V value) { + this.key = key; + this.value = value; + } + + public K key() { + return key; + } + + public V value() { + return value; + } + + 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/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 new file mode 100644 index 0000000000..030ca419a0 --- /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/Preference.java @@ -0,0 +1,35 @@ +/* + * 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; + +/** + * Configuration preference. + * + * @author Freeman + */ +public enum Preference { + /** + * 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/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..f0c420e8f9 --- /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,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.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; + +/** + * {@link Processors} holds all the {@link FileProcessor} instances. + * + * @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..3bd7f7def0 --- /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,72 @@ +/* + * 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; + +/** + * {@link ResourceKey} represents identity of a resource. + * + * @author Freeman + */ +public final class ResourceKey { + private final String type; + private final String name; + private final String namespace; + + public ResourceKey(String type, String name, String namespace) { + this.type = type; + this.name = name; + this.namespace = namespace; + } + + public String type() { + return type; + } + + public String name() { + return name; + } + + public String namespace() { + return namespace; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + 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); + } + + @Override + public String toString() { + 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/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 new file mode 100644 index 0000000000..b126d75a95 --- /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/ResourceKeyUtils.java @@ -0,0 +1,56 @@ +/* + * 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 com.alibaba.cloud.kubernetes.config.KubernetesConfigProperties; +import io.fabric8.kubernetes.api.model.ConfigMap; +import io.fabric8.kubernetes.api.model.Secret; + +/** + * {@link ResourceKeyUtils} used to generate {@link ResourceKey}. + * + * @author Freeman + */ +public final class ResourceKeyUtils { + + 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()); + } + + /** + * 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()); + } +} 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/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 new file mode 100644 index 0000000000..b550fbf20b --- /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/SinglePairPropertySource.java @@ -0,0 +1,41 @@ +/* + * 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.Collections; +import java.util.Map; + +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/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..760ed5026c --- /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,10 @@ +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ + com.alibaba.cloud.kubernetes.config.KubernetesConfigAutoConfiguration + +org.springframework.boot.env.EnvironmentPostProcessor=\ + com.alibaba.cloud.kubernetes.config.core.KubernetesConfigEnvironmentPostProcessor + +org.springframework.boot.diagnostics.FailureAnalyzer=\ + 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/it/ConfigMapIntegrationTests.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 new file mode 100644 index 0000000000..2243af2f0a --- /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/ConfigMapIntegrationTests.java @@ -0,0 +1,87 @@ +/* + * 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.it; + +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("configmap") +public class ConfigMapIntegrationTests { + + @BeforeAll + static void init() { + createOrReplaceConfigMap("configmap/configmap.yaml"); + } + + @AfterAll + static void recover() { + deleteConfigMap("configmap/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 + createOrReplaceConfigMap("configmap/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 + deleteConfigMap("configmap/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/it/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 new file mode 100644 index 0000000000..5eafa4da6f --- /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/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.it; + +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/it/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 new file mode 100644 index 0000000000..7f2df86028 --- /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/MissingConfigIntegrationTests.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.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; + +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 { + private static final String PROFILE = "missing-config"; + + @Test + void testEnabledFailOnMissingConfig() { + assertThatCode(() -> new SpringApplicationBuilder(Empty.class) + .web(WebApplicationType.NONE).profiles(PROFILE).run().close()) + .isInstanceOf(KubernetesConfigMissingException.class); + } + + @Test + void testDisabledFailOnMissingConfig() { + assertThatCode(() -> new SpringApplicationBuilder(Empty.class) + .web(WebApplicationType.NONE) + .properties(KubernetesConfigProperties.PREFIX + + ".fail-on-missing-config=false") + .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/it/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 new file mode 100644 index 0000000000..9ad7702925 --- /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/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.it; + +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/it/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 new file mode 100644 index 0000000000..672ae6df19 --- /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/SecretIntegrationTests.java @@ -0,0 +1,90 @@ +/* + * 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.it; + +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.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; + +/** + * @author Freeman + */ +@KubernetesAvailable +@SpringBootTest(classes = Empty.class, webEnvironment = NONE) +@ActiveProfiles("secret") +public class SecretIntegrationTests { + + @BeforeAll + static void init() { + createOrReplaceConfigMap("secret/configmap.yaml"); + createOrReplaceSecret("secret/secret.yaml"); + createOrReplaceSecret("secret/secret-refreshable.yaml"); + } + + @AfterAll + static void recover() { + deleteConfigMap("secret/configmap.yaml"); + deleteSecret("secret/secret.yaml"); + 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(); + + createOrReplaceSecret("secret/secret-changed.yaml"); + 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/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/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..7f6288ef17 --- /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,94 @@ +/* + * 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() { + // @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); + + 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() { + // @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); + + // 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..08b42ac66d --- /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,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.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() { + // @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); + + 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..4bc03b09bf --- /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,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.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() { + // @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); + 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() { + // @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); + // 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-configmap.yml b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/test/resources/application-configmap.yml new file mode 100644 index 0000000000..5a82eebeed --- /dev/null +++ b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/test/resources/application-configmap.yml @@ -0,0 +1,17 @@ +spring: + cloud: + alibaba-kubernetes: + config: + namespace: default + configmaps: + - name: my-configmap + refreshable: true + application: + name: configmap +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-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..61f7b44025 --- /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: + alibaba-kubernetes: + 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-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..1b47931ec1 --- /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: + alibaba-kubernetes: + 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..47fdf70bb4 --- /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: + alibaba-kubernetes: + 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/configmap/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 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/configmap/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/configmap/configmap.yaml b/spring-cloud-alibaba-starters/spring-cloud-alibaba-kubernetes/spring-cloud-starter-alibaba-kubernetes-config/src/test/resources/configmap/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/configmap/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