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.template.Configuration; 022import freemarker.template.TemplateException; 023import io.kubernetes.client.custom.V1Patch; 024import io.kubernetes.client.openapi.ApiClient; 025import io.kubernetes.client.openapi.ApiException; 026import io.kubernetes.client.util.generic.dynamic.DynamicKubernetesApi; 027import io.kubernetes.client.util.generic.dynamic.DynamicKubernetesObject; 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.Map; 034import java.util.logging.Logger; 035import org.jdrupes.vmoperator.common.K8s; 036import static org.jdrupes.vmoperator.manager.Constants.APP_NAME; 037import static org.jdrupes.vmoperator.manager.Constants.VM_OP_NAME; 038import org.jdrupes.vmoperator.manager.events.VmChannel; 039import org.yaml.snakeyaml.LoaderOptions; 040import org.yaml.snakeyaml.Yaml; 041import org.yaml.snakeyaml.constructor.SafeConstructor; 042 043/** 044 * Delegee for reconciling the config map 045 */ 046@SuppressWarnings("PMD.DataflowAnomalyAnalysis") 047/* default */ class ConfigMapReconciler { 048 049 protected final Logger logger = Logger.getLogger(getClass().getName()); 050 private final Configuration fmConfig; 051 052 /** 053 * Instantiates a new config map reconciler. 054 * 055 * @param fmConfig the fm config 056 */ 057 public ConfigMapReconciler(Configuration fmConfig) { 058 this.fmConfig = fmConfig; 059 } 060 061 /** 062 * Reconcile. 063 * 064 * @param model the model 065 * @param channel the channel 066 * @return the dynamic kubernetes object 067 * @throws IOException Signals that an I/O exception has occurred. 068 * @throws TemplateException the template exception 069 * @throws ApiException the api exception 070 */ 071 public DynamicKubernetesObject reconcile(Map<String, Object> model, 072 VmChannel channel) 073 throws IOException, TemplateException, ApiException { 074 // Get API 075 DynamicKubernetesApi cmApi = new DynamicKubernetesApi("", "v1", 076 "configmaps", channel.client()); 077 078 // Combine template and data and parse result 079 var fmTemplate = fmConfig.getTemplate("runnerConfig.ftl.yaml"); 080 StringWriter out = new StringWriter(); 081 fmTemplate.process(model, out); 082 // Avoid Yaml.load due to 083 // https://github.com/kubernetes-client/java/issues/2741 084 var mapDef = Dynamics.newFromYaml( 085 new Yaml(new SafeConstructor(new LoaderOptions())), out.toString()); 086 087 // Apply and maybe force pod update 088 var newState = K8s.apply(cmApi, mapDef, out.toString()); 089 maybeForceUpdate(channel.client(), newState); 090 return newState; 091 } 092 093 /** 094 * Triggers update of config map mounted in pod 095 * See https://ahmet.im/blog/kubernetes-secret-volumes-delay/ 096 * @param client 097 * 098 * @param newCm 099 */ 100 private void maybeForceUpdate(ApiClient client, 101 DynamicKubernetesObject newCm) { 102 ListOptions listOpts = new ListOptions(); 103 listOpts.setLabelSelector( 104 "app.kubernetes.io/managed-by=" + VM_OP_NAME + "," 105 + "app.kubernetes.io/name=" + APP_NAME + "," 106 + "app.kubernetes.io/instance=" + newCm.getMetadata() 107 .getLabels().get("app.kubernetes.io/instance")); 108 // Get pod, selected by label 109 var podApi = new DynamicKubernetesApi("", "v1", "pods", client); 110 var pods = podApi 111 .list(newCm.getMetadata().getNamespace(), listOpts).getObject(); 112 113 // If the VM is being created, the pod may not exist yet. 114 if (pods == null || pods.getItems().isEmpty()) { 115 return; 116 } 117 var pod = pods.getItems().get(0); 118 119 // Patch pod annotation 120 PatchOptions patchOpts = new PatchOptions(); 121 patchOpts.setFieldManager("kubernetes-java-kubectl-apply"); 122 var podMeta = pod.getMetadata(); 123 var res = podApi.patch(podMeta.getNamespace(), podMeta.getName(), 124 V1Patch.PATCH_FORMAT_JSON_PATCH, 125 new V1Patch("[{\"op\": \"replace\", \"path\": " 126 + "\"/metadata/annotations/vmrunner.jdrupes.org~1cmVersion\", " 127 + "\"value\": \"" + newCm.getMetadata().getResourceVersion() 128 + "\"}]"), 129 patchOpts); 130 if (!res.isSuccess()) { 131 logger.warning( 132 () -> "Cannot patch pod annotations: " + res.getStatus()); 133 } 134 } 135 136}