Skip to content

Commit

Permalink
Delay console opening for pool VMs.
Browse files Browse the repository at this point in the history
  • Loading branch information
mnlipp committed Feb 19, 2025
1 parent c582763 commit 5ad052f
Show file tree
Hide file tree
Showing 6 changed files with 191 additions and 119 deletions.

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
/*
* VM-Operator
* Copyright (C) 2024 Michael N. Lipp
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/

package org.jdrupes.vmoperator.manager.events;

import org.jdrupes.vmoperator.common.VmDefinition;
import org.jgrapes.core.Event;

/**
* Gets the current display secret and optionally updates it.
*/
@SuppressWarnings("PMD.DataClass")
public class PrepareConsole extends Event<String> {

private final VmDefinition vmDef;
private final String user;
private final boolean loginUser;

/**
* Instantiates a new request for the display secret.
* After handling the event, a result of `null` means that
* no password is needed. No result means that the console
* is not accessible.
*
* @param vmDef the vm name
* @param user the requesting user
* @param loginUser login the user
*/
public PrepareConsole(VmDefinition vmDef, String user,
boolean loginUser) {
this.vmDef = vmDef;
this.user = user;
this.loginUser = loginUser;
}

/**
* Instantiates a new request for the display secret.
* After handling the event, a result of `null` means that
* no password is needed. No result means that the console
* is not accessible.
*
* @param vmDef the vm name
* @param user the requesting user
*/
public PrepareConsole(VmDefinition vmDef, String user) {
this(vmDef, user, false);
}

/**
* Gets the vm definition.
*
* @return the vm definition
*/
public VmDefinition vmDefinition() {
return vmDef;
}

/**
* Return the id of the user who has requested the password.
*
* @return the string
*/
public String user() {
return user;
}

/**
* Checks if the user should be logged in before allowing access.
*
* @return the loginUser
*/
public boolean loginUser() {
return loginUser;
}

/**
* Returns `true` if a password is available. May only be called
* when the event is completed. Note that the password returned
* by {@link #password()} may be `null`, indicating that no password
* is needed.
*
* @return true, if successful
*/
public boolean passwordAvailable() {
if (!isDone()) {
throw new IllegalStateException("Event is not done.");
}
return !currentResults().isEmpty();
}

/**
* Return the password. May only be called when the event has been
* completed with a valid result (see {@link #passwordAvailable()}).
*
* @return the password. A value of `null` means that no password
* is required.
*/
public String password() {
if (!isDone() || currentResults().isEmpty()) {
throw new IllegalStateException("Event is not done.");
}
return currentResults().get(0);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@
import static org.jdrupes.vmoperator.manager.Constants.DATA_DISPLAY_PASSWORD;
import static org.jdrupes.vmoperator.manager.Constants.DATA_PASSWORD_EXPIRY;
import org.jdrupes.vmoperator.manager.events.ChannelDictionary;
import org.jdrupes.vmoperator.manager.events.GetDisplayPassword;
import org.jdrupes.vmoperator.manager.events.PrepareConsole;
import org.jdrupes.vmoperator.manager.events.VmChannel;
import org.jdrupes.vmoperator.manager.events.VmDefChanged;
import org.jgrapes.core.Channel;
Expand All @@ -72,7 +72,7 @@ public class DisplaySecretMonitor
extends AbstractMonitor<V1Secret, V1SecretList, VmChannel> {

private int passwordValidity = 10;
private final List<PendingGet> pendingGets
private final List<PendingGet> pendingPrepares
= Collections.synchronizedList(new LinkedList<>());
private final ChannelDictionary<String, VmChannel, ?> channelDictionary;

Expand Down Expand Up @@ -178,49 +178,59 @@ private void patchPod(K8sClient client, Response<V1Secret> change)
*/
@Handler
@SuppressWarnings("PMD.StringInstantiation")
public void onGetDisplaySecrets(GetDisplayPassword event, VmChannel channel)
public void onPrepareConsole(PrepareConsole event, VmChannel channel)
throws ApiException {
// Update console user in status
var vmStub = VmDefinitionStub.get(client(),
new GroupVersionKind(VM_OP_GROUP, "", VM_OP_KIND_VM),
event.vmDefinition().namespace(), event.vmDefinition().name());
vmStub.updateStatus(from -> {
var optVmDef = vmStub.updateStatus(from -> {
JsonObject status = from.statusJson();
status.addProperty("consoleUser", event.user());
return status;
});
if (optVmDef.isEmpty()) {
return;
}
var vmDef = optVmDef.get();

// Check if access is possible
if (event.loginUser()
? !vmDef.conditionStatus("Booted").orElse(false)
: !vmDef.conditionStatus("Running").orElse(false)) {
return;
}

// Look for secret
ListOptions options = new ListOptions();
options.setLabelSelector("app.kubernetes.io/name=" + APP_NAME + ","
+ "app.kubernetes.io/component=" + COMP_DISPLAY_SECRET + ","
+ "app.kubernetes.io/instance="
+ event.vmDefinition().metadata().getName());
var stubs = K8sV1SecretStub.list(client(),
event.vmDefinition().namespace(), options);
+ "app.kubernetes.io/instance=" + vmDef.name());
var stubs = K8sV1SecretStub.list(client(), vmDef.namespace(), options);
if (stubs.isEmpty()) {
// No secret means no password for this VM wanted
event.setResult(null);
return;
}
var stub = stubs.iterator().next();

// Check validity
var model = stub.model().get();
var secret = stub.model().get();
@SuppressWarnings("PMD.StringInstantiation")
var expiry = Optional.ofNullable(model.getData()
var expiry = Optional.ofNullable(secret.getData()
.get(DATA_PASSWORD_EXPIRY)).map(b -> new String(b)).orElse(null);
if (model.getData().get(DATA_DISPLAY_PASSWORD) != null
if (secret.getData().get(DATA_DISPLAY_PASSWORD) != null
&& stillValid(expiry)) {
// Fixed secret, don't touch
event.setResult(
new String(model.getData().get(DATA_DISPLAY_PASSWORD)));
new String(secret.getData().get(DATA_DISPLAY_PASSWORD)));
return;
}
updatePassword(stub, event);
}

@SuppressWarnings("PMD.StringInstantiation")
private void updatePassword(K8sV1SecretStub stub, GetDisplayPassword event)
private void updatePassword(K8sV1SecretStub stub, PrepareConsole event)
throws ApiException {
SecureRandom random = null;
try {
Expand All @@ -242,9 +252,9 @@ private void updatePassword(K8sV1SecretStub stub, GetDisplayPassword event)
var pending = new PendingGet(event,
event.vmDefinition().displayPasswordSerial().orElse(0L) + 1,
new CompletionLock(event, 1500));
pendingGets.add(pending);
pendingPrepares.add(pending);
Event.onCompletion(event, e -> {
pendingGets.remove(pending);
pendingPrepares.remove(pending);
});

// Update, will (eventually) trigger confirmation
Expand Down Expand Up @@ -273,9 +283,9 @@ private boolean stillValid(String expiry) {
@Handler
@SuppressWarnings("PMD.AvoidSynchronizedStatement")
public void onVmDefChanged(VmDefChanged event, Channel channel) {
synchronized (pendingGets) {
synchronized (pendingPrepares) {
String vmName = event.vmDefinition().name();
for (var pending : pendingGets) {
for (var pending : pendingPrepares) {
if (pending.event.vmDefinition().name().equals(vmName)
&& event.vmDefinition().displayPasswordSerial()
.map(s -> s >= pending.expectedSerial).orElse(false)) {
Expand All @@ -293,7 +303,7 @@ public void onVmDefChanged(VmDefChanged event, Channel channel) {
*/
@SuppressWarnings("PMD.DataClass")
private static class PendingGet {
public final GetDisplayPassword event;
public final PrepareConsole event;
public final long expectedSerial;
public final CompletionLock lock;

Expand All @@ -303,7 +313,7 @@ private static class PendingGet {
* @param event the event
* @param expectedSerial the expected serial
*/
public PendingGet(GetDisplayPassword event, long expectedSerial,
public PendingGet(PrepareConsole event, long expectedSerial,
CompletionLock lock) {
super();
this.event = event;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,11 +49,11 @@
import org.jdrupes.vmoperator.common.VmDefinition.Permission;
import org.jdrupes.vmoperator.common.VmPool;
import org.jdrupes.vmoperator.manager.events.AssignVm;
import org.jdrupes.vmoperator.manager.events.GetDisplayPassword;
import org.jdrupes.vmoperator.manager.events.GetPools;
import org.jdrupes.vmoperator.manager.events.GetVms;
import org.jdrupes.vmoperator.manager.events.GetVms.VmData;
import org.jdrupes.vmoperator.manager.events.ModifyVm;
import org.jdrupes.vmoperator.manager.events.PrepareConsole;
import org.jdrupes.vmoperator.manager.events.ResetVm;
import org.jdrupes.vmoperator.manager.events.VmChannel;
import org.jdrupes.vmoperator.manager.events.VmDefChanged;
Expand Down Expand Up @@ -808,18 +808,23 @@ private void openConsole(ConsoleConnection channel, ResourceModel model,
Map.of("autoClose", 5_000, "type", "Warning")));
return;
}
var pwQuery = Event.onCompletion(new GetDisplayPassword(vmDef, user),
e -> {
vmDef.extra()
.map(xtra -> xtra.connectionFile(e.password().orElse(null),
preferredIpVersion, deleteConnectionFile))
.ifPresent(
cf -> channel.respond(new NotifyConletView(type(),
model.getConletId(), "openConsole", cf)));
});
var pwQuery = Event.onCompletion(new PrepareConsole(vmDef, user,
model.mode() == ResourceModel.Mode.POOL),
e -> gotPassword(channel, model, vmDef, e));
fire(pwQuery, vmChannel);
}

private void gotPassword(ConsoleConnection channel, ResourceModel model,
VmDefinition vmDef, PrepareConsole event) {
if (!event.passwordAvailable()) {
return;
}
vmDef.extra().map(xtra -> xtra.connectionFile(event.password(),
preferredIpVersion, deleteConnectionFile))
.ifPresent(cf -> channel.respond(new NotifyConletView(type(),
model.getConletId(), "openConsole", cf)));
}

@SuppressWarnings({ "PMD.AvoidLiteralsInIfCondition",
"PMD.UseLocaleWithCaseConversions" })
private void selectResource(NotifyConletModel event,
Expand Down
Loading

0 comments on commit 5ad052f

Please sign in to comment.