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}