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}