/** * Copyright 2005-2016 Red Hat, Inc. * * Red Hat licenses this file to you under the Apache License, version * 2.0 (the "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or * implied. See the License for the specific language governing * permissions and limitations under the License. */ package io.fabric8.karaf.cm; import java.io.StringReader; import java.util.Dictionary; import java.util.Enumeration; import java.util.HashMap; import java.util.Hashtable; import java.util.Map; import java.util.concurrent.atomic.AtomicReference; import io.fabric8.kubernetes.api.model.ConfigMap; import io.fabric8.kubernetes.api.model.ConfigMapList; import io.fabric8.kubernetes.client.KubernetesClient; import io.fabric8.kubernetes.client.KubernetesClientException; import io.fabric8.kubernetes.client.Watch; import io.fabric8.kubernetes.client.Watcher; import io.fabric8.kubernetes.client.utils.Utils; import org.apache.felix.scr.annotations.Activate; import org.apache.felix.scr.annotations.Component; import org.apache.felix.scr.annotations.ConfigurationPolicy; import org.apache.felix.scr.annotations.Deactivate; import org.apache.felix.scr.annotations.Reference; import org.apache.felix.scr.annotations.ReferenceCardinality; import org.apache.felix.scr.annotations.ReferencePolicy; import org.apache.felix.scr.annotations.References; import org.osgi.service.cm.Configuration; import org.osgi.service.cm.ConfigurationAdmin; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import static io.fabric8.karaf.cm.KubernetesConstants.CM_META_KEYS; import static io.fabric8.karaf.cm.KubernetesConstants.FABRIC8_CM_BRIDGE_ENABLED; import static io.fabric8.karaf.cm.KubernetesConstants.FABRIC8_CM_BRIDGE_ENABLED_DEFAULT; import static io.fabric8.karaf.cm.KubernetesConstants.FABRIC8_CONFIG_MERGE; import static io.fabric8.karaf.cm.KubernetesConstants.FABRIC8_CONFIG_MERGE_DEFAULT; import static io.fabric8.karaf.cm.KubernetesConstants.FABRIC8_CONFIG_META; import static io.fabric8.karaf.cm.KubernetesConstants.FABRIC8_CONFIG_META_DEFAULT; import static io.fabric8.karaf.cm.KubernetesConstants.FABRIC8_CONFIG_PID_CFG; import static io.fabric8.karaf.cm.KubernetesConstants.FABRIC8_CONFIG_WATCH; import static io.fabric8.karaf.cm.KubernetesConstants.FABRIC8_CONFIG_WATCH_DEFAULT; import static io.fabric8.karaf.cm.KubernetesConstants.FABRIC8_K8S_META_NAME; import static io.fabric8.karaf.cm.KubernetesConstants.FABRIC8_K8S_META_NAMESPACE; import static io.fabric8.karaf.cm.KubernetesConstants.FABRIC8_K8S_META_RESOURCE_VERSION; import static io.fabric8.karaf.cm.KubernetesConstants.FABRIC8_META_KEYS; import static io.fabric8.karaf.cm.KubernetesConstants.FABRIC8_PID; import static io.fabric8.karaf.cm.KubernetesConstants.FABRIC8_PID_FILTERS; import static io.fabric8.karaf.cm.KubernetesConstants.FABRIC8_PID_LABEL; import static io.fabric8.karaf.cm.KubernetesConstants.FABRIC8_PID_LABEL_DEFAULT; import static io.fabric8.kubernetes.client.utils.Utils.getSystemPropertyOrEnvVar; @Component( immediate = true, policy = ConfigurationPolicy.IGNORE, createPid = false) @References({ @Reference( name = "configAdmin", referenceInterface = ConfigurationAdmin.class, policy = ReferencePolicy.STATIC, cardinality = ReferenceCardinality.MANDATORY_UNARY), @Reference( name = "kubernetesClient", referenceInterface = KubernetesClient.class, policy = ReferencePolicy.STATIC, cardinality = ReferenceCardinality.MANDATORY_UNARY) }) public class KubernetesConfigAdminBridge implements Watcher<ConfigMap> { private static final Logger LOGGER = LoggerFactory.getLogger(KubernetesConfigAdminBridge.class); private final Object lock; private final AtomicReference<ConfigurationAdmin> configAdmin; private final AtomicReference<KubernetesClient> kubernetesClient; private boolean enabled; private String pidLabel; private Map<String, String> filters; private Watch watch; private boolean configMerge; private boolean configMeta; private boolean configWatch; public KubernetesConfigAdminBridge() { this.enabled = FABRIC8_CM_BRIDGE_ENABLED_DEFAULT; this.lock = new Object(); this.configAdmin = new AtomicReference<>(); this.kubernetesClient = new AtomicReference<>(); this.configMerge = FABRIC8_CONFIG_MERGE_DEFAULT; this.configMeta = FABRIC8_CONFIG_META_DEFAULT; this.configWatch = FABRIC8_CONFIG_WATCH_DEFAULT; this.watch = null; this.pidLabel = FABRIC8_PID_LABEL_DEFAULT; this.filters = null; } // *********************** // Lifecycle // *********************** @Activate void activate() { enabled = getSystemPropertyOrEnvVar(FABRIC8_CM_BRIDGE_ENABLED, enabled); pidLabel = getSystemPropertyOrEnvVar(FABRIC8_PID_LABEL, pidLabel); configMerge = getSystemPropertyOrEnvVar(FABRIC8_CONFIG_MERGE, configMerge); configMeta = getSystemPropertyOrEnvVar(FABRIC8_CONFIG_META, configMeta); configWatch = getSystemPropertyOrEnvVar(FABRIC8_CONFIG_WATCH, configWatch); filters = new HashMap<>(); String filterList = getSystemPropertyOrEnvVar(FABRIC8_PID_FILTERS); if (!Utils.isNullOrEmpty(filterList)) { for (String filter : filterList.split(",")) { String[] kv = filter.split("="); if (kv.length == 2) { filters.put(kv[0].trim(), kv[1].trim()); } } } if (enabled) { synchronized (lock) { watchConfigMapList(); ConfigMapList list = getConfigMapList(); if (list != null) { for (ConfigMap map : list.getItems()) { updateConfig(map); } } } } } @Deactivate void deactivate() { if (watch != null) { watch.close(); } } // *********************** // References // *********************** protected void bindConfigAdmin(ConfigurationAdmin service) { this.configAdmin.set(service); } protected void unbindConfigAdmin(ConfigurationAdmin service) { this.configAdmin.compareAndSet(service, null); } protected void bindKubernetesClient(KubernetesClient service) { this.kubernetesClient.set(service); } protected void unbindKubernetesClient(KubernetesClient service) { this.kubernetesClient.compareAndSet(service, null); } // *********************** // Watcher // *********************** @Override public void eventReceived(Action action, ConfigMap map) { synchronized (lock) { switch (action) { case ADDED: case MODIFIED: updateConfig(map); break; case DELETED: case ERROR: deleteConfig(map); break; } } } @Override public void onClose(KubernetesClientException e) { } // ********************** // ConfigAdmin // ********************** private void updateConfig(ConfigMap map) { Long ver = Long.parseLong(map.getMetadata().getResourceVersion()); String pid = map.getMetadata().getLabels().get(pidLabel); String[] p = parsePid(pid); try { final Configuration config = getConfiguration(configAdmin.get(), pid, p[0], p[1]); final Map<String, String> configMapData = map.getData(); if (configMapData == null) { LOGGER.debug("Ignoring configuration pid={}, (empty)", config.getPid()); return; } final Dictionary<String, Object> props = config.getProperties(); final Hashtable<String, Object> configAdmCfg = props != null ? new Hashtable<String, Object>() : null; Hashtable<String, Object> configMapCfg = new Hashtable<>(); /* * If there is a key named as pid + ".cfg" (as the pid file on karaf) * it will be used as source of configuration instead of the content * of the data field. The name of the key can be changed by setting * the key fabric8.config.pid.cfg * * i.e. * apiVersion: v1 * data: * org.ops4j.pax.logging.cfg: |+ * log4j.rootLogger=DEBUG, out */ String pidCfg = configMapData.get(FABRIC8_CONFIG_PID_CFG); if (pidCfg == null) { pidCfg = pid + ".cfg"; } String cfgString = configMapData.get(pidCfg); if (Utils.isNotNullOrEmpty(cfgString)) { java.util.Properties cfg = new java.util.Properties(); cfg.load(new StringReader(cfgString)); for(Map.Entry<Object, Object> entry: cfg.entrySet()) { configMapCfg.put((String)entry.getKey(), entry.getValue()); } } else { for (Map.Entry<String, String> entry : map.getData().entrySet()) { configMapCfg.put(entry.getKey(), entry.getValue()); } } /* * Configure if mete-data should be added to the Config Admin or not */ boolean meta = configMapData.containsKey(FABRIC8_CONFIG_META) ? Boolean.valueOf(configMapData.get(FABRIC8_CONFIG_META)) : configMeta; /* * Configure if ConfigMap data should be merge with ConfigAdmin or it * should override it. */ boolean merge = configMapData.containsKey(FABRIC8_CONFIG_MERGE) ? Boolean.valueOf(configMapData.get(FABRIC8_CONFIG_MERGE)) : configMerge; if (configAdmCfg != null) { Long oldVer = (Long)props.get(FABRIC8_K8S_META_RESOURCE_VERSION); if (oldVer != null && (oldVer >= ver)) { LOGGER.debug("Ignoring configuration pid={}, oldVersion={} newVersion={} (no changes)", config.getPid(), oldVer, ver); return; } for (Enumeration<String> e = props.keys(); e.hasMoreElements();) { String key = e.nextElement(); Object val = props.get(key); configAdmCfg.put(key, val); } } if (shouldUpdate(configAdmCfg, configMapCfg)) { LOGGER.debug("Updating configuration pid={}", config.getPid()); if (meta) { configMapCfg.put(FABRIC8_PID, pid); configMapCfg.put(FABRIC8_K8S_META_RESOURCE_VERSION, ver); configMapCfg.put(FABRIC8_K8S_META_NAME, map.getMetadata().getName()); configMapCfg.put(FABRIC8_K8S_META_NAMESPACE, map.getMetadata().getNamespace()); } if (merge && configAdmCfg != null) { for(Map.Entry<String, Object> entry : configMapCfg.entrySet()) { // Do not override ConfigAdmin meta data if (!CM_META_KEYS.contains(entry.getKey())) { configAdmCfg.put(entry.getKey(), entry.getValue()); } } configMapCfg = configAdmCfg; } config.update(configMapCfg); } else { LOGGER.debug("Ignoring configuration pid={} (no changes)", config.getPid()); } } catch (Exception e) { LOGGER.warn("", e); } } private void deleteConfig(ConfigMap map) { String pid = map.getMetadata().getLabels().get(pidLabel); String[] p = parsePid(pid); try { Map<String, String> configMapData = map.getData(); Configuration config = getConfiguration(configAdmin.get(), pid, p[0], p[1]); if (configMapData != null) { boolean merge = configMapData.containsKey(FABRIC8_CONFIG_MERGE) ? Boolean.valueOf(configMapData.get(FABRIC8_CONFIG_MERGE)) : configMerge; if (!merge) { LOGGER.debug("Delete configuration {}", config.getPid()); config.delete(); } } } catch (Exception e) { LOGGER.warn("", e); } } // *********************** // Helpers // *********************** private String[] parsePid(String pid) { String factoryPid = null; int n = pid.indexOf('-'); if (n > 0) { factoryPid = pid.substring(n + 1); pid = pid.substring(0, n); } return new String[] { pid, factoryPid }; } private ConfigMapList getConfigMapList() { KubernetesClient client = kubernetesClient.get(); return client != null ? client.configMaps().withLabel(pidLabel).withLabels(filters).list() : null; } private void watchConfigMapList() { if (configWatch) { KubernetesClient client = kubernetesClient.get(); if (client != null) { watch = client.configMaps().withLabel(pidLabel).withLabels(filters).watch(this); } else { throw new RuntimeException("KubernetesClient not set"); } } } private Configuration getConfiguration(ConfigurationAdmin configAdmin, String fabric8pid, String pid, String factoryPid) throws Exception { String filter = "(" + FABRIC8_PID + "=" + fabric8pid + ")"; Configuration[] oldConfiguration = configAdmin.listConfigurations(filter); if (oldConfiguration != null && oldConfiguration.length > 0) { return oldConfiguration[0]; } else { return factoryPid != null ? configAdmin.createFactoryConfiguration(pid, null) : configAdmin.getConfiguration(pid, null); } } private boolean shouldUpdate(Hashtable<String, Object> configAdmCfg, Hashtable<String, Object> configMapCfg) { if (configAdmCfg == null) { return true; } for(Map.Entry<String, Object> entry : configMapCfg.entrySet()) { // Do not compare meta data if (FABRIC8_META_KEYS.contains(entry.getKey())) { continue; } Object value = configAdmCfg.get(entry.getKey()); if (value == null) { return true; } if (!value.equals(entry.getValue())) { return true; } } return false; } }