001/* 002 * VM-Operator 003 * Copyright (C) 2025 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 io.kubernetes.client.custom.V1Patch; 022import io.kubernetes.client.openapi.ApiException; 023import io.kubernetes.client.openapi.models.V1Secret; 024import io.kubernetes.client.openapi.models.V1SecretList; 025import io.kubernetes.client.util.Watch.Response; 026import io.kubernetes.client.util.generic.options.ListOptions; 027import io.kubernetes.client.util.generic.options.PatchOptions; 028import java.io.IOException; 029import java.util.logging.Level; 030import static org.jdrupes.vmoperator.common.Constants.APP_NAME; 031import org.jdrupes.vmoperator.common.Constants.DisplaySecret; 032import static org.jdrupes.vmoperator.common.Constants.VM_OP_NAME; 033import org.jdrupes.vmoperator.common.K8sClient; 034import org.jdrupes.vmoperator.common.K8sV1PodStub; 035import org.jdrupes.vmoperator.common.K8sV1SecretStub; 036import org.jdrupes.vmoperator.manager.events.ChannelDictionary; 037import org.jdrupes.vmoperator.manager.events.VmChannel; 038import org.jgrapes.core.Channel; 039 040/** 041 * Watches for changes of display secrets. Updates an artifical attribute 042 * of the pod running the VM in response to force an update of the files 043 * in the pod that reflect the information from the secret. 044 */ 045@SuppressWarnings({ "PMD.DataflowAnomalyAnalysis", "PMD.TooManyStaticImports" }) 046public class DisplaySecretMonitor 047 extends AbstractMonitor<V1Secret, V1SecretList, VmChannel> { 048 049 private final ChannelDictionary<String, VmChannel, ?> channelDictionary; 050 051 /** 052 * Instantiates a new display secrets monitor. 053 * 054 * @param componentChannel the component channel 055 * @param channelDictionary the channel dictionary 056 */ 057 public DisplaySecretMonitor(Channel componentChannel, 058 ChannelDictionary<String, VmChannel, ?> channelDictionary) { 059 super(componentChannel, V1Secret.class, V1SecretList.class); 060 this.channelDictionary = channelDictionary; 061 context(K8sV1SecretStub.CONTEXT); 062 ListOptions options = new ListOptions(); 063 options.setLabelSelector("app.kubernetes.io/name=" + APP_NAME + "," 064 + "app.kubernetes.io/component=" + DisplaySecret.NAME); 065 options(options); 066 } 067 068 @Override 069 protected void prepareMonitoring() throws IOException, ApiException { 070 client(new K8sClient()); 071 } 072 073 @Override 074 protected void handleChange(K8sClient client, Response<V1Secret> change) { 075 String vmName = change.object.getMetadata().getLabels() 076 .get("app.kubernetes.io/instance"); 077 if (vmName == null) { 078 return; 079 } 080 var channel = channelDictionary.channel(vmName).orElse(null); 081 if (channel == null || channel.vmDefinition() == null) { 082 return; 083 } 084 085 try { 086 patchPod(client, change); 087 } catch (ApiException e) { 088 logger.log(Level.WARNING, e, 089 () -> "Cannot patch pod annotations: " + e.getMessage()); 090 } 091 } 092 093 private void patchPod(K8sClient client, Response<V1Secret> change) 094 throws ApiException { 095 // Force update for pod 096 ListOptions listOpts = new ListOptions(); 097 listOpts.setLabelSelector( 098 "app.kubernetes.io/managed-by=" + VM_OP_NAME + "," 099 + "app.kubernetes.io/name=" + APP_NAME + "," 100 + "app.kubernetes.io/instance=" + change.object.getMetadata() 101 .getLabels().get("app.kubernetes.io/instance")); 102 // Get pod, selected by label 103 var pods = K8sV1PodStub.list(client, namespace(), listOpts); 104 105 // If the VM is being created, the pod may not exist yet. 106 if (pods.isEmpty()) { 107 return; 108 } 109 var pod = pods.iterator().next(); 110 111 // Patch pod annotation 112 PatchOptions patchOpts = new PatchOptions(); 113 patchOpts.setFieldManager("kubernetes-java-kubectl-apply"); 114 pod.patch(V1Patch.PATCH_FORMAT_JSON_PATCH, 115 new V1Patch("[{\"op\": \"replace\", \"path\": " 116 + "\"/metadata/annotations/vmrunner.jdrupes.org~1dpVersion\", " 117 + "\"value\": \"" 118 + change.object.getMetadata().getResourceVersion() 119 + "\"}]"), 120 patchOpts); 121 } 122}