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 freemarker.template.Configuration;
022import freemarker.template.TemplateException;
023import io.kubernetes.client.custom.V1Patch;
024import io.kubernetes.client.openapi.ApiException;
025import io.kubernetes.client.util.generic.dynamic.Dynamics;
026import io.kubernetes.client.util.generic.options.PatchOptions;
027import java.io.IOException;
028import java.io.StringWriter;
029import java.util.Map;
030import java.util.logging.Logger;
031import org.jdrupes.vmoperator.common.K8sV1StatefulSetStub;
032import org.jdrupes.vmoperator.manager.events.VmChannel;
033import org.jdrupes.vmoperator.manager.events.VmDefChanged;
034import org.jdrupes.vmoperator.util.GsonPtr;
035import org.yaml.snakeyaml.LoaderOptions;
036import org.yaml.snakeyaml.Yaml;
037import org.yaml.snakeyaml.constructor.SafeConstructor;
038
039/**
040 * Before version 3.4, the pod running the VM was created by a stateful set.
041 * Starting with version 3.4, this reconciler simply deletes the stateful
042 * set, provided that the VM is not running.
043 */
044@SuppressWarnings("PMD.DataflowAnomalyAnalysis")
045/* default */ class StatefulSetReconciler {
046
047    protected final Logger logger = Logger.getLogger(getClass().getName());
048    private final Configuration fmConfig;
049
050    /**
051     * Instantiates a new stateful set reconciler.
052     *
053     * @param fmConfig the fm config
054     */
055    public StatefulSetReconciler(Configuration fmConfig) {
056        this.fmConfig = fmConfig;
057    }
058
059    /**
060     * Reconcile stateful set.
061     *
062     * @param event the event
063     * @param model the model
064     * @param channel the channel
065     * @throws IOException Signals that an I/O exception has occurred.
066     * @throws TemplateException the template exception
067     * @throws ApiException the api exception
068     */
069    @SuppressWarnings("PMD.AvoidLiteralsInIfCondition")
070    public void reconcile(VmDefChanged event, Map<String, Object> model,
071            VmChannel channel)
072            throws IOException, TemplateException, ApiException {
073        var metadata = event.vmDefinition().getMetadata();
074        model.put("usingSts", false);
075
076        // If exists, delete when not running or supposed to be not running.
077        var stsStub = K8sV1StatefulSetStub.get(channel.client(),
078            metadata.getNamespace(), metadata.getName());
079        if (stsStub.model().isEmpty()) {
080            return;
081        }
082
083        // Stateful set still exists, check if replicas is 0 so we can
084        // delete it.
085        var stsModel = stsStub.model().get();
086        if (stsModel.getSpec().getReplicas() == 0) {
087            stsStub.delete();
088            return;
089        }
090
091        // Cannot yet delete the stateful set.
092        model.put("usingSts", true);
093
094        // Check if VM is supposed to be stopped. If so,
095        // set replicas to 0. This is the first step of the transition,
096        // the stateful set will be deleted when the VM is restarted.
097        var fmTemplate = fmConfig.getTemplate("runnerSts.ftl.yaml");
098        StringWriter out = new StringWriter();
099        fmTemplate.process(model, out);
100        // Avoid Yaml.load due to
101        // https://github.com/kubernetes-client/java/issues/2741
102        var stsDef = Dynamics.newFromYaml(
103            new Yaml(new SafeConstructor(new LoaderOptions())), out.toString());
104        var desired = GsonPtr.to(stsDef.getRaw())
105            .to("spec").getAsInt("replicas").orElse(1);
106        if (desired == 1) {
107            return;
108        }
109
110        // Do apply changes (set replicas to 0)
111        PatchOptions opts = new PatchOptions();
112        opts.setForce(true);
113        opts.setFieldManager("kubernetes-java-kubectl-apply");
114        if (stsStub.patch(V1Patch.PATCH_FORMAT_APPLY_YAML,
115            new V1Patch(channel.client().getJSON().serialize(stsDef)), opts)
116            .isEmpty()) {
117            logger.warning(
118                () -> "Could not patch stateful set for " + stsStub.name());
119        }
120    }
121
122}