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.JsonPrimitive;
022import freemarker.template.TemplateException;
023import io.kubernetes.client.openapi.ApiException;
024import io.kubernetes.client.openapi.models.V1ObjectMeta;
025import io.kubernetes.client.openapi.models.V1Secret;
026import io.kubernetes.client.util.generic.options.ListOptions;
027import java.io.IOException;
028import java.security.NoSuchAlgorithmException;
029import java.security.SecureRandom;
030import java.util.Map;
031import java.util.logging.Logger;
032import org.jdrupes.vmoperator.common.K8sV1SecretStub;
033import static org.jdrupes.vmoperator.manager.Constants.APP_NAME;
034import static org.jdrupes.vmoperator.manager.Constants.COMP_DISPLAY_SECRET;
035import static org.jdrupes.vmoperator.manager.Constants.DATA_DISPLAY_PASSWORD;
036import static org.jdrupes.vmoperator.manager.Constants.DATA_PASSWORD_EXPIRY;
037import org.jdrupes.vmoperator.manager.events.VmChannel;
038import org.jdrupes.vmoperator.manager.events.VmDefChanged;
039import org.jdrupes.vmoperator.util.GsonPtr;
040import org.jose4j.base64url.Base64;
041
042/**
043 * Delegee for reconciling the display secret
044 */
045@SuppressWarnings("PMD.DataflowAnomalyAnalysis")
046/* default */ class DisplaySecretReconciler {
047
048    protected final Logger logger = Logger.getLogger(getClass().getName());
049
050    /**
051     * Reconcile.
052     *
053     * @param event the event
054     * @param model the model
055     * @param channel the channel
056     * @throws IOException Signals that an I/O exception has occurred.
057     * @throws TemplateException the template exception
058     * @throws ApiException the api exception
059     */
060    public void reconcile(VmDefChanged event,
061            Map<String, Object> model, VmChannel channel)
062            throws IOException, TemplateException, ApiException {
063        // Secret needed at all?
064        var display = GsonPtr.to(event.vmDefinition().data()).to("spec", "vm",
065            "display");
066        if (!display.get(JsonPrimitive.class, "spice", "generateSecret")
067            .map(JsonPrimitive::getAsBoolean).orElse(true)) {
068            return;
069        }
070
071        // Check if exists
072        var metadata = event.vmDefinition().getMetadata();
073        ListOptions options = new ListOptions();
074        options.setLabelSelector("app.kubernetes.io/name=" + APP_NAME + ","
075            + "app.kubernetes.io/component=" + COMP_DISPLAY_SECRET + ","
076            + "app.kubernetes.io/instance=" + metadata.getName());
077        var stubs = K8sV1SecretStub.list(channel.client(),
078            metadata.getNamespace(), options);
079        if (!stubs.isEmpty()) {
080            return;
081        }
082
083        // Create secret
084        var secret = new V1Secret();
085        secret.setMetadata(new V1ObjectMeta().namespace(metadata.getNamespace())
086            .name(metadata.getName() + "-" + COMP_DISPLAY_SECRET)
087            .putLabelsItem("app.kubernetes.io/name", APP_NAME)
088            .putLabelsItem("app.kubernetes.io/component", COMP_DISPLAY_SECRET)
089            .putLabelsItem("app.kubernetes.io/instance", metadata.getName()));
090        secret.setType("Opaque");
091        SecureRandom random = null;
092        try {
093            random = SecureRandom.getInstanceStrong();
094        } catch (NoSuchAlgorithmException e) { // NOPMD
095            // "Every implementation of the Java platform is required
096            // to support at least one strong SecureRandom implementation."
097        }
098        byte[] bytes = new byte[16];
099        random.nextBytes(bytes);
100        var password = Base64.encode(bytes);
101        secret.setStringData(Map.of(DATA_DISPLAY_PASSWORD, password,
102            DATA_PASSWORD_EXPIRY, "now"));
103        K8sV1SecretStub.create(channel.client(), secret);
104    }
105
106}