diff --git a/apollo-client/pom.xml b/apollo-client/pom.xml
index 7a8e436a..ce9d7d3b 100644
--- a/apollo-client/pom.xml
+++ b/apollo-client/pom.xml
@@ -98,5 +98,10 @@
test
+
+ io.kubernetes
+ client-java
+ 18.0.0
+
diff --git a/apollo-client/src/main/java/com/ctrip/framework/apollo/Kubernetes/KubernetesManager.java b/apollo-client/src/main/java/com/ctrip/framework/apollo/Kubernetes/KubernetesManager.java
new file mode 100644
index 00000000..130a29c1
--- /dev/null
+++ b/apollo-client/src/main/java/com/ctrip/framework/apollo/Kubernetes/KubernetesManager.java
@@ -0,0 +1,180 @@
+/*
+ * Copyright 2022 Apollo Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+package com.ctrip.framework.apollo.Kubernetes;
+
+import io.kubernetes.client.openapi.ApiClient;
+import io.kubernetes.client.openapi.apis.CoreV1Api;
+import io.kubernetes.client.openapi.Configuration;
+import io.kubernetes.client.openapi.models.*;
+import io.kubernetes.client.util.Config;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.stereotype.Service;
+
+import javax.annotation.PostConstruct;
+import java.util.Map;
+
+@Service
+public class KubernetesManager {
+ private ApiClient client;
+ private CoreV1Api coreV1Api;
+
+ private final Logger log = LoggerFactory.getLogger(this.getClass());
+
+ @PostConstruct
+ public void initClient() {
+ try {
+ client = Config.defaultClient();
+ Configuration.setDefaultApiClient(client);
+ coreV1Api = new CoreV1Api(client);
+
+ } catch (Exception e) {
+ String errorMessage = "Failed to initialize Kubernetes client: " + e.getMessage();
+ log.error(errorMessage, e);
+ throw new RuntimeException(errorMessage, e);
+ }
+ }
+
+ /**
+ * Creates a Kubernetes ConfigMap.
+ *
+ * @param configMapNamespace the namespace of the ConfigMap
+ * @param name the name of the ConfigMap
+ * @param data the data to be stored in the ConfigMap
+ * @return the name of the created ConfigMap
+ * @throws RuntimeException if an error occurs while creating the ConfigMap
+ */
+ public String createConfigMap(String configMapNamespace, String name, Map data) {
+ if (configMapNamespace == null || configMapNamespace == "" || name == null || name == "") {
+ log.error("create config map failed due to null parameter");
+ throw new RuntimeException("ConfigMap namespace and name cannot be null or empty");
+ }
+ V1ConfigMap configMap = new V1ConfigMap().metadata(new V1ObjectMeta().name(name).namespace(configMapNamespace)).data(data);
+ try {
+ coreV1Api.createNamespacedConfigMap(configMapNamespace, configMap, null, null, null,null);
+ return name;
+ } catch (Exception e) {
+ log.error("create config map failed", e);
+ throw new RuntimeException("Failed to create ConfigMap: " + e.getMessage(), e);
+ }
+ }
+
+ /**
+ * get value from config map
+ * @param configMapNamespace
+ * @param name config map name (appId)
+ * @return configMap data(all key-value pairs in config map)
+ */
+ public String loadFromConfigMap(String configMapNamespace, String name) {
+ if (configMapNamespace == null || configMapNamespace.isEmpty() || name == null || name.isEmpty() || name == null || name.isEmpty()) {
+ log.error("参数不能为空");
+ throw new RuntimeException(String
+ .format("参数不能为空, configMapNamespace: %s, name: %s", configMapNamespace, name));
+ }
+ try {
+ V1ConfigMap configMap = coreV1Api.readNamespacedConfigMap(name, configMapNamespace, null);
+ if (configMap == null) {
+ log.error("ConfigMap不存在");
+ throw new RuntimeException(String
+ .format("ConfigMap不存在, configMapNamespace: %s, name: %s", configMapNamespace, name));
+ }
+ Map data = configMap.getData();
+ if (data != null && data.containsKey(name)) {
+ return data.get(name);
+ } else {
+ log.error("在ConfigMap中未找到指定的键: " + name);
+ throw new RuntimeException(String
+ .format("在ConfigMap中未找到指定的键: %s, configMapNamespace: %s, name: %s", name, configMapNamespace, name));
+ }
+ } catch (Exception e) {
+ log.error("get config map failed", e);
+ throw new RuntimeException(String
+ .format("get config map failed, configMapNamespace: %s, name: %s", configMapNamespace, name));
+ }
+ }
+
+ /**
+ * get value from config map
+ * @param configMapNamespace configMapNamespace
+ * @param name config map name (appId)
+ * @param key config map key (cluster+namespace)
+ * @return value(json string)
+ */
+ public String getValueFromConfigMap(String configMapNamespace, String name, String key) {
+ if (configMapNamespace == null || configMapNamespace.isEmpty() || name == null || name.isEmpty() || key == null || key.isEmpty()) {
+ log.error("参数不能为空");
+ return null;
+ }
+ try {
+ V1ConfigMap configMap = coreV1Api.readNamespacedConfigMap(name, configMapNamespace, null);
+ if (configMap == null || configMap.getData() == null) {
+ log.error("ConfigMap不存在或没有数据");
+ return null;
+ }
+ if (!configMap.getData().containsKey(key)) {
+ log.error("在ConfigMap中未找到指定的键: " + key);
+ return null;
+ }
+ return configMap.getData().get(key);
+ } catch (Exception e) {
+ log.error("get config map failed", e);
+ return null;
+ }
+ }
+
+ /**
+ * update config map
+ * @param configMapNamespace
+ * @param name config map name (appId)
+ * @param data new data
+ * @return config map name
+ */
+ public String updateConfigMap(String configMapNamespace, String name, Map data) {
+ if (configMapNamespace == null || configMapNamespace.isEmpty() || name == null || name.isEmpty() || data == null || data.isEmpty()) {
+ log.error("参数不能为空");
+ return null;
+ }
+ try {
+ V1ConfigMap configMap = new V1ConfigMap().metadata(new V1ObjectMeta().name(name).namespace(configMapNamespace)).data(data);
+ coreV1Api.replaceNamespacedConfigMap(name, configMapNamespace, configMap, null, null, null, "fieldManagerValue");
+ return name;
+ } catch (Exception e) {
+ log.error("update config map failed", e);
+ return null;
+ }
+ }
+
+ /**
+ * check config map exist
+ * @param configMapNamespace config map namespace
+ * @param configMapName config map name
+ * @return true if config map exist, false otherwise
+ */
+ public boolean checkConfigMapExist(String configMapNamespace, String configMapName) {
+ if (configMapNamespace == null || configMapNamespace.isEmpty() || configMapName == null || configMapName.isEmpty()) {
+ log.error("参数不能为空");
+ return false;
+ }
+ try {
+ coreV1Api.readNamespacedConfigMap(configMapName, configMapNamespace, null);
+ return true;
+ } catch (Exception e) {
+ log.error("check config map failed", e);
+ return false;
+ }
+ }
+}
diff --git a/apollo-client/src/main/java/com/ctrip/framework/apollo/enums/ConfigSourceType.java b/apollo-client/src/main/java/com/ctrip/framework/apollo/enums/ConfigSourceType.java
index b8249472..d3e3507e 100644
--- a/apollo-client/src/main/java/com/ctrip/framework/apollo/enums/ConfigSourceType.java
+++ b/apollo-client/src/main/java/com/ctrip/framework/apollo/enums/ConfigSourceType.java
@@ -22,7 +22,10 @@
* @since 1.1.0
*/
public enum ConfigSourceType {
- REMOTE("Loaded from remote config service"), LOCAL("Loaded from local cache"), NONE("Load failed");
+ REMOTE("Loaded from remote config service"),
+ LOCAL("Loaded from local cache"),
+ CONFIGMAP("Loaded from k8s config map"),
+ NONE("Load failed");
private final String description;
diff --git a/apollo-client/src/main/java/com/ctrip/framework/apollo/internals/K8sConfigMapConfigRepository.java b/apollo-client/src/main/java/com/ctrip/framework/apollo/internals/K8sConfigMapConfigRepository.java
new file mode 100644
index 00000000..aa26389b
--- /dev/null
+++ b/apollo-client/src/main/java/com/ctrip/framework/apollo/internals/K8sConfigMapConfigRepository.java
@@ -0,0 +1,314 @@
+/*
+ * Copyright 2022 Apollo Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+package com.ctrip.framework.apollo.internals;
+
+import com.ctrip.framework.apollo.Kubernetes.KubernetesManager;
+import com.ctrip.framework.apollo.build.ApolloInjector;
+import com.ctrip.framework.apollo.core.ConfigConsts;
+import com.ctrip.framework.apollo.core.utils.DeferredLoggerFactory;
+import com.ctrip.framework.apollo.core.utils.StringUtils;
+import com.ctrip.framework.apollo.enums.ConfigSourceType;
+import com.ctrip.framework.apollo.exceptions.ApolloConfigException;
+import com.ctrip.framework.apollo.tracer.Tracer;
+import com.ctrip.framework.apollo.tracer.spi.Transaction;
+import com.ctrip.framework.apollo.util.ConfigUtil;
+import com.ctrip.framework.apollo.util.ExceptionUtil;
+import com.google.common.base.Joiner;
+import com.google.common.base.Preconditions;
+import com.google.gson.Gson;
+import com.google.gson.reflect.TypeToken;
+import org.slf4j.Logger;
+
+import java.io.IOException;
+import java.lang.reflect.Type;
+import java.util.Base64;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Properties;
+
+/**
+ * @author dyx1234
+ */
+public class K8sConfigMapConfigRepository extends AbstractConfigRepository
+ implements RepositoryChangeListener {
+ private static final Logger logger = DeferredLoggerFactory.getLogger(K8sConfigMapConfigRepository.class);
+ private final String namespace;
+ private String configMapName;
+ private String configMapKey;
+ private String configMapNamespace;
+ private final ConfigUtil configUtil;
+ private final KubernetesManager kubernetesManager;
+ private volatile Properties configMapProperties;
+ // 上游数据源
+ private volatile ConfigRepository upstream;
+ private volatile ConfigSourceType sourceType = ConfigSourceType.CONFIGMAP;
+
+ /**
+ * configmapNamespace 用户配的,不配用默认default
+ * configmapName appid
+ * configmap-key cluster+namespace
+ * configmap-value 配置文件信息的json串
+ */
+
+ // TODO Properties和appConfig格式的兼容(go客户端存的格式是封装过的ApolloConfig,不是单纯的配置信息json)
+
+ /**
+ * Constructor
+ *
+ * @param namespace the namespace
+ */
+ public K8sConfigMapConfigRepository(String namespace) {
+ this(namespace, null);
+ }
+
+ public K8sConfigMapConfigRepository(String namespace, ConfigRepository upstream) {
+ this.namespace = namespace;
+ configUtil = ApolloInjector.getInstance(ConfigUtil.class);
+ // 单例模式,客户端只会初始化一个
+ kubernetesManager = ApolloInjector.getInstance(KubernetesManager.class);
+ // 读取,默认为default
+ configMapNamespace = configUtil.getConfigMapNamespace();
+
+ this.setConfigMapKey(configUtil.getCluster(), namespace);
+ this.setConfigMapName(configUtil.getAppId(), false);
+ this.setUpstreamRepository(upstream);
+ }
+
+ void setConfigMapKey(String cluster, String namespace){
+ // TODO 兜底key怎么设计不会冲突(cluster初始化时已经设置了层级)
+ // cluster 就是填写>idc>default,所以不需要额外层级设置了
+ if (StringUtils.isBlank(cluster)){
+ configMapKey = Joiner.on(ConfigConsts.CLUSTER_NAMESPACE_SEPARATOR).join("default", namespace);
+ return;
+ }
+ configMapKey = Joiner.on(ConfigConsts.CLUSTER_NAMESPACE_SEPARATOR).join(cluster, namespace);
+ }
+
+ void setConfigMapName(String appId, boolean syncImmediately){
+ configMapName = appId;
+ this.checkConfigMapName(configMapName);
+ if (syncImmediately) {
+ this.sync();
+ }
+ }
+
+ private void checkConfigMapName(String configMapName) {
+ if (StringUtils.isBlank(configMapName)) {
+ throw new ApolloConfigException("ConfigMap name cannot be null");
+ }
+ // 判断configMap是否存在,若存在直接返回,若不存在尝试创建
+ if(kubernetesManager.checkConfigMapExist(configMapNamespace, configMapName)){
+ return;
+ }
+ // TODO 初步理解这里只生成就可以,后续update事件再写入新值
+
+ Transaction transaction = Tracer.newTransaction("Apollo.ConfigService", "createK8sConfigMap");
+ transaction.addData("configMapName", configMapName);
+ try {
+ kubernetesManager.createConfigMap(configMapNamespace, configMapName, null);
+ transaction.setStatus(Transaction.SUCCESS);
+ } catch (Throwable ex) {
+ Tracer.logEvent("ApolloConfigException", ExceptionUtil.getDetailMessage(ex));
+ transaction.setStatus(ex);
+ throw new ApolloConfigException("Create configmap failed!", ex);
+ } finally {
+ transaction.complete();
+ }
+ }
+
+ @Override
+ public Properties getConfig() {
+ if (configMapProperties == null) {
+ sync();
+ }
+ Properties result = propertiesFactory.getPropertiesInstance();
+ result.putAll(configMapProperties);
+ return result;
+ }
+
+ /**
+ * Update the memory when the configuration center changes
+ * @param upstreamConfigRepository the upstream repo
+ */
+ @Override
+ public void setUpstreamRepository(ConfigRepository upstreamConfigRepository) {
+ // 设置上游数据源
+ if (upstreamConfigRepository == null) {
+ return;
+ }
+ //clear previous listener
+ if (upstream != null) {
+ upstream.removeChangeListener(this);
+ }
+ upstream = upstreamConfigRepository;
+ upstreamConfigRepository.addChangeListener(this);
+ }
+
+ @Override
+ public ConfigSourceType getSourceType() {
+ return sourceType;
+ }
+
+ /**
+ * Sync the configmap
+ */
+ @Override
+ protected void sync() {
+ // 链式恢复,先从上游数据源读取
+ boolean syncFromUpstreamResultSuccess = trySyncFromUpstream();
+
+ if (syncFromUpstreamResultSuccess) {
+ return;
+ }
+
+ Transaction transaction = Tracer.newTransaction("Apollo.ConfigService", "syncK8sConfigMap");
+ Throwable exception = null;
+ try {
+ configMapProperties = loadFromK8sConfigMap();
+ sourceType = ConfigSourceType.CONFIGMAP;
+ transaction.setStatus(Transaction.SUCCESS);
+ } catch (Throwable ex) {
+ Tracer.logEvent("ApolloConfigException", ExceptionUtil.getDetailMessage(ex));
+ transaction.setStatus(ex);
+ exception = ex;
+ throw new ApolloConfigException("Load config from Kubernetes ConfigMap failed!", ex);
+ } finally {
+ transaction.complete();
+ }
+
+ if (configMapProperties == null) {
+ sourceType = ConfigSourceType.NONE;
+ throw new ApolloConfigException(
+ "Load config from Kubernetes ConfigMap failed!", exception);
+ }
+ }
+
+ // 职责明确: manager层进行序列化和解析,把key传进去
+ // repo这里只负责更新内存, Properties和appConfig格式的兼容
+ public Properties loadFromK8sConfigMap() throws IOException {
+ Preconditions.checkNotNull(configMapName, "ConfigMap name cannot be null");
+
+ Properties properties = null;
+ try {
+ // 从ConfigMap获取整个配置信息的JSON字符串
+ String jsonConfig = kubernetesManager.getValueFromConfigMap(configMapNamespace, configUtil.getAppId(), configMapKey);
+ if (jsonConfig == null) {
+ // TODO 待修改,重试访问保底configmap
+ jsonConfig = kubernetesManager.getValueFromConfigMap(configMapNamespace, configMapName, Joiner.on(ConfigConsts.CLUSTER_NAMESPACE_SEPARATOR).join("default", namespace));
+ }
+
+ // 确保获取到的配置信息不为空
+ if (jsonConfig != null) {
+ // 解码Base64编码的JSON字符串
+ jsonConfig = new String(Base64.getDecoder().decode(jsonConfig));
+ }
+
+ // 创建Properties实例
+ properties = propertiesFactory.getPropertiesInstance();
+
+ // 使用Gson将JSON字符串转换为Map对象
+ if (jsonConfig != null && !jsonConfig.isEmpty()) {
+ Gson gson = new Gson();
+ Type type = new TypeToken