|
| 1 | +/* |
| 2 | + * Copyright 2022 Apollo Authors |
| 3 | + * |
| 4 | + * Licensed under the Apache License, Version 2.0 (the "License"); |
| 5 | + * you may not use this file except in compliance with the License. |
| 6 | + * You may obtain a copy of the License at |
| 7 | + * |
| 8 | + * http://www.apache.org/licenses/LICENSE-2.0 |
| 9 | + * |
| 10 | + * Unless required by applicable law or agreed to in writing, software |
| 11 | + * distributed under the License is distributed on an "AS IS" BASIS, |
| 12 | + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 13 | + * See the License for the specific language governing permissions and |
| 14 | + * limitations under the License. |
| 15 | + * |
| 16 | + */ |
| 17 | +package com.ctrip.framework.apollo.internals; |
| 18 | + |
| 19 | +import com.ctrip.framework.apollo.kubernetes.KubernetesManager; |
| 20 | +import com.ctrip.framework.apollo.build.ApolloInjector; |
| 21 | +import com.ctrip.framework.apollo.core.ConfigConsts; |
| 22 | +import com.ctrip.framework.apollo.core.utils.DeferredLoggerFactory; |
| 23 | +import com.ctrip.framework.apollo.core.utils.StringUtils; |
| 24 | +import com.ctrip.framework.apollo.enums.ConfigSourceType; |
| 25 | +import com.ctrip.framework.apollo.exceptions.ApolloConfigException; |
| 26 | +import com.ctrip.framework.apollo.tracer.Tracer; |
| 27 | +import com.ctrip.framework.apollo.tracer.spi.Transaction; |
| 28 | +import com.ctrip.framework.apollo.util.ConfigUtil; |
| 29 | +import com.ctrip.framework.apollo.util.ExceptionUtil; |
| 30 | +import com.ctrip.framework.apollo.util.escape.EscapeUtil; |
| 31 | +import com.google.common.base.Preconditions; |
| 32 | +import com.google.gson.Gson; |
| 33 | +import com.google.gson.reflect.TypeToken; |
| 34 | +import org.slf4j.Logger; |
| 35 | + |
| 36 | +import java.lang.reflect.Type; |
| 37 | +import java.util.HashMap; |
| 38 | +import java.util.Map; |
| 39 | +import java.util.Properties; |
| 40 | + |
| 41 | +/** |
| 42 | + * @author dyx1234 |
| 43 | + */ |
| 44 | +public class K8sConfigMapConfigRepository extends AbstractConfigRepository |
| 45 | + implements RepositoryChangeListener { |
| 46 | + private static final Logger logger = DeferredLoggerFactory.getLogger(K8sConfigMapConfigRepository.class); |
| 47 | + private final String namespace; |
| 48 | + private String configMapName; |
| 49 | + private String configMapKey; |
| 50 | + private final String k8sNamespace; |
| 51 | + private final ConfigUtil configUtil; |
| 52 | + private final KubernetesManager kubernetesManager; |
| 53 | + private volatile Properties configMapProperties; |
| 54 | + private volatile ConfigRepository upstream; |
| 55 | + private volatile ConfigSourceType sourceType = ConfigSourceType.CONFIGMAP; |
| 56 | + private static final Gson GSON = new Gson(); |
| 57 | + |
| 58 | + |
| 59 | + public K8sConfigMapConfigRepository(String namespace, ConfigRepository upstream) { |
| 60 | + this.namespace = namespace; |
| 61 | + configUtil = ApolloInjector.getInstance(ConfigUtil.class); |
| 62 | + kubernetesManager = ApolloInjector.getInstance(KubernetesManager.class); |
| 63 | + k8sNamespace = configUtil.getK8sNamespace(); |
| 64 | + |
| 65 | + this.setConfigMapKey(configUtil.getCluster(), namespace); |
| 66 | + this.setConfigMapName(configUtil.getAppId(), false); |
| 67 | + this.setUpstreamRepository(upstream); |
| 68 | + } |
| 69 | + |
| 70 | + private void setConfigMapKey(String cluster, String namespace) { |
| 71 | + // cluster: User Definition >idc>default |
| 72 | + if (StringUtils.isBlank(cluster)) { |
| 73 | + configMapKey = EscapeUtil.createConfigMapKey("default", namespace); |
| 74 | + return; |
| 75 | + } |
| 76 | + configMapKey = EscapeUtil.createConfigMapKey(cluster, namespace); |
| 77 | + } |
| 78 | + |
| 79 | + private void setConfigMapName(String appId, boolean syncImmediately) { |
| 80 | + Preconditions.checkNotNull(appId, "AppId cannot be null"); |
| 81 | + configMapName = ConfigConsts.APOLLO_CONFIG_CACHE + appId; |
| 82 | + this.checkConfigMapName(configMapName); |
| 83 | + if (syncImmediately) { |
| 84 | + this.sync(); |
| 85 | + } |
| 86 | + } |
| 87 | + |
| 88 | + private void checkConfigMapName(String configMapName) { |
| 89 | + if (StringUtils.isBlank(configMapName)) { |
| 90 | + throw new IllegalArgumentException("ConfigMap name cannot be null"); |
| 91 | + } |
| 92 | + if (kubernetesManager.checkConfigMapExist(k8sNamespace, configMapName)) { |
| 93 | + return; |
| 94 | + } |
| 95 | + // Create an empty configmap, write the new value in the update event |
| 96 | + Transaction transaction = Tracer.newTransaction("Apollo.ConfigService", "createK8sConfigMap"); |
| 97 | + transaction.addData("configMapName", configMapName); |
| 98 | + try { |
| 99 | + kubernetesManager.createConfigMap(k8sNamespace, configMapName, null); |
| 100 | + transaction.setStatus(Transaction.SUCCESS); |
| 101 | + } catch (Throwable ex) { |
| 102 | + Tracer.logEvent("ApolloConfigException", ExceptionUtil.getDetailMessage(ex)); |
| 103 | + transaction.setStatus(ex); |
| 104 | + throw new ApolloConfigException("Create configmap failed!", ex); |
| 105 | + } finally { |
| 106 | + transaction.complete(); |
| 107 | + } |
| 108 | + } |
| 109 | + |
| 110 | + @Override |
| 111 | + public Properties getConfig() { |
| 112 | + if (configMapProperties == null) { |
| 113 | + sync(); |
| 114 | + } |
| 115 | + Properties result = propertiesFactory.getPropertiesInstance(); |
| 116 | + result.putAll(configMapProperties); |
| 117 | + return result; |
| 118 | + } |
| 119 | + |
| 120 | + /** |
| 121 | + * Update the memory when the configuration center changes |
| 122 | + * |
| 123 | + * @param upstreamConfigRepository the upstream repo |
| 124 | + */ |
| 125 | + @Override |
| 126 | + public void setUpstreamRepository(ConfigRepository upstreamConfigRepository) { |
| 127 | + if (upstreamConfigRepository == null) { |
| 128 | + return; |
| 129 | + } |
| 130 | + //clear previous listener |
| 131 | + if (upstream != null) { |
| 132 | + upstream.removeChangeListener(this); |
| 133 | + } |
| 134 | + upstream = upstreamConfigRepository; |
| 135 | + upstreamConfigRepository.addChangeListener(this); |
| 136 | + } |
| 137 | + |
| 138 | + @Override |
| 139 | + public ConfigSourceType getSourceType() { |
| 140 | + return sourceType; |
| 141 | + } |
| 142 | + |
| 143 | + /** |
| 144 | + * Sync the configmap |
| 145 | + */ |
| 146 | + @Override |
| 147 | + protected void sync() { |
| 148 | + // Chain recovery, first read from upstream data source |
| 149 | + boolean syncFromUpstreamResultSuccess = trySyncFromUpstream(); |
| 150 | + |
| 151 | + if (syncFromUpstreamResultSuccess) { |
| 152 | + return; |
| 153 | + } |
| 154 | + |
| 155 | + Transaction transaction = Tracer.newTransaction("Apollo.ConfigService", "syncK8sConfigMap"); |
| 156 | + Throwable exception = null; |
| 157 | + try { |
| 158 | + configMapProperties = loadFromK8sConfigMap(); |
| 159 | + sourceType = ConfigSourceType.CONFIGMAP; |
| 160 | + transaction.setStatus(Transaction.SUCCESS); |
| 161 | + } catch (Throwable ex) { |
| 162 | + Tracer.logEvent("ApolloConfigException", ExceptionUtil.getDetailMessage(ex)); |
| 163 | + transaction.setStatus(ex); |
| 164 | + exception = ex; |
| 165 | + } finally { |
| 166 | + transaction.complete(); |
| 167 | + } |
| 168 | + |
| 169 | + if (configMapProperties == null) { |
| 170 | + sourceType = ConfigSourceType.NONE; |
| 171 | + throw new ApolloConfigException( |
| 172 | + "Load config from Kubernetes ConfigMap failed!", exception); |
| 173 | + } |
| 174 | + } |
| 175 | + |
| 176 | + Properties loadFromK8sConfigMap() { |
| 177 | + Preconditions.checkNotNull(configMapName, "ConfigMap name cannot be null"); |
| 178 | + |
| 179 | + try { |
| 180 | + String jsonConfig = kubernetesManager.getValueFromConfigMap(k8sNamespace, configMapName, configMapKey); |
| 181 | + |
| 182 | + // Convert jsonConfig to properties |
| 183 | + Properties properties = propertiesFactory.getPropertiesInstance(); |
| 184 | + if (jsonConfig != null && !jsonConfig.isEmpty()) { |
| 185 | + Type type = new TypeToken<Map<String, String>>() {}.getType(); |
| 186 | + Map<String, String> configMap = GSON.fromJson(jsonConfig, type); |
| 187 | + configMap.forEach(properties::setProperty); |
| 188 | + } |
| 189 | + return properties; |
| 190 | + } catch (Exception ex) { |
| 191 | + Tracer.logError(ex); |
| 192 | + throw new ApolloConfigException(String |
| 193 | + .format("Load config from Kubernetes ConfigMap %s failed!", configMapName), ex); |
| 194 | + } |
| 195 | + } |
| 196 | + |
| 197 | + private boolean trySyncFromUpstream() { |
| 198 | + if (upstream == null) { |
| 199 | + return false; |
| 200 | + } |
| 201 | + try { |
| 202 | + updateConfigMapProperties(upstream.getConfig(), upstream.getSourceType()); |
| 203 | + return true; |
| 204 | + } catch (Throwable ex) { |
| 205 | + Tracer.logError(ex); |
| 206 | + logger.warn("Sync config from upstream repository {} failed, reason: {}", upstream.getClass(), |
| 207 | + ExceptionUtil.getDetailMessage(ex)); |
| 208 | + } |
| 209 | + return false; |
| 210 | + } |
| 211 | + |
| 212 | + private synchronized void updateConfigMapProperties(Properties newProperties, ConfigSourceType sourceType) { |
| 213 | + this.sourceType = sourceType; |
| 214 | + if (newProperties == null || newProperties.equals(configMapProperties)) { |
| 215 | + return; |
| 216 | + } |
| 217 | + this.configMapProperties = newProperties; |
| 218 | + persistConfigMap(configMapProperties); |
| 219 | + } |
| 220 | + |
| 221 | + /** |
| 222 | + * Update the memory |
| 223 | + * |
| 224 | + * @param namespace the namespace of this repository change |
| 225 | + * @param newProperties the properties after change |
| 226 | + */ |
| 227 | + @Override |
| 228 | + public void onRepositoryChange(String namespace, Properties newProperties) { |
| 229 | + if (newProperties == null || newProperties.equals(configMapProperties)) { |
| 230 | + return; |
| 231 | + } |
| 232 | + Properties newFileProperties = propertiesFactory.getPropertiesInstance(); |
| 233 | + newFileProperties.putAll(newProperties); |
| 234 | + updateConfigMapProperties(newFileProperties, upstream.getSourceType()); |
| 235 | + this.fireRepositoryChange(namespace, newProperties); |
| 236 | + } |
| 237 | + |
| 238 | + void persistConfigMap(Properties properties) { |
| 239 | + Transaction transaction = Tracer.newTransaction("Apollo.ConfigService", "persistK8sConfigMap"); |
| 240 | + transaction.addData("configMapName", configMapName); |
| 241 | + transaction.addData("k8sNamespace", k8sNamespace); |
| 242 | + try { |
| 243 | + // Convert properties to a JSON string using Gson |
| 244 | + String jsonConfig = GSON.toJson(properties); |
| 245 | + Map<String, String> data = new HashMap<>(); |
| 246 | + data.put(configMapKey, jsonConfig); |
| 247 | + |
| 248 | + // update configmap |
| 249 | + kubernetesManager.updateConfigMap(k8sNamespace, configMapName, data); |
| 250 | + transaction.setStatus(Transaction.SUCCESS); |
| 251 | + } catch (Exception ex) { |
| 252 | + ApolloConfigException exception = |
| 253 | + new ApolloConfigException( |
| 254 | + String.format("Persist config to Kubernetes ConfigMap %s failed!", configMapName), ex); |
| 255 | + Tracer.logError(exception); |
| 256 | + transaction.setStatus(exception); |
| 257 | + logger.error("Persist config to Kubernetes ConfigMap failed!", exception); |
| 258 | + } finally { |
| 259 | + transaction.complete(); |
| 260 | + } |
| 261 | + } |
| 262 | + |
| 263 | +} |
0 commit comments