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 com.google.gson.JsonPrimitive; 022import freemarker.template.TemplateException; 023import io.kubernetes.client.openapi.ApiException; 024import io.kubernetes.client.openapi.models.V1ObjectMeta; 025import io.kubernetes.client.openapi.models.V1Secret; 026import io.kubernetes.client.util.generic.options.ListOptions; 027import java.io.IOException; 028import java.security.NoSuchAlgorithmException; 029import java.security.SecureRandom; 030import java.util.Map; 031import java.util.logging.Logger; 032import org.jdrupes.vmoperator.common.K8sV1SecretStub; 033import static org.jdrupes.vmoperator.manager.Constants.APP_NAME; 034import static org.jdrupes.vmoperator.manager.Constants.COMP_DISPLAY_SECRET; 035import static org.jdrupes.vmoperator.manager.Constants.DATA_DISPLAY_PASSWORD; 036import static org.jdrupes.vmoperator.manager.Constants.DATA_PASSWORD_EXPIRY; 037import org.jdrupes.vmoperator.manager.events.VmChannel; 038import org.jdrupes.vmoperator.manager.events.VmDefChanged; 039import org.jdrupes.vmoperator.util.GsonPtr; 040import org.jose4j.base64url.Base64; 041 042/** 043 * Delegee for reconciling the display secret 044 */ 045@SuppressWarnings("PMD.DataflowAnomalyAnalysis") 046/* default */ class DisplaySecretReconciler { 047 048 protected final Logger logger = Logger.getLogger(getClass().getName()); 049 050 /** 051 * Reconcile. 052 * 053 * @param event the event 054 * @param model the model 055 * @param channel the channel 056 * @throws IOException Signals that an I/O exception has occurred. 057 * @throws TemplateException the template exception 058 * @throws ApiException the api exception 059 */ 060 public void reconcile(VmDefChanged event, 061 Map<String, Object> model, VmChannel channel) 062 throws IOException, TemplateException, ApiException { 063 // Secret needed at all? 064 var display = GsonPtr.to(event.vmDefinition().data()).to("spec", "vm", 065 "display"); 066 if (!display.get(JsonPrimitive.class, "spice", "generateSecret") 067 .map(JsonPrimitive::getAsBoolean).orElse(true)) { 068 return; 069 } 070 071 // Check if exists 072 var metadata = event.vmDefinition().getMetadata(); 073 ListOptions options = new ListOptions(); 074 options.setLabelSelector("app.kubernetes.io/name=" + APP_NAME + "," 075 + "app.kubernetes.io/component=" + COMP_DISPLAY_SECRET + "," 076 + "app.kubernetes.io/instance=" + metadata.getName()); 077 var stubs = K8sV1SecretStub.list(channel.client(), 078 metadata.getNamespace(), options); 079 if (!stubs.isEmpty()) { 080 return; 081 } 082 083 // Create secret 084 var secret = new V1Secret(); 085 secret.setMetadata(new V1ObjectMeta().namespace(metadata.getNamespace()) 086 .name(metadata.getName() + "-" + COMP_DISPLAY_SECRET) 087 .putLabelsItem("app.kubernetes.io/name", APP_NAME) 088 .putLabelsItem("app.kubernetes.io/component", COMP_DISPLAY_SECRET) 089 .putLabelsItem("app.kubernetes.io/instance", metadata.getName())); 090 secret.setType("Opaque"); 091 SecureRandom random = null; 092 try { 093 random = SecureRandom.getInstanceStrong(); 094 } catch (NoSuchAlgorithmException e) { // NOPMD 095 // "Every implementation of the Java platform is required 096 // to support at least one strong SecureRandom implementation." 097 } 098 byte[] bytes = new byte[16]; 099 random.nextBytes(bytes); 100 var password = Base64.encode(bytes); 101 secret.setStringData(Map.of(DATA_DISPLAY_PASSWORD, password, 102 DATA_PASSWORD_EXPIRY, "now")); 103 K8sV1SecretStub.create(channel.client(), secret); 104 } 105 106}