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.openapi.ApiException;
024import io.kubernetes.client.util.generic.dynamic.Dynamics;
025import io.kubernetes.client.util.generic.options.ListOptions;
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 static org.jdrupes.vmoperator.common.Constants.APP_NAME;
032import org.jdrupes.vmoperator.common.Constants.DisplaySecret;
033import org.jdrupes.vmoperator.common.K8sClient;
034import org.jdrupes.vmoperator.common.K8sV1PodStub;
035import org.jdrupes.vmoperator.common.K8sV1SecretStub;
036import org.jdrupes.vmoperator.common.VmDefinition;
037import org.jdrupes.vmoperator.common.VmDefinition.RequestedVmState;
038import org.jdrupes.vmoperator.manager.events.VmChannel;
039import org.yaml.snakeyaml.LoaderOptions;
040import org.yaml.snakeyaml.Yaml;
041import org.yaml.snakeyaml.constructor.SafeConstructor;
042
043/**
044 * Delegee for reconciling the pod.
045 */
046@SuppressWarnings("PMD.DataflowAnomalyAnalysis")
047/* default */ class PodReconciler {
048
049    protected final Logger logger = Logger.getLogger(getClass().getName());
050    private final Configuration fmConfig;
051
052    /**
053     * Instantiates a new pod reconciler.
054     *
055     * @param fmConfig the fm config
056     */
057    public PodReconciler(Configuration fmConfig) {
058        this.fmConfig = fmConfig;
059    }
060
061    /**
062     * Reconcile the pod.
063     *
064     * @param vmDef the vm def
065     * @param model the model
066     * @param channel the channel
067     * @param specChanged the spec changed
068     * @throws IOException Signals that an I/O exception has occurred.
069     * @throws TemplateException the template exception
070     * @throws ApiException the api exception
071     */
072    public void reconcile(VmDefinition vmDef, Map<String, Object> model,
073            VmChannel channel, boolean specChanged)
074            throws IOException, TemplateException, ApiException {
075        // Get pod stub.
076        var podStub = K8sV1PodStub.get(channel.client(), vmDef.namespace(),
077            vmDef.name());
078
079        // Nothing to do if exists and should be running
080        if (vmDef.vmState() == RequestedVmState.RUNNING
081            && podStub.model().isPresent()) {
082            return;
083        }
084
085        // Delete if running but should be stopped
086        if (vmDef.vmState() == RequestedVmState.STOPPED) {
087            if (podStub.model().isPresent()) {
088                podStub.delete();
089            }
090            return;
091        }
092
093        // Create pod. First combine template and data and parse result
094        logger.fine(() -> "Create/update pod " + podStub.name());
095        addDisplaySecret(channel.client(), model, vmDef);
096        var fmTemplate = fmConfig.getTemplate("runnerPod.ftl.yaml");
097        StringWriter out = new StringWriter();
098        fmTemplate.process(model, out);
099        // Avoid Yaml.load due to
100        // https://github.com/kubernetes-client/java/issues/2741
101        var podDef = Dynamics.newFromYaml(
102            new Yaml(new SafeConstructor(new LoaderOptions())), out.toString());
103
104        // Do apply changes
105        PatchOptions opts = new PatchOptions();
106        opts.setForce(true);
107        opts.setFieldManager("kubernetes-java-kubectl-apply");
108        if (podStub.apply(podDef).isEmpty()) {
109            logger.warning(
110                () -> "Could not patch pod for " + podStub.name());
111        }
112    }
113
114    private void addDisplaySecret(K8sClient client, Map<String, Object> model,
115            VmDefinition vmDef) throws ApiException {
116        ListOptions options = new ListOptions();
117        options.setLabelSelector("app.kubernetes.io/name=" + APP_NAME + ","
118            + "app.kubernetes.io/component=" + DisplaySecret.NAME + ","
119            + "app.kubernetes.io/instance=" + vmDef.name());
120        var dsStub = K8sV1SecretStub
121            .list(client, vmDef.namespace(), options).stream().findFirst();
122        if (dsStub.isPresent()) {
123            dsStub.get().model().ifPresent(m -> {
124                model.put("displaySecret", m.getMetadata().getName());
125            });
126        }
127    }
128
129}