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.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 vmDef = event.vmDefinition(); 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=" + vmDef.name()); 089 var knownDisks = K8sV1PvcStub.list(channel.client(), 090 vmDef.namespace(), 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 = vmDef.<List<Map<String, Object>>> fromVm("disks") 099 .orElse(List.of()); 100 var diskCounter = 0; 101 for (var diskDef : diskDefs) { 102 if (!diskDef.containsKey("volumeClaimTemplate")) { 103 continue; 104 } 105 var diskName = DataPath.get(diskDef, "volumeClaimTemplate", 106 "metadata", "name").map(name -> name + "-disk") 107 .orElse("disk-" + diskCounter); 108 diskCounter += 1; 109 diskDef.put("generatedDiskName", diskName); 110 111 // Don't do anything if pvc with old (sts generated) name exists. 112 var stsDiskPvcName = diskName + "-" + vmDef.name() + "-0"; 113 if (knownPvcs.contains(stsDiskPvcName)) { 114 diskDef.put("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 vmDef = event.vmDefinition(); 131 132 // Look for old (sts generated) name. 133 var stsRunnerDataPvcName 134 = "runner-data" + "-" + vmDef.name() + "-0"; 135 if (knownPvcs.contains(stsRunnerDataPvcName)) { 136 model.put("runnerDataPvcName", stsRunnerDataPvcName); 137 return; 138 } 139 140 // Generate PVC 141 model.put("runnerDataPvcName", vmDef.name() + "-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 vmDef.namespace(), (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 vmDef = event.vmDefinition(); 169 170 // Generate PVC 171 @SuppressWarnings("unchecked") 172 var diskDef = (Map<String, Object>) model.get("disk"); 173 var pvcName = vmDef.name() + "-" + diskDef.get("generatedDiskName"); 174 diskDef.put("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 // Apply changes 184 var pvcStub 185 = K8sV1PvcStub.get(channel.client(), vmDef.namespace(), pvcName); 186 var pvc = pvcStub.model(); 187 if (pvc.isEmpty() 188 || !"Bound".equals(pvc.get().getStatus().getPhase())) { 189 // Does not exist or isn't bound, use apply 190 PatchOptions opts = new PatchOptions(); 191 opts.setForce(true); 192 opts.setFieldManager("kubernetes-java-kubectl-apply"); 193 if (pvcStub.patch(V1Patch.PATCH_FORMAT_APPLY_YAML, 194 new V1Patch(channel.client().getJSON().serialize(pvcDef)), opts) 195 .isEmpty()) { 196 logger.warning( 197 () -> "Could not patch pvc for " + pvcStub.name()); 198 } 199 return; 200 } 201 202 // If bound, use json merge, omitting immutable fields 203 var spec = GsonPtr.to(pvcDef.getRaw()).to("spec"); 204 spec.removeExcept("volumeAttributesClassName", "resources"); 205 spec.get("resources").ifPresent(p -> p.removeExcept("requests")); 206 PatchOptions opts = new PatchOptions(); 207 opts.setFieldManager("kubernetes-java-kubectl-apply"); 208 if (pvcStub.patch(V1Patch.PATCH_FORMAT_JSON_MERGE_PATCH, 209 new V1Patch(channel.client().getJSON().serialize(pvcDef)), opts) 210 .isEmpty()) { 211 logger.warning( 212 () -> "Could not patch pvc for " + pvcStub.name()); 213 } 214 } 215}