001/* 002 * VM-Operator 003 * Copyright (C) 2023 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.manager; 020 021import com.google.gson.JsonElement; 022import com.google.gson.JsonObject; 023import freemarker.core.ParseException; 024import freemarker.template.Configuration; 025import freemarker.template.MalformedTemplateNameException; 026import freemarker.template.TemplateException; 027import freemarker.template.TemplateNotFoundException; 028import io.kubernetes.client.custom.V1Patch; 029import io.kubernetes.client.openapi.ApiException; 030import io.kubernetes.client.util.generic.dynamic.Dynamics; 031import io.kubernetes.client.util.generic.options.ListOptions; 032import io.kubernetes.client.util.generic.options.PatchOptions; 033import java.io.IOException; 034import java.io.StringWriter; 035import java.util.Map; 036import java.util.Set; 037import java.util.logging.Logger; 038import java.util.stream.Collectors; 039import static org.jdrupes.vmoperator.common.Constants.APP_NAME; 040import static org.jdrupes.vmoperator.common.Constants.VM_OP_NAME; 041import org.jdrupes.vmoperator.common.K8sV1PvcStub; 042import org.jdrupes.vmoperator.manager.events.VmChannel; 043import org.jdrupes.vmoperator.manager.events.VmDefChanged; 044import org.jdrupes.vmoperator.util.GsonPtr; 045import org.yaml.snakeyaml.LoaderOptions; 046import org.yaml.snakeyaml.Yaml; 047import org.yaml.snakeyaml.constructor.SafeConstructor; 048 049/** 050 * Delegee for reconciling the stateful set (effectively the pod). 051 */ 052@SuppressWarnings("PMD.DataflowAnomalyAnalysis") 053/* default */ class PvcReconciler { 054 055 protected final Logger logger = Logger.getLogger(getClass().getName()); 056 private final Configuration fmConfig; 057 058 /** 059 * Instantiates a new pvc reconciler. 060 * 061 * @param fmConfig the fm config 062 */ 063 public PvcReconciler(Configuration fmConfig) { 064 this.fmConfig = fmConfig; 065 } 066 067 /** 068 * Reconcile the PVCs. 069 * 070 * @param event the event 071 * @param model the model 072 * @param channel the channel 073 * @throws IOException Signals that an I/O exception has occurred. 074 * @throws TemplateException the template exception 075 * @throws ApiException the api exception 076 */ 077 @SuppressWarnings("PMD.AvoidDuplicateLiterals") 078 public void reconcile(VmDefChanged event, Map<String, Object> model, 079 VmChannel channel) 080 throws IOException, TemplateException, ApiException { 081 var metadata = event.vmDefinition().getMetadata(); 082 083 // Existing disks 084 ListOptions listOpts = new ListOptions(); 085 listOpts.setLabelSelector( 086 "app.kubernetes.io/managed-by=" + VM_OP_NAME + "," 087 + "app.kubernetes.io/name=" + APP_NAME + "," 088 + "app.kubernetes.io/instance=" + metadata.getName()); 089 var knownDisks = K8sV1PvcStub.list(channel.client(), 090 metadata.getNamespace(), listOpts); 091 var knownPvcs = knownDisks.stream().map(K8sV1PvcStub::name) 092 .collect(Collectors.toSet()); 093 094 // Reconcile runner data pvc 095 reconcileRunnerDataPvc(event, model, channel, knownPvcs); 096 097 // Reconcile pvcs for defined disks 098 var diskDefs = GsonPtr.to((JsonObject) model.get("cr")) 099 .getAsListOf(JsonObject.class, "spec", "vm", "disks"); 100 var diskCounter = 0; 101 for (var diskDef : diskDefs) { 102 if (!diskDef.has("volumeClaimTemplate")) { 103 continue; 104 } 105 var diskName = GsonPtr.to(diskDef) 106 .getAsString("volumeClaimTemplate", "metadata", "name") 107 .map(name -> name + "-disk").orElse("disk-" + diskCounter); 108 diskCounter += 1; 109 diskDef.addProperty("generatedDiskName", diskName); 110 111 // Don't do anything if pvc with old (sts generated) name exists. 112 var stsDiskPvcName = diskName + "-" + metadata.getName() + "-0"; 113 if (knownPvcs.contains(stsDiskPvcName)) { 114 diskDef.addProperty("generatedPvcName", stsDiskPvcName); 115 continue; 116 } 117 118 // Update PVC 119 model.put("disk", diskDef); 120 reconcileRunnerDiskPvc(event, model, channel); 121 } 122 model.remove("disk"); 123 } 124 125 private void reconcileRunnerDataPvc(VmDefChanged event, 126 Map<String, Object> model, VmChannel channel, 127 Set<String> knownPvcs) 128 throws TemplateNotFoundException, MalformedTemplateNameException, 129 ParseException, IOException, TemplateException, ApiException { 130 var metadata = event.vmDefinition().getMetadata(); 131 132 // Look for old (sts generated) name. 133 var stsRunnerDataPvcName 134 = "runner-data" + "-" + metadata.getName() + "-0"; 135 if (knownPvcs.contains(stsRunnerDataPvcName)) { 136 model.put("runnerDataPvcName", stsRunnerDataPvcName); 137 return; 138 } 139 140 // Generate PVC 141 model.put("runnerDataPvcName", metadata.getName() + "-runner-data"); 142 var fmTemplate = fmConfig.getTemplate("runnerDataPvc.ftl.yaml"); 143 StringWriter out = new StringWriter(); 144 fmTemplate.process(model, out); 145 // Avoid Yaml.load due to 146 // https://github.com/kubernetes-client/java/issues/2741 147 var pvcDef = Dynamics.newFromYaml( 148 new Yaml(new SafeConstructor(new LoaderOptions())), out.toString()); 149 150 // Do apply changes 151 var pvcStub = K8sV1PvcStub.get(channel.client(), 152 metadata.getNamespace(), (String) model.get("runnerDataPvcName")); 153 PatchOptions opts = new PatchOptions(); 154 opts.setForce(true); 155 opts.setFieldManager("kubernetes-java-kubectl-apply"); 156 if (pvcStub.patch(V1Patch.PATCH_FORMAT_APPLY_YAML, 157 new V1Patch(channel.client().getJSON().serialize(pvcDef)), opts) 158 .isEmpty()) { 159 logger.warning( 160 () -> "Could not patch pvc for " + pvcStub.name()); 161 } 162 } 163 164 private void reconcileRunnerDiskPvc(VmDefChanged event, 165 Map<String, Object> model, VmChannel channel) 166 throws TemplateNotFoundException, MalformedTemplateNameException, 167 ParseException, IOException, TemplateException, ApiException { 168 var metadata = event.vmDefinition().getMetadata(); 169 170 // Generate PVC 171 var diskDef = GsonPtr.to((JsonElement) model.get("disk")); 172 var pvcName = metadata.getName() + "-" 173 + diskDef.getAsString("generatedDiskName").get(); 174 diskDef.set("generatedPvcName", pvcName); 175 var fmTemplate = fmConfig.getTemplate("runnerDiskPvc.ftl.yaml"); 176 StringWriter out = new StringWriter(); 177 fmTemplate.process(model, out); 178 // Avoid Yaml.load due to 179 // https://github.com/kubernetes-client/java/issues/2741 180 var pvcDef = Dynamics.newFromYaml( 181 new Yaml(new SafeConstructor(new LoaderOptions())), out.toString()); 182 183 // Do apply changes 184 var pvcStub = K8sV1PvcStub.get(channel.client(), 185 metadata.getNamespace(), GsonPtr.to((JsonElement) model.get("disk")) 186 .getAsString("generatedPvcName").get()); 187 PatchOptions opts = new PatchOptions(); 188 opts.setForce(true); 189 opts.setFieldManager("kubernetes-java-kubectl-apply"); 190 if (pvcStub.patch(V1Patch.PATCH_FORMAT_APPLY_YAML, 191 new V1Patch(channel.client().getJSON().serialize(pvcDef)), opts) 192 .isEmpty()) { 193 logger.warning( 194 () -> "Could not patch pvc for " + pvcStub.name()); 195 } 196 } 197}