001/* 002 * VM-Operator 003 * Copyright (C) 2023 Michael N. Lipp 004 * 005 * This program is free software: you can redistribute it and/or modify 006 * it under the terms of the GNU Affero General Public License as 007 * published by the Free Software Foundation, either version 3 of the 008 * License, or (at your option) any later version. 009 * 010 * This program is distributed in the hope that it will be useful, 011 * but WITHOUT ANY WARRANTY; without even the implied warranty of 012 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 013 * GNU Affero General Public License for more details. 014 * 015 * You should have received a copy of the GNU Affero General Public License 016 * along with this program. If not, see <https://www.gnu.org/licenses/>. 017 */ 018 019package org.jdrupes.vmoperator.manager; 020 021import com.google.gson.JsonObject; 022import freemarker.template.Configuration; 023import freemarker.template.TemplateException; 024import io.kubernetes.client.openapi.ApiException; 025import io.kubernetes.client.openapi.models.V1APIService; 026import io.kubernetes.client.openapi.models.V1ObjectMeta; 027import io.kubernetes.client.util.generic.dynamic.DynamicKubernetesApi; 028import io.kubernetes.client.util.generic.dynamic.DynamicKubernetesObject; 029import io.kubernetes.client.util.generic.dynamic.Dynamics; 030import java.io.IOException; 031import java.io.StringWriter; 032import java.util.Map; 033import java.util.Optional; 034import java.util.logging.Logger; 035import org.jdrupes.vmoperator.common.K8s; 036import org.jdrupes.vmoperator.common.K8sDynamicModel; 037import org.jdrupes.vmoperator.manager.events.VmChannel; 038import org.jdrupes.vmoperator.manager.events.VmDefChanged; 039import org.jdrupes.vmoperator.util.GsonPtr; 040import org.yaml.snakeyaml.LoaderOptions; 041import org.yaml.snakeyaml.Yaml; 042import org.yaml.snakeyaml.constructor.SafeConstructor; 043 044/** 045 * Delegee for reconciling the service 046 */ 047@SuppressWarnings("PMD.DataflowAnomalyAnalysis") 048/* default */ class LoadBalancerReconciler { 049 050 private static final String LOAD_BALANCER_SERVICE = "loadBalancerService"; 051 private static final String METADATA 052 = V1APIService.SERIALIZED_NAME_METADATA; 053 private static final String LABELS = V1ObjectMeta.SERIALIZED_NAME_LABELS; 054 private static final String ANNOTATIONS 055 = V1ObjectMeta.SERIALIZED_NAME_ANNOTATIONS; 056 protected final Logger logger = Logger.getLogger(getClass().getName()); 057 private final Configuration fmConfig; 058 059 /** 060 * Instantiates a new service reconciler. 061 * 062 * @param fmConfig the fm config 063 */ 064 public LoadBalancerReconciler(Configuration fmConfig) { 065 this.fmConfig = fmConfig; 066 } 067 068 /** 069 * Reconcile. 070 * 071 * @param event the event 072 * @param model the model 073 * @param channel the channel 074 * @throws IOException Signals that an I/O exception has occurred. 075 * @throws TemplateException the template exception 076 * @throws ApiException the api exception 077 */ 078 public void reconcile(VmDefChanged event, 079 Map<String, Object> model, VmChannel channel) 080 throws IOException, TemplateException, ApiException { 081 // Check if to be generated 082 @SuppressWarnings({ "PMD.AvoidDuplicateLiterals", "unchecked" }) 083 var lbsDef = Optional.of(model) 084 .map(m -> (Map<String, Object>) m.get("reconciler")) 085 .map(c -> c.get(LOAD_BALANCER_SERVICE)).orElse(Boolean.FALSE); 086 if (!(lbsDef instanceof Map) && !(lbsDef instanceof Boolean)) { 087 logger.warning(() -> "\"" + LOAD_BALANCER_SERVICE 088 + "\" in configuration must be boolean or mapping but is " 089 + lbsDef.getClass() + "."); 090 return; 091 } 092 if (lbsDef instanceof Boolean isOn && !isOn) { 093 return; 094 } 095 JsonObject cfgMeta = new JsonObject(); 096 if (lbsDef instanceof Map) { 097 var json = channel.client().getJSON(); 098 cfgMeta 099 = json.deserialize(json.serialize(lbsDef), JsonObject.class); 100 } 101 102 // Combine template and data and parse result 103 var fmTemplate = fmConfig.getTemplate("runnerLoadBalancer.ftl.yaml"); 104 StringWriter out = new StringWriter(); 105 fmTemplate.process(model, out); 106 // Avoid Yaml.load due to 107 // https://github.com/kubernetes-client/java/issues/2741 108 var svcDef = Dynamics.newFromYaml( 109 new Yaml(new SafeConstructor(new LoaderOptions())), out.toString()); 110 mergeMetadata(svcDef, cfgMeta, event.vmDefinition()); 111 112 // Apply 113 DynamicKubernetesApi svcApi = new DynamicKubernetesApi("", "v1", 114 "services", channel.client()); 115 K8s.apply(svcApi, svcDef, svcDef.getRaw().toString()); 116 } 117 118 private void mergeMetadata(DynamicKubernetesObject svcDef, 119 JsonObject cfgMeta, K8sDynamicModel vmDefinition) { 120 // Get metadata from VM definition 121 var vmMeta = GsonPtr.to(vmDefinition.data()).to("spec") 122 .get(JsonObject.class, LOAD_BALANCER_SERVICE) 123 .map(JsonObject::deepCopy).orElseGet(() -> new JsonObject()); 124 125 // Merge Data from VM definition into config data 126 mergeReplace(GsonPtr.to(cfgMeta).to(LABELS).get(JsonObject.class), 127 GsonPtr.to(vmMeta).to(LABELS).get(JsonObject.class)); 128 mergeReplace( 129 GsonPtr.to(cfgMeta).to(ANNOTATIONS).get(JsonObject.class), 130 GsonPtr.to(vmMeta).to(ANNOTATIONS).get(JsonObject.class)); 131 132 // Merge additional data into service definition 133 var svcMeta = GsonPtr.to(svcDef.getRaw()).to(METADATA); 134 mergeIfAbsent(svcMeta.to(LABELS).get(JsonObject.class), 135 GsonPtr.to(cfgMeta).to(LABELS).get(JsonObject.class)); 136 mergeIfAbsent(svcMeta.to(ANNOTATIONS).get(JsonObject.class), 137 GsonPtr.to(cfgMeta).to(ANNOTATIONS).get(JsonObject.class)); 138 } 139 140 private void mergeReplace(JsonObject dest, JsonObject src) { 141 for (var e : src.entrySet()) { 142 if (e.getValue().isJsonNull()) { 143 dest.remove(e.getKey()); 144 continue; 145 } 146 dest.add(e.getKey(), e.getValue()); 147 } 148 } 149 150 private void mergeIfAbsent(JsonObject dest, JsonObject src) { 151 for (var e : src.entrySet()) { 152 if (dest.has(e.getKey())) { 153 continue; 154 } 155 dest.add(e.getKey(), e.getValue()); 156 } 157 } 158 159}