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