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 */ 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. The `from` VM definition is used to determine the 113 * observed generation and the current status. This method is intended 114 * to be called in the function passed to 115 * {@link K8sGenericStub#updateStatus}. 116 * 117 * @param from the VM definition 118 * @param type the condition type 119 * @param state the new state 120 * @param reason the reason for the change 121 * @param message the message 122 * @return the updated status 123 */ 124 protected JsonObject updateCondition(VmDefinition from, String type, 125 boolean state, String reason, String message) { 126 JsonObject status = from.statusJson(); 127 // Avoid redundant updates, as this may be called several times 128 var current = status.getAsJsonArray("conditions").asList().stream() 129 .map(cond -> (JsonObject) cond) 130 .filter(cond -> type.equals(cond.get("type").getAsString())) 131 .findFirst(); 132 var stateUnchanged = current.map(c -> c.get("status").getAsString()) 133 .map("True"::equals).map(s -> s == state).orElse(false); 134 if (stateUnchanged 135 && current.map(c -> c.get("reason").getAsString()) 136 .map(reason::equals).orElse(false) 137 && current.map(c -> c.get("observedGeneration").getAsLong()) 138 .map(from.getMetadata().getGeneration()::equals) 139 .orElse(false)) { 140 return status; 141 } 142 143 // Do update 144 final var condition = new HashMap<>(Map.of("type", type, 145 "status", state ? "True" : "False", 146 "observedGeneration", from.getMetadata().getGeneration(), 147 "reason", reason, 148 "lastTransitionTime", stateUnchanged 149 ? current.get().get("lastTransitionTime").getAsString() 150 : Instant.now().toString())); 151 if (message != null) { 152 condition.put("message", message); 153 } 154 List<Object> toReplace = new ArrayList<>(List.of(condition)); 155 List<Object> newConds 156 = status.getAsJsonArray("conditions").asList().stream() 157 .map(cond -> (JsonObject) cond) 158 .map(cond -> type.equals(cond.get("type").getAsString()) 159 ? toReplace.remove(0) 160 : cond) 161 .collect(Collectors.toCollection(() -> new ArrayList<>())); 162 newConds.addAll(toReplace); 163 status.add("conditions", 164 apiClient.getJSON().getGson().toJsonTree(newConds)); 165 return status; 166 } 167}