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 */
045public class DisplaySecretMonitor
046        extends AbstractMonitor<V1Secret, V1SecretList, VmChannel> {
047
048    private final ChannelDictionary<String, VmChannel, ?> channelDictionary;
049
050    /**
051     * Instantiates a new display secrets monitor.
052     *
053     * @param componentChannel the component channel
054     * @param channelDictionary the channel dictionary
055     */
056    public DisplaySecretMonitor(Channel componentChannel,
057            ChannelDictionary<String, VmChannel, ?> channelDictionary) {
058        super(componentChannel, V1Secret.class, V1SecretList.class);
059        this.channelDictionary = channelDictionary;
060        context(K8sV1SecretStub.CONTEXT);
061        ListOptions options = new ListOptions();
062        options.setLabelSelector("app.kubernetes.io/name=" + APP_NAME + ","
063            + "app.kubernetes.io/component=" + DisplaySecret.NAME);
064        options(options);
065    }
066
067    @Override
068    protected void prepareMonitoring() throws IOException, ApiException {
069        client(new K8sClient());
070    }
071
072    @Override
073    protected void handleChange(K8sClient client, Response<V1Secret> change) {
074        String vmName = change.object.getMetadata().getLabels()
075            .get("app.kubernetes.io/instance");
076        if (vmName == null) {
077            return;
078        }
079        var channel = channelDictionary.channel(vmName).orElse(null);
080        if (channel == null || channel.vmDefinition() == null) {
081            return;
082        }
083
084        try {
085            patchPod(client, change);
086        } catch (ApiException e) {
087            logger.log(Level.WARNING, e,
088                () -> "Cannot patch pod annotations: " + e.getMessage());
089        }
090    }
091
092    private void patchPod(K8sClient client, Response<V1Secret> change)
093            throws ApiException {
094        // Force update for pod
095        ListOptions listOpts = new ListOptions();
096        listOpts.setLabelSelector(
097            "app.kubernetes.io/managed-by=" + VM_OP_NAME + ","
098                + "app.kubernetes.io/name=" + APP_NAME + ","
099                + "app.kubernetes.io/instance=" + change.object.getMetadata()
100                    .getLabels().get("app.kubernetes.io/instance"));
101        // Get pod, selected by label
102        var pods = K8sV1PodStub.list(client, namespace(), listOpts);
103
104        // If the VM is being created, the pod may not exist yet.
105        if (pods.isEmpty()) {
106            return;
107        }
108        var pod = pods.iterator().next();
109
110        // Patch pod annotation
111        PatchOptions patchOpts = new PatchOptions();
112        patchOpts.setFieldManager("kubernetes-java-kubectl-apply");
113        pod.patch(V1Patch.PATCH_FORMAT_JSON_PATCH,
114            new V1Patch("[{\"op\": \"replace\", \"path\": "
115                + "\"/metadata/annotations/vmrunner.jdrupes.org~1dpVersion\", "
116                + "\"value\": \""
117                + change.object.getMetadata().getResourceVersion()
118                + "\"}]"),
119            patchOpts);
120    }
121}