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.K8sV1PodStub;
032import org.jdrupes.vmoperator.common.VmDefinitionModel.RequestedVmState;
033import org.jdrupes.vmoperator.manager.events.VmChannel;
034import org.jdrupes.vmoperator.manager.events.VmDefChanged;
035import org.yaml.snakeyaml.LoaderOptions;
036import org.yaml.snakeyaml.Yaml;
037import org.yaml.snakeyaml.constructor.SafeConstructor;
038
039/**
040 * Delegee for reconciling the pod.
041 */
042@SuppressWarnings("PMD.DataflowAnomalyAnalysis")
043/* default */ class PodReconciler {
044
045    protected final Logger logger = Logger.getLogger(getClass().getName());
046    private final Configuration fmConfig;
047
048    /**
049     * Instantiates a new pod reconciler.
050     *
051     * @param fmConfig the fm config
052     */
053    public PodReconciler(Configuration fmConfig) {
054        this.fmConfig = fmConfig;
055    }
056
057    /**
058     * Reconcile the pod.
059     *
060     * @param event the event
061     * @param model the model
062     * @param channel the channel
063     * @throws IOException Signals that an I/O exception has occurred.
064     * @throws TemplateException the template exception
065     * @throws ApiException the api exception
066     */
067    public void reconcile(VmDefChanged event, Map<String, Object> model,
068            VmChannel channel)
069            throws IOException, TemplateException, ApiException {
070        // Don't do anything if stateful set is still in use (pre v3.4)
071        if ((Boolean) model.get("usingSts")) {
072            return;
073        }
074
075        // Get pod stub.
076        var metadata = event.vmDefinition().getMetadata();
077        var podStub = K8sV1PodStub.get(channel.client(),
078            metadata.getNamespace(), metadata.getName());
079
080        // Nothing to do if exists and should be running
081        if (event.vmDefinition().vmState() == RequestedVmState.RUNNING
082            && podStub.model().isPresent()) {
083            return;
084        }
085
086        // Delete if running but should be stopped
087        if (event.vmDefinition().vmState() == RequestedVmState.STOPPED) {
088            if (podStub.model().isPresent()) {
089                podStub.delete();
090            }
091            return;
092        }
093
094        // Create pod. First combine template and data and parse result
095        var fmTemplate = fmConfig.getTemplate("runnerPod.ftl.yaml");
096        StringWriter out = new StringWriter();
097        fmTemplate.process(model, out);
098        // Avoid Yaml.load due to
099        // https://github.com/kubernetes-client/java/issues/2741
100        var podDef = Dynamics.newFromYaml(
101            new Yaml(new SafeConstructor(new LoaderOptions())), out.toString());
102
103        // Do apply changes
104        PatchOptions opts = new PatchOptions();
105        opts.setForce(true);
106        opts.setFieldManager("kubernetes-java-kubectl-apply");
107        if (podStub.patch(V1Patch.PATCH_FORMAT_APPLY_YAML,
108            new V1Patch(channel.client().getJSON().serialize(podDef)), opts)
109            .isEmpty()) {
110            logger.warning(
111                () -> "Could not patch pod for " + podStub.name());
112        }
113    }
114
115}