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.options.PatchOptions;
026import java.io.IOException;
027import java.util.Map;
028import java.util.logging.Logger;
029import org.jdrupes.vmoperator.common.K8sV1StatefulSetStub;
030import org.jdrupes.vmoperator.common.VmDefinition.RequestedVmState;
031import org.jdrupes.vmoperator.manager.events.VmChannel;
032import org.jdrupes.vmoperator.manager.events.VmDefChanged;
033
034/**
035 * Before version 3.4, the pod running the VM was created by a stateful set.
036 * Starting with version 3.4, this reconciler simply deletes the stateful
037 * set, provided that the VM is not running.
038 */
039@SuppressWarnings("PMD.DataflowAnomalyAnalysis")
040/* default */ class StatefulSetReconciler {
041
042    protected final Logger logger = Logger.getLogger(getClass().getName());
043
044    /**
045     * Instantiates a new stateful set reconciler.
046     *
047     * @param fmConfig the fm config
048     */
049    @SuppressWarnings("PMD.UnusedFormalParameter")
050    public StatefulSetReconciler(Configuration fmConfig) {
051        // Nothing to do
052    }
053
054    /**
055     * Reconcile stateful set.
056     *
057     * @param event the event
058     * @param model the model
059     * @param channel the channel
060     * @throws IOException Signals that an I/O exception has occurred.
061     * @throws TemplateException the template exception
062     * @throws ApiException the api exception
063     */
064    @SuppressWarnings("PMD.AvoidLiteralsInIfCondition")
065    public void reconcile(VmDefChanged event, Map<String, Object> model,
066            VmChannel channel)
067            throws IOException, TemplateException, ApiException {
068        model.put("usingSts", false);
069
070        // If exists, delete when not running or supposed to be not running.
071        var stsStub = K8sV1StatefulSetStub.get(channel.client(),
072            event.vmDefinition().namespace(), event.vmDefinition().name());
073        if (stsStub.model().isEmpty()) {
074            return;
075        }
076
077        // Stateful set still exists, check if replicas is 0 so we can
078        // delete it.
079        var stsModel = stsStub.model().get();
080        if (stsModel.getSpec().getReplicas() == 0) {
081            stsStub.delete();
082            return;
083        }
084
085        // Cannot yet delete the stateful set.
086        model.put("usingSts", true);
087
088        // Check if VM is supposed to be stopped. If so,
089        // set replicas to 0. This is the first step of the transition,
090        // the stateful set will be deleted when the VM is restarted.
091        if (event.vmDefinition().vmState() == RequestedVmState.RUNNING) {
092            return;
093        }
094
095        // Do apply changes (set replicas to 0)
096        PatchOptions opts = new PatchOptions();
097        opts.setForce(true);
098        opts.setFieldManager("kubernetes-java-kubectl-apply");
099        if (stsStub.patch(V1Patch.PATCH_FORMAT_JSON_PATCH,
100            new V1Patch("[{\"op\": \"replace\", \"path\": \"/spec/replicas"
101                + "\", \"value\": 0}]"),
102            channel.client().defaultPatchOptions()).isEmpty()) {
103            logger.warning(
104                () -> "Could not patch stateful set for " + stsStub.name());
105        }
106    }
107}