Merge branch 'prep/v4.0.0' into testing

This commit is contained in:
Michael Lipp 2025-02-02 13:59:44 +01:00
commit aea8b9540e
20 changed files with 436 additions and 418 deletions

View file

@ -102,7 +102,7 @@ public class K8sDynamicModel implements KubernetesObject {
* *
* @return the JSON object describing the status * @return the JSON object describing the status
*/ */
public JsonObject status() { public JsonObject statusJson() {
return data.getAsJsonObject("status"); return data.getAsJsonObject("status");
} }

View file

@ -193,42 +193,41 @@ public class K8sGenericStub<O extends KubernetesObject,
} }
/** /**
* Updates the object's status, retrying for the given number of times * Updates the object's status.
* if the update fails due to a conflict.
* *
* @param object the current state of the object (passed to `status`) * @param object the current state of the object (passed to `status`)
* @param status function that returns the new status * @param status function that returns the new status
* @param retries the retries * @return the updated model or empty if the object was not found
* @return the updated model or empty if not successful
* @throws ApiException the api exception * @throws ApiException the api exception
*/ */
@SuppressWarnings("PMD.AssignmentInOperand") @SuppressWarnings("PMD.AssignmentInOperand")
public Optional<O> updateStatus(O object, public Optional<O> updateStatus(O object, Function<O, Object> status)
Function<O, Object> status, int retries) throws ApiException { throws ApiException {
while (true) { return K8s.optional(api.updateStatus(object, status));
try {
return K8s.optional(api.updateStatus(object, status));
} catch (ApiException e) {
if (HttpURLConnection.HTTP_CONFLICT != e.getCode()
|| retries-- <= 0) {
throw e;
}
}
}
} }
/** /**
* Updates the object's status, retrying up to 16 times if there * Gets the object and updates the status. In case of conflict, retries
* is a conflict. * up to `retries` times.
* *
* @param object the current state of the object (passed to `status`) * @param status the status
* @param status function that returns the new status * @param retries the retries in case of conflict
* @return the updated model or empty if not successful * @return the updated model or empty if the object was not found
* @throws ApiException the api exception * @throws ApiException the api exception
*/ */
public Optional<O> updateStatus(O object, @SuppressWarnings({ "PMD.AssignmentInOperand", "PMD.UnusedAssignment" })
Function<O, Object> status) throws ApiException { public Optional<O> updateStatus(Function<O, Object> status, int retries)
return updateStatus(object, status, 16); throws ApiException {
try {
return updateStatus(api.get(namespace, name).throwsApiException()
.getObject(), status);
} catch (ApiException e) {
if (HttpURLConnection.HTTP_CONFLICT != e.getCode()
|| retries-- <= 0) {
throw e;
}
}
return Optional.empty();
} }
/** /**
@ -241,8 +240,7 @@ public class K8sGenericStub<O extends KubernetesObject,
*/ */
public Optional<O> updateStatus(Function<O, Object> status) public Optional<O> updateStatus(Function<O, Object> status)
throws ApiException { throws ApiException {
return updateStatus( return updateStatus(status, 16);
api.get(namespace, name).throwsApiException().getObject(), status);
} }
/** /**

View file

@ -20,11 +20,10 @@ package org.jdrupes.vmoperator.common;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import com.google.gson.Gson;
import com.google.gson.JsonObject;
import io.kubernetes.client.openapi.JSON;
import io.kubernetes.client.openapi.models.V1Condition; import io.kubernetes.client.openapi.models.V1Condition;
import io.kubernetes.client.openapi.models.V1ObjectMeta;
import io.kubernetes.client.util.Strings;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.time.Instant; import java.time.Instant;
import java.util.Collection; import java.util.Collection;
import java.util.Collections; import java.util.Collections;
@ -36,9 +35,7 @@ import java.util.Map;
import java.util.Objects; import java.util.Objects;
import java.util.Optional; import java.util.Optional;
import java.util.Set; import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Function; import java.util.function.Function;
import java.util.logging.Level;
import java.util.logging.Logger; import java.util.logging.Logger;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import org.jdrupes.vmoperator.util.DataPath; import org.jdrupes.vmoperator.util.DataPath;
@ -46,22 +43,21 @@ import org.jdrupes.vmoperator.util.DataPath;
/** /**
* Represents a VM definition. * Represents a VM definition.
*/ */
@SuppressWarnings({ "PMD.DataClass", "PMD.TooManyMethods" }) @SuppressWarnings({ "PMD.DataClass", "PMD.TooManyMethods",
public class VmDefinition { "PMD.CouplingBetweenObjects" })
public class VmDefinition extends K8sDynamicModel {
@SuppressWarnings("PMD.FieldNamingConventions") @SuppressWarnings({ "PMD.FieldNamingConventions", "unused" })
private static final Logger logger private static final Logger logger
= Logger.getLogger(VmDefinition.class.getName()); = Logger.getLogger(VmDefinition.class.getName());
@SuppressWarnings("PMD.FieldNamingConventions") @SuppressWarnings("PMD.FieldNamingConventions")
private static final Gson gson = new JSON().getGson();
@SuppressWarnings("PMD.FieldNamingConventions")
private static final ObjectMapper objectMapper private static final ObjectMapper objectMapper
= new ObjectMapper().registerModule(new JavaTimeModule()); = new ObjectMapper().registerModule(new JavaTimeModule());
private String kind; private final Model model;
private String apiVersion; private VmExtraData extraData;
private V1ObjectMeta metadata;
private Map<String, Object> spec;
private Map<String, Object> status;
private final Map<String, Object> extra = new ConcurrentHashMap<>();
/** /**
* The VM state from the VM definition. * The VM state from the VM definition.
@ -145,66 +141,34 @@ public class VmDefinition {
} }
/** /**
* Gets the kind. * Instantiates a new vm definition.
* *
* @return the kind * @param delegate the delegate
* @param json the json
*/ */
public String getKind() { public VmDefinition(Gson delegate, JsonObject json) {
return kind; super(delegate, json);
model = gson.fromJson(json, Model.class);
} }
/** /**
* Sets the kind. * Gets the spec.
* *
* @param kind the kind to set * @return the spec
*/ */
public void setKind(String kind) { public Map<String, Object> spec() {
this.kind = kind; return model.getSpec();
} }
/** /**
* Gets the api version. * Get a value from the spec using {@link DataPath#get}.
* *
* @return the apiVersion * @param <T> the generic type
* @param selectors the selectors
* @return the value, if found
*/ */
public String getApiVersion() { public <T> Optional<T> fromSpec(Object... selectors) {
return apiVersion; return DataPath.get(spec(), selectors);
}
/**
* Sets the api version.
*
* @param apiVersion the apiVersion to set
*/
public void setApiVersion(String apiVersion) {
this.apiVersion = apiVersion;
}
/**
* Gets the metadata.
*
* @return the metadata
*/
public V1ObjectMeta getMetadata() {
return metadata;
}
/**
* Gets the metadata.
*
* @return the metadata
*/
public V1ObjectMeta metadata() {
return metadata;
}
/**
* Sets the metadata.
*
* @param metadata the metadata to set
*/
public void setMetadata(V1ObjectMeta metadata) {
this.metadata = metadata;
} }
/** /**
@ -217,35 +181,6 @@ public class VmDefinition {
.orElse(Collections.emptyList()); .orElse(Collections.emptyList());
} }
/**
* Gets the spec.
*
* @return the spec
*/
public Map<String, Object> getSpec() {
return spec;
}
/**
* Gets the spec.
*
* @return the spec
*/
public Map<String, Object> spec() {
return spec;
}
/**
* Get a value from the spec using {@link DataPath#get}.
*
* @param <T> the generic type
* @param selectors the selectors
* @return the value, if found
*/
public <T> Optional<T> fromSpec(Object... selectors) {
return DataPath.get(spec, selectors);
}
/** /**
* Get a value from the `spec().get("vm")` using {@link DataPath#get}. * Get a value from the `spec().get("vm")` using {@link DataPath#get}.
* *
@ -254,35 +189,17 @@ public class VmDefinition {
* @return the value, if found * @return the value, if found
*/ */
public <T> Optional<T> fromVm(Object... selectors) { public <T> Optional<T> fromVm(Object... selectors) {
return DataPath.get(spec, "vm") return DataPath.get(spec(), "vm")
.flatMap(vm -> DataPath.get(vm, selectors)); .flatMap(vm -> DataPath.get(vm, selectors));
} }
/**
* Sets the spec.
*
* @param spec the spec to set
*/
public void setSpec(Map<String, Object> spec) {
this.spec = spec;
}
/**
* Gets the status.
*
* @return the status
*/
public Map<String, Object> getStatus() {
return status;
}
/** /**
* Gets the status. * Gets the status.
* *
* @return the status * @return the status
*/ */
public Map<String, Object> status() { public Map<String, Object> status() {
return status; return model.getStatus();
} }
/** /**
@ -293,16 +210,7 @@ public class VmDefinition {
* @return the value, if found * @return the value, if found
*/ */
public <T> Optional<T> fromStatus(Object... selectors) { public <T> Optional<T> fromStatus(Object... selectors) {
return DataPath.get(status, selectors); return DataPath.get(status(), selectors);
}
/**
* Sets the status.
*
* @param status the status to set
*/
public void setStatus(Map<String, Object> status) {
this.status = status;
} }
/** /**
@ -382,27 +290,21 @@ public class VmDefinition {
} }
/** /**
* Set extra data (locally used, unknown to kubernetes). * Set extra data (unknown to kubernetes).
*
* @param property the property
* @param value the value
* @return the VM definition * @return the VM definition
*/ */
public VmDefinition extra(String property, Object value) { /* default */ VmDefinition extra(VmExtraData extraData) {
extra.put(property, value); this.extraData = extraData;
return this; return this;
} }
/** /**
* Return extra data. * Return the extra data.
* *
* @param <T> the generic type * @return the data
* @param property the property
* @return the object
*/ */
@SuppressWarnings("unchecked") public Optional<VmExtraData> extra() {
public <T> T extra(String property) { return Optional.ofNullable(extraData);
return (T) extra.get(property);
} }
/** /**
@ -411,7 +313,7 @@ public class VmDefinition {
* @return the string * @return the string
*/ */
public String name() { public String name() {
return metadata.getName(); return metadata().getName();
} }
/** /**
@ -420,7 +322,7 @@ public class VmDefinition {
* @return the string * @return the string
*/ */
public String namespace() { public String namespace() {
return metadata.getNamespace(); return metadata().getNamespace();
} }
/** /**
@ -490,78 +392,6 @@ public class VmDefinition {
.map(Number::longValue); .map(Number::longValue);
} }
/**
* Create a connection file.
*
* @param password the password
* @param preferredIpVersion the preferred IP version
* @param deleteConnectionFile the delete connection file
* @return the string
*/
public String connectionFile(String password,
Class<?> preferredIpVersion, boolean deleteConnectionFile) {
var addr = displayIp(preferredIpVersion);
if (addr.isEmpty()) {
logger.severe(() -> "Failed to find display IP for " + name());
return null;
}
var port = this.<Number> fromVm("display", "spice", "port")
.map(Number::longValue);
if (port.isEmpty()) {
logger.severe(() -> "No port defined for display of " + name());
return null;
}
StringBuffer data = new StringBuffer(100)
.append("[virt-viewer]\ntype=spice\nhost=")
.append(addr.get().getHostAddress()).append("\nport=")
.append(port.get().toString())
.append('\n');
if (password != null) {
data.append("password=").append(password).append('\n');
}
this.<String> fromVm("display", "spice", "proxyUrl")
.ifPresent(u -> {
if (!Strings.isNullOrEmpty(u)) {
data.append("proxy=").append(u).append('\n');
}
});
if (deleteConnectionFile) {
data.append("delete-this-file=1\n");
}
return data.toString();
}
private Optional<InetAddress> displayIp(Class<?> preferredIpVersion) {
Optional<String> server = fromVm("display", "spice", "server");
if (server.isPresent()) {
var srv = server.get();
try {
var addr = InetAddress.getByName(srv);
logger.fine(() -> "Using IP address from CRD for "
+ getMetadata().getName() + ": " + addr);
return Optional.of(addr);
} catch (UnknownHostException e) {
logger.log(Level.SEVERE, e, () -> "Invalid server address "
+ srv + ": " + e.getMessage());
return Optional.empty();
}
}
var addrs = Optional.<List<String>> ofNullable(
extra("nodeAddresses")).orElse(Collections.emptyList()).stream()
.map(a -> {
try {
return InetAddress.getByName(a);
} catch (UnknownHostException e) {
logger.warning(() -> "Invalid IP address: " + a);
return null;
}
}).filter(a -> a != null).toList();
logger.fine(() -> "Known IP addresses for " + name() + ": " + addrs);
return addrs.stream()
.filter(a -> preferredIpVersion.isAssignableFrom(a.getClass()))
.findFirst().or(() -> addrs.stream().findFirst());
}
/** /**
* Hash code. * Hash code.
* *
@ -569,7 +399,7 @@ public class VmDefinition {
*/ */
@Override @Override
public int hashCode() { public int hashCode() {
return Objects.hash(metadata.getNamespace(), metadata.getName()); return Objects.hash(metadata().getNamespace(), metadata().getName());
} }
/** /**
@ -590,9 +420,55 @@ public class VmDefinition {
return false; return false;
} }
VmDefinition other = (VmDefinition) obj; VmDefinition other = (VmDefinition) obj;
return Objects.equals(metadata.getNamespace(), return Objects.equals(metadata().getNamespace(),
other.metadata.getNamespace()) other.metadata().getNamespace())
&& Objects.equals(metadata.getName(), other.metadata.getName()); && Objects.equals(metadata().getName(), other.metadata().getName());
}
/**
* The Class Model.
*/
public static class Model {
private Map<String, Object> spec;
private Map<String, Object> status;
/**
* Gets the spec.
*
* @return the spec
*/
public Map<String, Object> getSpec() {
return spec;
}
/**
* Sets the spec.
*
* @param spec the spec to set
*/
public void setSpec(Map<String, Object> spec) {
this.spec = spec;
}
/**
* Gets the status.
*
* @return the status
*/
public Map<String, Object> getStatus() {
return status;
}
/**
* Sets the status.
*
* @param status the status to set
*/
public void setStatus(Map<String, Object> status) {
this.status = status;
}
} }
} }

View file

@ -1,39 +0,0 @@
/*
* 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.common;
import com.google.gson.Gson;
import com.google.gson.JsonObject;
/**
* Represents a VM definition.
*/
@SuppressWarnings("PMD.DataClass")
public class VmDefinitionModel extends K8sDynamicModel {
/**
* Instantiates a new model from the JSON representation.
*
* @param delegate the gson instance to use for extracting structured data
* @param json the JSON
*/
public VmDefinitionModel(Gson delegate, JsonObject json) {
super(delegate, json);
}
}

View file

@ -33,10 +33,10 @@ import java.util.Collection;
*/ */
@SuppressWarnings("PMD.DataflowAnomalyAnalysis") @SuppressWarnings("PMD.DataflowAnomalyAnalysis")
public class VmDefinitionStub public class VmDefinitionStub
extends K8sDynamicStubBase<VmDefinitionModel, VmDefinitionModels> { extends K8sDynamicStubBase<VmDefinition, VmDefinitions> {
private static DynamicTypeAdapterFactory<VmDefinitionModel, private static DynamicTypeAdapterFactory<VmDefinition,
VmDefinitionModels> taf = new VmDefintionModelTypeAdapterFactory(); VmDefinitions> taf = new VmDefintionModelTypeAdapterFactory();
/** /**
* Instantiates a new stub for VM defintions. * Instantiates a new stub for VM defintions.
@ -48,7 +48,7 @@ public class VmDefinitionStub
*/ */
public VmDefinitionStub(K8sClient client, APIResource context, public VmDefinitionStub(K8sClient client, APIResource context,
String namespace, String name) { String namespace, String name) {
super(VmDefinitionModel.class, VmDefinitionModels.class, taf, client, super(VmDefinition.class, VmDefinitions.class, taf, client,
context, namespace, name); context, namespace, name);
} }
@ -101,10 +101,10 @@ public class VmDefinitionStub
*/ */
public static VmDefinitionStub createFromYaml(K8sClient client, public static VmDefinitionStub createFromYaml(K8sClient client,
APIResource context, Reader yaml) throws ApiException { APIResource context, Reader yaml) throws ApiException {
var model = new VmDefinitionModel(client.getJSON().getGson(), var model = new VmDefinition(client.getJSON().getGson(),
K8s.yamlToJson(client, yaml)); K8s.yamlToJson(client, yaml));
return K8sGenericStub.create(VmDefinitionModel.class, return K8sGenericStub.create(VmDefinition.class,
VmDefinitionModels.class, client, context, model, VmDefinitions.class, client, context, model,
(c, ns, n) -> new VmDefinitionStub(c, context, ns, n)); (c, ns, n) -> new VmDefinitionStub(c, context, ns, n));
} }
@ -121,8 +121,8 @@ public class VmDefinitionStub
public static Collection<VmDefinitionStub> list(K8sClient client, public static Collection<VmDefinitionStub> list(K8sClient client,
APIResource context, String namespace, ListOptions options) APIResource context, String namespace, ListOptions options)
throws ApiException { throws ApiException {
return K8sGenericStub.list(VmDefinitionModel.class, return K8sGenericStub.list(VmDefinition.class,
VmDefinitionModels.class, client, context, namespace, options, VmDefinitions.class, client, context, namespace, options,
(c, ns, n) -> new VmDefinitionStub(c, context, ns, n)); (c, ns, n) -> new VmDefinitionStub(c, context, ns, n));
} }
@ -144,13 +144,13 @@ public class VmDefinitionStub
* A factory for creating VmDefinitionModel(s) objects. * A factory for creating VmDefinitionModel(s) objects.
*/ */
public static class VmDefintionModelTypeAdapterFactory extends public static class VmDefintionModelTypeAdapterFactory extends
DynamicTypeAdapterFactory<VmDefinitionModel, VmDefinitionModels> { DynamicTypeAdapterFactory<VmDefinition, VmDefinitions> {
/** /**
* Instantiates a new dynamic model type adapter factory. * Instantiates a new dynamic model type adapter factory.
*/ */
public VmDefintionModelTypeAdapterFactory() { public VmDefintionModelTypeAdapterFactory() {
super(VmDefinitionModel.class, VmDefinitionModels.class); super(VmDefinition.class, VmDefinitions.class);
} }
} }

View file

@ -22,10 +22,10 @@ import com.google.gson.Gson;
import com.google.gson.JsonObject; import com.google.gson.JsonObject;
/** /**
* Represents a list of {@link VmDefinitionModel}s. * Represents a list of {@link VmDefinition}s.
*/ */
public class VmDefinitionModels public class VmDefinitions
extends K8sDynamicModelsBase<VmDefinitionModel> { extends K8sDynamicModelsBase<VmDefinition> {
/** /**
* Initialize the object list using the given JSON data. * Initialize the object list using the given JSON data.
@ -33,7 +33,7 @@ public class VmDefinitionModels
* @param delegate the gson instance to use for extracting structured data * @param delegate the gson instance to use for extracting structured data
* @param data the data * @param data the data
*/ */
public VmDefinitionModels(Gson delegate, JsonObject data) { public VmDefinitions(Gson delegate, JsonObject data) {
super(VmDefinitionModel.class, delegate, data); super(VmDefinition.class, delegate, data);
} }
} }

View file

@ -0,0 +1,171 @@
/*
* VM-Operator
* Copyright (C) 2025 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.common;
import io.kubernetes.client.util.Strings;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* Represents internally used dynamic data associated with a
* {@link VmDefinition}.
*/
public class VmExtraData {
@SuppressWarnings("PMD.FieldNamingConventions")
private static final Logger logger
= Logger.getLogger(VmExtraData.class.getName());
private final VmDefinition vmDef;
private String nodeName = "";
private List<String> nodeAddresses = Collections.emptyList();
private long resetCount;
/**
* Initializes a new instance.
*
* @param vmDef the VM definition
*/
public VmExtraData(VmDefinition vmDef) {
this.vmDef = vmDef;
vmDef.extra(this);
}
/**
* Sets the node info.
*
* @param name the name
* @param addresses the addresses
* @return the VM extra data
*/
public VmExtraData nodeInfo(String name, List<String> addresses) {
nodeName = name;
nodeAddresses = addresses;
return this;
}
/**
* Return the node name.
*
* @return the string
*/
public String nodeName() {
return nodeName;
}
/**
* Sets the reset count.
*
* @param resetCount the reset count
* @return the vm extra data
*/
public VmExtraData resetCount(long resetCount) {
this.resetCount = resetCount;
return this;
}
/**
* Returns the reset count.
*
* @return the long
*/
public long resetCount() {
return resetCount;
}
/**
* Create a connection file.
*
* @param password the password
* @param preferredIpVersion the preferred IP version
* @param deleteConnectionFile the delete connection file
* @return the string
*/
public String connectionFile(String password,
Class<?> preferredIpVersion, boolean deleteConnectionFile) {
var addr = displayIp(preferredIpVersion);
if (addr.isEmpty()) {
logger
.severe(() -> "Failed to find display IP for " + vmDef.name());
return null;
}
var port = vmDef.<Number> fromVm("display", "spice", "port")
.map(Number::longValue);
if (port.isEmpty()) {
logger
.severe(() -> "No port defined for display of " + vmDef.name());
return null;
}
StringBuffer data = new StringBuffer(100)
.append("[virt-viewer]\ntype=spice\nhost=")
.append(addr.get().getHostAddress()).append("\nport=")
.append(port.get().toString())
.append('\n');
if (password != null) {
data.append("password=").append(password).append('\n');
}
vmDef.<String> fromVm("display", "spice", "proxyUrl")
.ifPresent(u -> {
if (!Strings.isNullOrEmpty(u)) {
data.append("proxy=").append(u).append('\n');
}
});
if (deleteConnectionFile) {
data.append("delete-this-file=1\n");
}
return data.toString();
}
private Optional<InetAddress> displayIp(Class<?> preferredIpVersion) {
Optional<String> server = vmDef.fromVm("display", "spice", "server");
if (server.isPresent()) {
var srv = server.get();
try {
var addr = InetAddress.getByName(srv);
logger.fine(() -> "Using IP address from CRD for "
+ vmDef.metadata().getName() + ": " + addr);
return Optional.of(addr);
} catch (UnknownHostException e) {
logger.log(Level.SEVERE, e, () -> "Invalid server address "
+ srv + ": " + e.getMessage());
return Optional.empty();
}
}
var addrs = nodeAddresses.stream().map(a -> {
try {
return InetAddress.getByName(a);
} catch (UnknownHostException e) {
logger.warning(() -> "Invalid IP address: " + a);
return null;
}
}).filter(Objects::nonNull).toList();
logger.fine(
() -> "Known IP addresses for " + vmDef.name() + ": " + addrs);
return addrs.stream()
.filter(a -> preferredIpVersion.isAssignableFrom(a.getClass()))
.findFirst().or(() -> addrs.stream().findFirst());
}
}

View file

@ -10,7 +10,7 @@ metadata:
annotations: annotations:
vmoperator.jdrupes.org/version: ${ managerVersion } vmoperator.jdrupes.org/version: ${ managerVersion }
ownerReferences: ownerReferences:
- apiVersion: ${ cr.apiVersion } - apiVersion: ${ cr.apiVersion() }
kind: ${ constants.VM_OP_KIND_VM } kind: ${ constants.VM_OP_KIND_VM }
name: ${ cr.name() } name: ${ cr.name() }
uid: ${ cr.metadata().getUid() } uid: ${ cr.metadata().getUid() }
@ -53,7 +53,7 @@ data:
# i.e. if you start the VM without a value for this property, and # i.e. if you start the VM without a value for this property, and
# decide to trigger a reset later, you have to first set the value # decide to trigger a reset later, you have to first set the value
# and then inrement it. # and then inrement it.
resetCounter: ${ cr.extra("resetCount")?c } resetCounter: ${ cr.extra().get().resetCount()?c }
# Forward the cloud-init data if provided # Forward the cloud-init data if provided
<#if spec.cloudInit??> <#if spec.cloudInit??>

View file

@ -10,7 +10,7 @@ metadata:
annotations: annotations:
vmoperator.jdrupes.org/version: ${ managerVersion } vmoperator.jdrupes.org/version: ${ managerVersion }
ownerReferences: ownerReferences:
- apiVersion: ${ cr.apiVersion } - apiVersion: ${ cr.apiVersion() }
kind: ${ constants.VM_OP_KIND_VM } kind: ${ constants.VM_OP_KIND_VM }
name: ${ cr.name() } name: ${ cr.name() }
uid: ${ cr.metadata().getUid() } uid: ${ cr.metadata().getUid() }

View file

@ -14,7 +14,7 @@ metadata:
vmrunner.jdrupes.org/cmVersion: "${ cm.metadata.resourceVersion }" vmrunner.jdrupes.org/cmVersion: "${ cm.metadata.resourceVersion }"
vmoperator.jdrupes.org/version: ${ managerVersion } vmoperator.jdrupes.org/version: ${ managerVersion }
ownerReferences: ownerReferences:
- apiVersion: ${ cr.apiVersion } - apiVersion: ${ cr.apiVersion() }
kind: ${ constants.VM_OP_KIND_VM } kind: ${ constants.VM_OP_KIND_VM }
name: ${ cr.name() } name: ${ cr.name() }
uid: ${ cr.metadata().getUid() } uid: ${ cr.metadata().getUid() }

View file

@ -24,6 +24,7 @@ import io.kubernetes.client.custom.V1Patch;
import io.kubernetes.client.openapi.ApiException; import io.kubernetes.client.openapi.ApiException;
import io.kubernetes.client.openapi.Configuration; import io.kubernetes.client.openapi.Configuration;
import java.io.IOException; import java.io.IOException;
import java.net.HttpURLConnection;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.time.Instant; import java.time.Instant;
@ -211,7 +212,10 @@ public class Controller extends Component {
} }
/** /**
* Update the assignment information in the status of the VM CR. * Attempt to Update the assignment information in the status of the
* VM CR. Returns true if successful. The handler does not attempt
* retries, because in case of failure it will be necessary to
* re-evaluate the chosen VM.
* *
* @param event the event * @param event the event
* @param channel the channel * @param channel the channel
@ -220,18 +224,27 @@ public class Controller extends Component {
@Handler @Handler
public void onUpdatedAssignment(UpdateAssignment event, VmChannel channel) public void onUpdatedAssignment(UpdateAssignment event, VmChannel channel)
throws ApiException { throws ApiException {
var vmDef = channel.vmDefinition(); try {
var vmStub = VmDefinitionStub.get(channel.client(), var vmDef = channel.vmDefinition();
new GroupVersionKind(VM_OP_GROUP, "", VM_OP_KIND_VM), var vmStub = VmDefinitionStub.get(channel.client(),
vmDef.namespace(), vmDef.name()); new GroupVersionKind(VM_OP_GROUP, "", VM_OP_KIND_VM),
vmStub.updateStatus(from -> { vmDef.namespace(), vmDef.name());
JsonObject status = from.status(); if (vmStub.updateStatus(vmDef, from -> {
var assignment = GsonPtr.to(status).to("assignment"); JsonObject status = from.statusJson();
assignment.set("pool", event.usedPool()); var assignment = GsonPtr.to(status).to("assignment");
assignment.set("user", event.toUser()); assignment.set("pool", event.usedPool());
assignment.set("lastUsed", Instant.now().toString()); assignment.set("user", event.toUser());
return status; assignment.set("lastUsed", Instant.now().toString());
}); return status;
event.setResult(true); }).isPresent()) {
event.setResult(true);
}
} catch (ApiException e) {
// Log exceptions except for conflict, which can be expected
if (HttpURLConnection.HTTP_CONFLICT != e.getCode()) {
throw e;
}
}
event.setResult(false);
} }
} }

View file

@ -185,7 +185,7 @@ public class DisplaySecretMonitor
new GroupVersionKind(VM_OP_GROUP, "", VM_OP_KIND_VM), new GroupVersionKind(VM_OP_GROUP, "", VM_OP_KIND_VM),
event.vmDefinition().namespace(), event.vmDefinition().name()); event.vmDefinition().namespace(), event.vmDefinition().name());
vmStub.updateStatus(from -> { vmStub.updateStatus(from -> {
JsonObject status = from.status(); JsonObject status = from.statusJson();
status.addProperty("consoleUser", event.user()); status.addProperty("consoleUser", event.user());
return status; return status;
}); });

View file

@ -187,7 +187,8 @@ public class PoolMonitor extends
new GroupVersionKind(VM_OP_GROUP, "", VM_OP_KIND_VM), new GroupVersionKind(VM_OP_GROUP, "", VM_OP_KIND_VM),
vmDef.namespace(), vmDef.name()); vmDef.namespace(), vmDef.name());
vmStub.updateStatus(from -> { vmStub.updateStatus(from -> {
JsonObject status = from.status(); // TODO
JsonObject status = from.statusJson();
var assignment = GsonPtr.to(status).to("assignment"); var assignment = GsonPtr.to(status).to("assignment");
assignment.set("lastUsed", ccChange.get().toString()); assignment.set("lastUsed", ccChange.get().toString());
return status; return status;

View file

@ -248,7 +248,7 @@ public class Reconciler extends Component {
public void onResetVm(ResetVm event, VmChannel channel) public void onResetVm(ResetVm event, VmChannel channel)
throws ApiException, IOException, TemplateException { throws ApiException, IOException, TemplateException {
var vmDef = channel.vmDefinition(); var vmDef = channel.vmDefinition();
vmDef.extra("resetCount", vmDef.<Long> extra("resetCount") + 1); vmDef.extra().ifPresent(e -> e.resetCount(e.resetCount() + 1));
Map<String, Object> model Map<String, Object> model
= prepareModel(channel.client(), channel.vmDefinition()); = prepareModel(channel.client(), channel.vmDefinition());
cmReconciler.reconcile(model, channel); cmReconciler.reconcile(model, channel);

View file

@ -41,9 +41,9 @@ import org.jdrupes.vmoperator.common.K8sV1ConfigMapStub;
import org.jdrupes.vmoperator.common.K8sV1PodStub; import org.jdrupes.vmoperator.common.K8sV1PodStub;
import org.jdrupes.vmoperator.common.K8sV1StatefulSetStub; import org.jdrupes.vmoperator.common.K8sV1StatefulSetStub;
import org.jdrupes.vmoperator.common.VmDefinition; import org.jdrupes.vmoperator.common.VmDefinition;
import org.jdrupes.vmoperator.common.VmDefinitionModel;
import org.jdrupes.vmoperator.common.VmDefinitionModels;
import org.jdrupes.vmoperator.common.VmDefinitionStub; import org.jdrupes.vmoperator.common.VmDefinitionStub;
import org.jdrupes.vmoperator.common.VmDefinitions;
import org.jdrupes.vmoperator.common.VmExtraData;
import org.jdrupes.vmoperator.common.VmPool; import org.jdrupes.vmoperator.common.VmPool;
import static org.jdrupes.vmoperator.manager.Constants.APP_NAME; import static org.jdrupes.vmoperator.manager.Constants.APP_NAME;
import static org.jdrupes.vmoperator.manager.Constants.VM_OP_NAME; import static org.jdrupes.vmoperator.manager.Constants.VM_OP_NAME;
@ -65,7 +65,7 @@ import org.jgrapes.core.annotation.Handler;
*/ */
@SuppressWarnings({ "PMD.DataflowAnomalyAnalysis", "PMD.ExcessiveImports" }) @SuppressWarnings({ "PMD.DataflowAnomalyAnalysis", "PMD.ExcessiveImports" })
public class VmMonitor extends public class VmMonitor extends
AbstractMonitor<VmDefinitionModel, VmDefinitionModels, VmChannel> { AbstractMonitor<VmDefinition, VmDefinitions, VmChannel> {
private final ChannelManager<String, VmChannel, ?> channelManager; private final ChannelManager<String, VmChannel, ?> channelManager;
@ -77,8 +77,8 @@ public class VmMonitor extends
*/ */
public VmMonitor(Channel componentChannel, public VmMonitor(Channel componentChannel,
ChannelManager<String, VmChannel, ?> channelManager) { ChannelManager<String, VmChannel, ?> channelManager) {
super(componentChannel, VmDefinitionModel.class, super(componentChannel, VmDefinition.class,
VmDefinitionModels.class); VmDefinitions.class);
this.channelManager = channelManager; this.channelManager = channelManager;
} }
@ -122,7 +122,7 @@ public class VmMonitor extends
@Override @Override
protected void handleChange(K8sClient client, protected void handleChange(K8sClient client,
Watch.Response<VmDefinitionModel> response) { Watch.Response<VmDefinition> response) {
V1ObjectMeta metadata = response.object.getMetadata(); V1ObjectMeta metadata = response.object.getMetadata();
AtomicBoolean toBeAdded = new AtomicBoolean(false); AtomicBoolean toBeAdded = new AtomicBoolean(false);
VmChannel channel = channelManager.channel(metadata.getName()) VmChannel channel = channelManager.channel(metadata.getName())
@ -132,21 +132,17 @@ public class VmMonitor extends
}); });
// Get full definition and associate with channel as backup // Get full definition and associate with channel as backup
var vmModel = response.object; var vmDef = response.object;
if (vmModel.data() == null) { if (vmDef.data() == null) {
// ADDED event does not provide data, see // ADDED event does not provide data, see
// https://github.com/kubernetes-client/java/issues/3215 // https://github.com/kubernetes-client/java/issues/3215
vmModel = getModel(client, vmModel); vmDef = getModel(client, vmDef);
} }
VmDefinition vmDef = null; if (vmDef.data() != null) {
if (vmModel.data() != null) {
// New data, augment and save // New data, augment and save
vmDef = client.getJSON().getGson().fromJson(vmModel.data(), addExtraData(channel.client(), vmDef, channel.vmDefinition());
VmDefinition.class);
addDynamicData(channel.client(), vmDef, channel.vmDefinition());
channel.setVmDefinition(vmDef); channel.setVmDefinition(vmDef);
} } else {
if (vmDef == null) {
// Reuse cached (e.g. if deleted) // Reuse cached (e.g. if deleted)
vmDef = channel.vmDefinition(); vmDef = channel.vmDefinition();
} }
@ -173,8 +169,7 @@ public class VmMonitor extends
channel.pipeline().fire(chgEvt, channel); channel.pipeline().fire(chgEvt, channel);
} }
private VmDefinitionModel getModel(K8sClient client, private VmDefinition getModel(K8sClient client, VmDefinition vmDef) {
VmDefinitionModel vmDef) {
try { try {
return VmDefinitionStub.get(client, context(), namespace(), return VmDefinitionStub.get(client, context(), namespace(),
vmDef.metadata().getName()).model().orElse(null); vmDef.metadata().getName()).model().orElse(null);
@ -184,17 +179,14 @@ public class VmMonitor extends
} }
@SuppressWarnings("PMD.AvoidDuplicateLiterals") @SuppressWarnings("PMD.AvoidDuplicateLiterals")
private void addDynamicData(K8sClient client, VmDefinition vmDef, private void addExtraData(K8sClient client, VmDefinition vmDef,
VmDefinition prevState) { VmDefinition prevState) {
// Maintain (or initialize) the resetCount var extra = new VmExtraData(vmDef);
vmDef.extra("resetCount",
Optional.ofNullable(prevState).map(d -> d.extra("resetCount"))
.orElse(0L));
// Node information // Maintain (or initialize) the resetCount
// Add defaults in case the VM is not running extra.resetCount(
vmDef.extra("nodeName", ""); Optional.ofNullable(prevState).flatMap(VmDefinition::extra)
vmDef.extra("nodeAddress", ""); .map(VmExtraData::resetCount).orElse(0L));
// VM definition status changes before the pod terminates. // VM definition status changes before the pod terminates.
// This results in pod information being shown for a stopped // This results in pod information being shown for a stopped
@ -202,6 +194,8 @@ public class VmMonitor extends
if (!vmDef.conditionStatus("Running").orElse(false)) { if (!vmDef.conditionStatus("Running").orElse(false)) {
return; return;
} }
// Get pod and extract node information.
var podSearch = new ListOptions(); var podSearch = new ListOptions();
podSearch.setLabelSelector("app.kubernetes.io/name=" + APP_NAME podSearch.setLabelSelector("app.kubernetes.io/name=" + APP_NAME
+ ",app.kubernetes.io/component=" + APP_NAME + ",app.kubernetes.io/component=" + APP_NAME
@ -211,16 +205,15 @@ public class VmMonitor extends
= K8sV1PodStub.list(client, namespace(), podSearch); = K8sV1PodStub.list(client, namespace(), podSearch);
for (var podStub : podList) { for (var podStub : podList) {
var nodeName = podStub.model().get().getSpec().getNodeName(); var nodeName = podStub.model().get().getSpec().getNodeName();
vmDef.extra("nodeName", nodeName); logger.fine(() -> "Adding node name " + nodeName
logger.fine(() -> "Added node name " + nodeName
+ " to VM info for " + vmDef.name()); + " to VM info for " + vmDef.name());
@SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops") @SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops")
var addrs = new ArrayList<String>(); var addrs = new ArrayList<String>();
podStub.model().get().getStatus().getPodIPs().stream() podStub.model().get().getStatus().getPodIPs().stream()
.map(ip -> ip.getIp()).forEach(addrs::add); .map(ip -> ip.getIp()).forEach(addrs::add);
vmDef.extra("nodeAddresses", addrs); logger.fine(() -> "Adding node addresses " + addrs
logger.fine(() -> "Added node addresses " + addrs
+ " to VM info for " + vmDef.name()); + " to VM info for " + vmDef.name());
extra.nodeInfo(nodeName, addrs);
} }
} catch (ApiException e) { } catch (ApiException e) {
logger.log(Level.WARNING, e, logger.log(Level.WARNING, e,
@ -262,49 +255,56 @@ public class VmMonitor extends
@SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops") @SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops")
public void onAssignVm(AssignVm event) public void onAssignVm(AssignVm event)
throws ApiException, InterruptedException { throws ApiException, InterruptedException {
// Search for existing assignment. while (true) {
var assignedVm = channelManager.channels().stream() // Search for existing assignment.
.filter(c -> c.vmDefinition().assignedFrom() var vmQuery = channelManager.channels().stream()
.map(p -> p.equals(event.fromPool())).orElse(false)) .filter(c -> c.vmDefinition().assignedFrom()
.filter(c -> c.vmDefinition().assignedTo() .map(p -> p.equals(event.fromPool())).orElse(false))
.map(u -> u.equals(event.toUser())).orElse(false)) .filter(c -> c.vmDefinition().assignedTo()
.findFirst(); .map(u -> u.equals(event.toUser())).orElse(false))
if (assignedVm.isPresent()) { .findFirst();
var vmDef = assignedVm.get().vmDefinition(); if (vmQuery.isPresent()) {
event.setResult(new VmData(vmDef, assignedVm.get())); var vmDef = vmQuery.get().vmDefinition();
return; event.setResult(new VmData(vmDef, vmQuery.get()));
return;
}
// Get the pool definition for checking possible assignment
VmPool vmPool = newEventPipeline().fire(new GetPools()
.withName(event.fromPool())).get().stream().findFirst()
.orElse(null);
if (vmPool == null) {
return;
}
// Find available VM.
vmQuery = channelManager.channels().stream()
.filter(c -> vmPool.isAssignable(c.vmDefinition()))
.sorted(Comparator.comparing((VmChannel c) -> c.vmDefinition()
.assignmentLastUsed().orElse(Instant.ofEpochSecond(0)))
.thenComparing(preferRunning))
.findFirst();
// None found
if (vmQuery.isEmpty()) {
return;
}
// Assign to user
var chosenVm = vmQuery.get();
var vmPipeline = chosenVm.pipeline();
if (Optional.ofNullable(vmPipeline.fire(new UpdateAssignment(
vmPool.name(), event.toUser()), chosenVm).get())
.orElse(false)) {
var vmDef = chosenVm.vmDefinition();
event.setResult(new VmData(vmDef, chosenVm));
// Make sure that a newly assigned VM is running.
chosenVm.pipeline().fire(new ModifyVm(vmDef.name(),
"state", "Running", chosenVm));
return;
}
} }
// Get the pool definition assignability check
VmPool vmPool = newEventPipeline().fire(new GetPools()
.withName(event.fromPool())).get().stream().findFirst()
.orElse(null);
if (vmPool == null) {
return;
}
// Find available VM.
assignedVm = channelManager.channels().stream()
.filter(c -> vmPool.isAssignable(c.vmDefinition()))
.sorted(Comparator.comparing((VmChannel c) -> c.vmDefinition()
.assignmentLastUsed().orElse(Instant.ofEpochSecond(0)))
.thenComparing(preferRunning))
.findFirst();
// None found
if (assignedVm.isEmpty()) {
return;
}
// Assign to user
assignedVm.get().pipeline().fire(new UpdateAssignment(vmPool.name(),
event.toUser()), assignedVm.get()).get();
var vmDef = assignedVm.get().vmDefinition();
event.setResult(new VmData(vmDef, assignedVm.get()));
// Make sure that a newly assigned VM is running.
assignedVm.get().pipeline().fire(new ModifyVm(vmDef.name(),
"state", "Running", assignedVm.get()));
} }
private static Comparator<VmChannel> preferRunning private static Comparator<VmChannel> preferRunning

View file

@ -106,7 +106,7 @@ public class ConsoleTracker extends VmDefUpdater {
mainChannelClientHost = event.clientHost(); mainChannelClientHost = event.clientHost();
mainChannelClientPort = event.clientPort(); mainChannelClientPort = event.clientPort();
vmStub.updateStatus(from -> { vmStub.updateStatus(from -> {
JsonObject status = from.status(); JsonObject status = from.statusJson();
status.addProperty("consoleClient", event.clientHost()); status.addProperty("consoleClient", event.clientHost());
updateCondition(from, status, "ConsoleConnected", true, "Connected", updateCondition(from, status, "ConsoleConnected", true, "Connected",
"Connection from " + event.clientHost()); "Connection from " + event.clientHost());
@ -141,7 +141,7 @@ public class ConsoleTracker extends VmDefUpdater {
return; return;
} }
vmStub.updateStatus(from -> { vmStub.updateStatus(from -> {
JsonObject status = from.status(); JsonObject status = from.statusJson();
status.addProperty("consoleClient", ""); status.addProperty("consoleClient", "");
updateCondition(from, status, "ConsoleConnected", false, updateCondition(from, status, "ConsoleConnected", false,
"Disconnected", event.clientHost() + " has disconnected"); "Disconnected", event.clientHost() + " has disconnected");

View file

@ -33,7 +33,7 @@ import static org.jdrupes.vmoperator.common.Constants.APP_NAME;
import static org.jdrupes.vmoperator.common.Constants.VM_OP_GROUP; import static org.jdrupes.vmoperator.common.Constants.VM_OP_GROUP;
import static org.jdrupes.vmoperator.common.Constants.VM_OP_KIND_VM; import static org.jdrupes.vmoperator.common.Constants.VM_OP_KIND_VM;
import org.jdrupes.vmoperator.common.K8s; import org.jdrupes.vmoperator.common.K8s;
import org.jdrupes.vmoperator.common.VmDefinitionModel; import org.jdrupes.vmoperator.common.VmDefinition;
import org.jdrupes.vmoperator.common.VmDefinitionStub; import org.jdrupes.vmoperator.common.VmDefinitionStub;
import org.jdrupes.vmoperator.runner.qemu.events.BalloonChangeEvent; import org.jdrupes.vmoperator.runner.qemu.events.BalloonChangeEvent;
import org.jdrupes.vmoperator.runner.qemu.events.ConfigureQemu; import org.jdrupes.vmoperator.runner.qemu.events.ConfigureQemu;
@ -140,12 +140,12 @@ public class StatusUpdater extends VmDefUpdater {
if (vmDef.isPresent() if (vmDef.isPresent()
&& vmDef.get().metadata().getGeneration() == observedGeneration && vmDef.get().metadata().getGeneration() == observedGeneration
&& (event.configuration().hasDisplayPassword && (event.configuration().hasDisplayPassword
|| vmDef.get().status().getAsJsonPrimitive( || vmDef.get().statusJson().getAsJsonPrimitive(
"displayPasswordSerial").getAsInt() == -1)) { "displayPasswordSerial").getAsInt() == -1)) {
return; return;
} }
vmStub.updateStatus(vmDef.get(), from -> { vmStub.updateStatus(vmDef.get(), from -> {
JsonObject status = from.status(); JsonObject status = from.statusJson();
if (!event.configuration().hasDisplayPassword) { if (!event.configuration().hasDisplayPassword) {
status.addProperty("displayPasswordSerial", -1); status.addProperty("displayPasswordSerial", -1);
} }
@ -169,14 +169,14 @@ public class StatusUpdater extends VmDefUpdater {
"PMD.AvoidLiteralsInIfCondition" }) "PMD.AvoidLiteralsInIfCondition" })
public void onRunnerStateChanged(RunnerStateChange event) public void onRunnerStateChanged(RunnerStateChange event)
throws ApiException { throws ApiException {
VmDefinitionModel vmDef; VmDefinition vmDef;
if (vmStub == null || (vmDef = vmStub.model().orElse(null)) == null) { if (vmStub == null || (vmDef = vmStub.model().orElse(null)) == null) {
return; return;
} }
vmStub.updateStatus(vmDef, from -> { vmStub.updateStatus(vmDef, from -> {
JsonObject status = from.status(); JsonObject status = from.statusJson();
boolean running = RUNNING_STATES.contains(event.runState()); boolean running = RUNNING_STATES.contains(event.runState());
updateCondition(vmDef, vmDef.status(), "Running", running, updateCondition(vmDef, vmDef.statusJson(), "Running", running,
event.reason(), event.message()); event.reason(), event.message());
if (event.runState() == RunState.STARTING) { if (event.runState() == RunState.STARTING) {
status.addProperty("ram", GsonPtr.to(from.data()) status.addProperty("ram", GsonPtr.to(from.data())
@ -230,7 +230,7 @@ public class StatusUpdater extends VmDefUpdater {
return; return;
} }
vmStub.updateStatus(from -> { vmStub.updateStatus(from -> {
JsonObject status = from.status(); JsonObject status = from.statusJson();
status.addProperty("ram", status.addProperty("ram",
new Quantity(new BigDecimal(event.size()), Format.BINARY_SI) new Quantity(new BigDecimal(event.size()), Format.BINARY_SI)
.toSuffixedString()); .toSuffixedString());
@ -250,7 +250,7 @@ public class StatusUpdater extends VmDefUpdater {
return; return;
} }
vmStub.updateStatus(from -> { vmStub.updateStatus(from -> {
JsonObject status = from.status(); JsonObject status = from.statusJson();
status.addProperty("cpus", event.usedCpus().size()); status.addProperty("cpus", event.usedCpus().size());
return status; return status;
}); });
@ -269,7 +269,7 @@ public class StatusUpdater extends VmDefUpdater {
return; return;
} }
vmStub.updateStatus(from -> { vmStub.updateStatus(from -> {
JsonObject status = from.status(); JsonObject status = from.statusJson();
status.addProperty("displayPasswordSerial", status.addProperty("displayPasswordSerial",
status.get("displayPasswordSerial").getAsLong() + 1); status.get("displayPasswordSerial").getAsLong() + 1);
return status; return status;

View file

@ -31,7 +31,7 @@ import java.util.Optional;
import java.util.logging.Level; import java.util.logging.Level;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import org.jdrupes.vmoperator.common.K8sClient; import org.jdrupes.vmoperator.common.K8sClient;
import org.jdrupes.vmoperator.common.VmDefinitionModel; import org.jdrupes.vmoperator.common.VmDefinition;
import org.jdrupes.vmoperator.runner.qemu.events.Exit; import org.jdrupes.vmoperator.runner.qemu.events.Exit;
import org.jgrapes.core.Channel; import org.jgrapes.core.Channel;
import org.jgrapes.core.Component; import org.jgrapes.core.Component;
@ -118,7 +118,7 @@ public class VmDefUpdater extends Component {
* @param reason the reason for the change * @param reason the reason for the change
* @param message the message * @param message the message
*/ */
protected void updateCondition(VmDefinitionModel from, JsonObject status, protected void updateCondition(VmDefinition from, JsonObject status,
String type, boolean state, String reason, String message) { String type, boolean state, String reason, String message) {
// Optimize, as we can get this several times // Optimize, as we can get this several times
var current = status.getAsJsonArray("conditions").asList().stream() var current = status.getAsJsonArray("conditions").asList().stream()

View file

@ -593,7 +593,7 @@ public class VmAccess extends FreeMarkerConlet<VmAccess.ResourceModel> {
Map.of("namespace", vmDef.namespace(), Map.of("namespace", vmDef.namespace(),
"name", vmDef.name()), "name", vmDef.name()),
"spec", vmDef.spec(), "spec", vmDef.spec(),
"status", vmDef.getStatus()); "status", vmDef.status());
} catch (JsonSyntaxException e) { } catch (JsonSyntaxException e) {
logger.log(Level.SEVERE, e, logger.log(Level.SEVERE, e,
() -> "Failed to serialize VM definition"); () -> "Failed to serialize VM definition");
@ -810,13 +810,12 @@ public class VmAccess extends FreeMarkerConlet<VmAccess.ResourceModel> {
} }
var pwQuery = Event.onCompletion(new GetDisplayPassword(vmDef, user), var pwQuery = Event.onCompletion(new GetDisplayPassword(vmDef, user),
e -> { e -> {
var data = vmDef.connectionFile(e.password().orElse(null), vmDef.extra()
preferredIpVersion, deleteConnectionFile); .map(xtra -> xtra.connectionFile(e.password().orElse(null),
if (data == null) { preferredIpVersion, deleteConnectionFile))
return; .ifPresent(
} cf -> channel.respond(new NotifyConletView(type(),
channel.respond(new NotifyConletView(type(), model.getConletId(), "openConsole", cf)));
model.getConletId(), "openConsole", data));
}); });
fire(pwQuery, vmChannel); fire(pwQuery, vmChannel);
} }

View file

@ -41,6 +41,7 @@ import java.util.Set;
import org.jdrupes.vmoperator.common.K8sObserver; import org.jdrupes.vmoperator.common.K8sObserver;
import org.jdrupes.vmoperator.common.VmDefinition; import org.jdrupes.vmoperator.common.VmDefinition;
import org.jdrupes.vmoperator.common.VmDefinition.Permission; import org.jdrupes.vmoperator.common.VmDefinition.Permission;
import org.jdrupes.vmoperator.common.VmExtraData;
import org.jdrupes.vmoperator.manager.events.ChannelTracker; import org.jdrupes.vmoperator.manager.events.ChannelTracker;
import org.jdrupes.vmoperator.manager.events.GetDisplayPassword; import org.jdrupes.vmoperator.manager.events.GetDisplayPassword;
import org.jdrupes.vmoperator.manager.events.ModifyVm; import org.jdrupes.vmoperator.manager.events.ModifyVm;
@ -252,7 +253,7 @@ public class VmMgmt extends FreeMarkerConlet<VmMgmt.VmsModel> {
"name", vmDef.name()), "name", vmDef.name()),
"spec", spec, "spec", spec,
"status", status, "status", status,
"nodeName", vmDef.extra("nodeName"), "nodeName", vmDef.extra().map(VmExtraData::nodeName).orElse(""),
"permissions", vmDef.permissionsFor(user, roles).stream() "permissions", vmDef.permissionsFor(user, roles).stream()
.map(VmDefinition.Permission::toString).toList()); .map(VmDefinition.Permission::toString).toList());
} }
@ -484,13 +485,11 @@ public class VmMgmt extends FreeMarkerConlet<VmMgmt.VmsModel> {
} }
var pwQuery = Event.onCompletion(new GetDisplayPassword(vmDef, user), var pwQuery = Event.onCompletion(new GetDisplayPassword(vmDef, user),
e -> { e -> {
var data = vmDef.connectionFile(e.password().orElse(null), vmDef.extra().map(xtra -> xtra.connectionFile(
preferredIpVersion, deleteConnectionFile); e.password().orElse(null), preferredIpVersion,
if (data == null) { deleteConnectionFile)).ifPresent(
return; cf -> channel.respond(new NotifyConletView(type(),
} model.getConletId(), "openConsole", cf)));
channel.respond(new NotifyConletView(type(),
model.getConletId(), "openConsole", data));
}); });
fire(pwQuery, vmChannel); fire(pwQuery, vmChannel);
} }