001/*
002 * VM-Operator
003 * Copyright (C) 2024 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.runner.qemu;
020
021import com.google.gson.JsonObject;
022import java.io.IOException;
023import java.nio.file.Files;
024import java.nio.file.Path;
025import java.time.Instant;
026import java.util.ArrayList;
027import java.util.HashMap;
028import java.util.List;
029import java.util.Map;
030import java.util.Optional;
031import java.util.logging.Level;
032import java.util.stream.Collectors;
033import org.jdrupes.vmoperator.common.K8sClient;
034import org.jdrupes.vmoperator.common.K8sGenericStub;
035import org.jdrupes.vmoperator.common.VmDefinition;
036import org.jdrupes.vmoperator.runner.qemu.events.Exit;
037import org.jgrapes.core.Channel;
038import org.jgrapes.core.Component;
039import org.jgrapes.core.annotation.Handler;
040import org.jgrapes.util.events.ConfigurationUpdate;
041import org.jgrapes.util.events.InitialConfiguration;
042
043/**
044 * Updates the CR status.
045 */
046@SuppressWarnings("PMD.DataflowAnomalyAnalysis")
047public class VmDefUpdater extends Component {
048
049    protected String namespace;
050    protected String vmName;
051    protected K8sClient apiClient;
052
053    /**
054     * Instantiates a new status updater.
055     *
056     * @param componentChannel the component channel
057     * @throws IOException 
058     */
059    @SuppressWarnings("PMD.ConstructorCallsOverridableMethod")
060    public VmDefUpdater(Channel componentChannel) {
061        super(componentChannel);
062        if (apiClient == null) {
063            try {
064                apiClient = new K8sClient();
065                io.kubernetes.client.openapi.Configuration
066                    .setDefaultApiClient(apiClient);
067            } catch (IOException e) {
068                logger.log(Level.SEVERE, e,
069                    () -> "Cannot access events API, terminating.");
070                fire(new Exit(1));
071            }
072        }
073    }
074
075    /**
076     * On configuration update.
077     *
078     * @param event the event
079     */
080    @Handler
081    @SuppressWarnings("unchecked")
082    public void onConfigurationUpdate(ConfigurationUpdate event) {
083        event.structured("/Runner").ifPresent(c -> {
084            if (event instanceof InitialConfiguration) {
085                namespace = (String) c.get("namespace");
086                updateNamespace();
087                vmName = Optional.ofNullable((Map<String, String>) c.get("vm"))
088                    .map(vm -> vm.get("name")).orElse(null);
089            }
090        });
091    }
092
093    private void updateNamespace() {
094        if (namespace == null) {
095            var path = Path
096                .of("/var/run/secrets/kubernetes.io/serviceaccount/namespace");
097            if (Files.isReadable(path)) {
098                try {
099                    namespace = Files.lines(path).findFirst().orElse(null);
100                } catch (IOException e) {
101                    logger.log(Level.WARNING, e,
102                        () -> "Cannot read namespace.");
103                }
104            }
105        }
106        if (namespace == null) {
107            logger.warning(() -> "Namespace is unknown, some functions"
108                + " won't be available.");
109        }
110    }
111
112    /**
113     * Update condition. The `from` VM definition is used to determine the
114     * observed generation and the current status. This method is intended
115     * to be called in the function passed to
116     * {@link K8sGenericStub#updateStatus}.
117     *
118     * @param from the VM definition
119     * @param type the condition type
120     * @param state the new state
121     * @param reason the reason for the change
122     * @param message the message
123     * @return the updated status
124     */
125    protected JsonObject updateCondition(VmDefinition from, String type,
126            boolean state, String reason, String message) {
127        JsonObject status = from.statusJson();
128        // Optimize, as we can get this several times
129        var current = status.getAsJsonArray("conditions").asList().stream()
130            .map(cond -> (JsonObject) cond)
131            .filter(cond -> type.equals(cond.get("type").getAsString()))
132            .findFirst()
133            .map(cond -> "True".equals(cond.get("status").getAsString()));
134        if (current.isPresent() && current.get() == state) {
135            return status;
136        }
137
138        // Do update
139        final var condition = new HashMap<>(Map.of("type", type,
140            "status", state ? "True" : "False",
141            "observedGeneration", from.getMetadata().getGeneration(),
142            "reason", reason,
143            "lastTransitionTime", Instant.now().toString()));
144        if (message != null) {
145            condition.put("message", message);
146        }
147        List<Object> toReplace = new ArrayList<>(List.of(condition));
148        List<Object> newConds
149            = status.getAsJsonArray("conditions").asList().stream()
150                .map(cond -> (JsonObject) cond)
151                .map(cond -> type.equals(cond.get("type").getAsString())
152                    ? toReplace.remove(0)
153                    : cond)
154                .collect(Collectors.toCollection(() -> new ArrayList<>()));
155        newConds.addAll(toReplace);
156        status.add("conditions",
157            apiClient.getJSON().getGson().toJsonTree(newConds));
158        return status;
159    }
160}