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