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}