Merge branch 'feature/less-gson' into 'main'
Feature/less gson See merge request org/jdrupes/vm-operator!6
This commit is contained in:
commit
a9c31a378e
33 changed files with 1405 additions and 905 deletions
|
|
@ -9,6 +9,14 @@
|
||||||
"/Reconciler":
|
"/Reconciler":
|
||||||
runnerData:
|
runnerData:
|
||||||
storageClassName: null
|
storageClassName: null
|
||||||
|
loadBalancerService:
|
||||||
|
labels:
|
||||||
|
label1: label1
|
||||||
|
label2: toBeReplaced
|
||||||
|
annotations:
|
||||||
|
metallb.universe.tf/loadBalancerIPs: 192.168.168.1
|
||||||
|
metallb.universe.tf/ip-allocated-from-pool: single-common
|
||||||
|
metallb.universe.tf/allow-shared-ip: single-common
|
||||||
"/GuiSocketServer":
|
"/GuiSocketServer":
|
||||||
port: 8888
|
port: 8888
|
||||||
"/GuiHttpServer":
|
"/GuiHttpServer":
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,14 @@ patches:
|
||||||
"/Reconciler":
|
"/Reconciler":
|
||||||
runnerData:
|
runnerData:
|
||||||
storageClassName: null
|
storageClassName: null
|
||||||
|
loadBalancerService:
|
||||||
|
labels:
|
||||||
|
label1: label1
|
||||||
|
label2: toBeReplaced
|
||||||
|
annotations:
|
||||||
|
metallb.universe.tf/loadBalancerIPs: 192.168.168.1
|
||||||
|
metallb.universe.tf/ip-allocated-from-pool: single-common
|
||||||
|
metallb.universe.tf/allow-shared-ip: single-common
|
||||||
"/GuiSocketServer":
|
"/GuiSocketServer":
|
||||||
port: 8888
|
port: 8888
|
||||||
"/GuiHttpServer":
|
"/GuiHttpServer":
|
||||||
|
|
@ -43,10 +51,10 @@ patches:
|
||||||
"/WebConsole":
|
"/WebConsole":
|
||||||
"/LoginConlet":
|
"/LoginConlet":
|
||||||
users:
|
users:
|
||||||
admin:
|
- name: admin
|
||||||
fullName: Administrator
|
fullName: Administrator
|
||||||
password: "$2b$05$NiBd74ZGdplLC63ePZf1f.UtjMKkbQ23cQoO2OKOFalDBHWAOy21."
|
password: "$2b$05$NiBd74ZGdplLC63ePZf1f.UtjMKkbQ23cQoO2OKOFalDBHWAOy21."
|
||||||
test:
|
- name: test
|
||||||
fullName: Test Account
|
fullName: Test Account
|
||||||
password: "$2b$05$hZaI/jToXf/d3BctZdT38Or7H7h6Pn2W3WiB49p5AyhDHFkkYCvo2"
|
password: "$2b$05$hZaI/jToXf/d3BctZdT38Or7H7h6Pn2W3WiB49p5AyhDHFkkYCvo2"
|
||||||
"/RoleConfigurator":
|
"/RoleConfigurator":
|
||||||
|
|
|
||||||
|
|
@ -62,3 +62,5 @@ spec:
|
||||||
spice:
|
spice:
|
||||||
port: 5810
|
port: 5810
|
||||||
generateSecret: true
|
generateSecret: true
|
||||||
|
|
||||||
|
loadBalancerService: {}
|
||||||
|
|
|
||||||
|
|
@ -157,27 +157,6 @@ public class K8s {
|
||||||
return Optional.of(apiRes);
|
return Optional.of(apiRes);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Get an object from its metadata.
|
|
||||||
*
|
|
||||||
* @param <T> the generic type
|
|
||||||
* @param <LT> the generic type
|
|
||||||
* @param api the api
|
|
||||||
* @param meta the meta
|
|
||||||
* @return the object
|
|
||||||
*/
|
|
||||||
@Deprecated
|
|
||||||
@SuppressWarnings("PMD.GenericsNaming")
|
|
||||||
public static <T extends KubernetesObject, LT extends KubernetesListObject>
|
|
||||||
Optional<T>
|
|
||||||
get(GenericKubernetesApi<T, LT> api, V1ObjectMeta meta) {
|
|
||||||
var response = api.get(meta.getNamespace(), meta.getName());
|
|
||||||
if (response.isSuccess()) {
|
|
||||||
return Optional.of(response.getObject());
|
|
||||||
}
|
|
||||||
return Optional.empty();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Apply the given patch data.
|
* Apply the given patch data.
|
||||||
*
|
*
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,7 @@ import io.kubernetes.client.openapi.ApiException;
|
||||||
import io.kubernetes.client.util.Strings;
|
import io.kubernetes.client.util.Strings;
|
||||||
import io.kubernetes.client.util.generic.GenericKubernetesApi;
|
import io.kubernetes.client.util.generic.GenericKubernetesApi;
|
||||||
import io.kubernetes.client.util.generic.KubernetesApiResponse;
|
import io.kubernetes.client.util.generic.KubernetesApiResponse;
|
||||||
|
import io.kubernetes.client.util.generic.dynamic.DynamicKubernetesObject;
|
||||||
import io.kubernetes.client.util.generic.options.GetOptions;
|
import io.kubernetes.client.util.generic.options.GetOptions;
|
||||||
import io.kubernetes.client.util.generic.options.ListOptions;
|
import io.kubernetes.client.util.generic.options.ListOptions;
|
||||||
import io.kubernetes.client.util.generic.options.PatchOptions;
|
import io.kubernetes.client.util.generic.options.PatchOptions;
|
||||||
|
|
@ -47,7 +48,7 @@ import java.util.function.Function;
|
||||||
* @param <O> the generic type
|
* @param <O> the generic type
|
||||||
* @param <L> the generic type
|
* @param <L> the generic type
|
||||||
*/
|
*/
|
||||||
@SuppressWarnings("PMD.DataflowAnomalyAnalysis")
|
@SuppressWarnings({ "PMD.DataflowAnomalyAnalysis", "PMD.TooManyMethods" })
|
||||||
public class K8sGenericStub<O extends KubernetesObject,
|
public class K8sGenericStub<O extends KubernetesObject,
|
||||||
L extends KubernetesListObject> {
|
L extends KubernetesListObject> {
|
||||||
protected final K8sClient client;
|
protected final K8sClient client;
|
||||||
|
|
@ -224,7 +225,7 @@ public class K8sGenericStub<O extends KubernetesObject,
|
||||||
* @param patchType the patch type
|
* @param patchType the patch type
|
||||||
* @param patch the patch
|
* @param patch the patch
|
||||||
* @param options the options
|
* @param options the options
|
||||||
* @return the kubernetes api response
|
* @return the kubernetes api response if successful
|
||||||
* @throws ApiException the api exception
|
* @throws ApiException the api exception
|
||||||
*/
|
*/
|
||||||
public Optional<O> patch(String patchType, V1Patch patch,
|
public Optional<O> patch(String patchType, V1Patch patch,
|
||||||
|
|
@ -239,7 +240,7 @@ public class K8sGenericStub<O extends KubernetesObject,
|
||||||
*
|
*
|
||||||
* @param patchType the patch type
|
* @param patchType the patch type
|
||||||
* @param patch the patch
|
* @param patch the patch
|
||||||
* @return the kubernetes api response
|
* @return the kubernetes api response if successful
|
||||||
* @throws ApiException the api exception
|
* @throws ApiException the api exception
|
||||||
*/
|
*/
|
||||||
public Optional<O>
|
public Optional<O>
|
||||||
|
|
@ -248,6 +249,21 @@ public class K8sGenericStub<O extends KubernetesObject,
|
||||||
return patch(patchType, patch, opts);
|
return patch(patchType, patch, opts);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply the given definition.
|
||||||
|
*
|
||||||
|
* @param def the def
|
||||||
|
* @return the kubernetes api response if successful
|
||||||
|
* @throws ApiException the api exception
|
||||||
|
*/
|
||||||
|
public Optional<O> apply(DynamicKubernetesObject def) throws ApiException {
|
||||||
|
PatchOptions opts = new PatchOptions();
|
||||||
|
opts.setForce(true);
|
||||||
|
opts.setFieldManager("kubernetes-java-kubectl-apply");
|
||||||
|
return patch(V1Patch.PATCH_FORMAT_APPLY_YAML,
|
||||||
|
new V1Patch(client.getJSON().serialize(def)), opts);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update the object.
|
* Update the object.
|
||||||
*
|
*
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,332 @@
|
||||||
|
/*
|
||||||
|
* 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 io.kubernetes.client.openapi.models.V1ObjectMeta;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.EnumSet;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import java.util.function.Function;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
import org.jdrupes.vmoperator.util.DataPath;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a VM definition.
|
||||||
|
*/
|
||||||
|
@SuppressWarnings({ "PMD.DataClass" })
|
||||||
|
public class VmDefinition {
|
||||||
|
|
||||||
|
private String kind;
|
||||||
|
private String apiVersion;
|
||||||
|
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.
|
||||||
|
*/
|
||||||
|
public enum RequestedVmState {
|
||||||
|
STOPPED, RUNNING
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Permissions for accessing and manipulating the VM.
|
||||||
|
*/
|
||||||
|
public enum Permission {
|
||||||
|
START("start"), STOP("stop"), RESET("reset"),
|
||||||
|
ACCESS_CONSOLE("accessConsole");
|
||||||
|
|
||||||
|
@SuppressWarnings("PMD.UseConcurrentHashMap")
|
||||||
|
private static Map<String, Permission> reprs = new HashMap<>();
|
||||||
|
|
||||||
|
static {
|
||||||
|
for (var value : EnumSet.allOf(Permission.class)) {
|
||||||
|
reprs.put(value.repr, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private final String repr;
|
||||||
|
|
||||||
|
Permission(String repr) {
|
||||||
|
this.repr = repr;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create permission from representation in CRD.
|
||||||
|
*
|
||||||
|
* @param value the value
|
||||||
|
* @return the permission
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("PMD.AvoidLiteralsInIfCondition")
|
||||||
|
public static Set<Permission> parse(String value) {
|
||||||
|
if ("*".equals(value)) {
|
||||||
|
return EnumSet.allOf(Permission.class);
|
||||||
|
}
|
||||||
|
return Set.of(reprs.get(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return repr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the kind.
|
||||||
|
*
|
||||||
|
* @return the kind
|
||||||
|
*/
|
||||||
|
public String getKind() {
|
||||||
|
return kind;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the kind.
|
||||||
|
*
|
||||||
|
* @param kind the kind to set
|
||||||
|
*/
|
||||||
|
public void setKind(String kind) {
|
||||||
|
this.kind = kind;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the api version.
|
||||||
|
*
|
||||||
|
* @return the apiVersion
|
||||||
|
*/
|
||||||
|
public String getApiVersion() {
|
||||||
|
return apiVersion;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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}.
|
||||||
|
*
|
||||||
|
* @param <T> the generic type
|
||||||
|
* @param selectors the selectors
|
||||||
|
* @return the value, if found
|
||||||
|
*/
|
||||||
|
public <T> Optional<T> fromVm(Object... selectors) {
|
||||||
|
return DataPath.get(spec, "vm")
|
||||||
|
.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.
|
||||||
|
*
|
||||||
|
* @return the status
|
||||||
|
*/
|
||||||
|
public Map<String, Object> status() {
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a value from the status using {@link DataPath#get}.
|
||||||
|
*
|
||||||
|
* @param <T> the generic type
|
||||||
|
* @param selectors the selectors
|
||||||
|
* @return the value, if found
|
||||||
|
*/
|
||||||
|
public <T> Optional<T> fromStatus(Object... 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set extra data (locally used, unknown to kubernetes).
|
||||||
|
*
|
||||||
|
* @param property the property
|
||||||
|
* @param value the value
|
||||||
|
* @return the VM definition
|
||||||
|
*/
|
||||||
|
public VmDefinition extra(String property, Object value) {
|
||||||
|
extra.put(property, value);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return extra data.
|
||||||
|
*
|
||||||
|
* @param property the property
|
||||||
|
* @return the object
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
public <T> T extra(String property) {
|
||||||
|
return (T) extra.get(property);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the definition's name.
|
||||||
|
*
|
||||||
|
* @return the string
|
||||||
|
*/
|
||||||
|
public String name() {
|
||||||
|
return metadata.getName();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the definition's namespace.
|
||||||
|
*
|
||||||
|
* @return the string
|
||||||
|
*/
|
||||||
|
public String namespace() {
|
||||||
|
return metadata.getNamespace();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the requested VM state
|
||||||
|
*
|
||||||
|
* @return the string
|
||||||
|
*/
|
||||||
|
public RequestedVmState vmState() {
|
||||||
|
// TODO
|
||||||
|
return fromVm("state")
|
||||||
|
.map(s -> "Running".equals(s) ? RequestedVmState.RUNNING
|
||||||
|
: RequestedVmState.STOPPED)
|
||||||
|
.orElse(RequestedVmState.STOPPED);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Collect all permissions for the given user with the given roles.
|
||||||
|
*
|
||||||
|
* @param user the user
|
||||||
|
* @param roles the roles
|
||||||
|
* @return the sets the
|
||||||
|
*/
|
||||||
|
public Set<Permission> permissionsFor(String user,
|
||||||
|
Collection<String> roles) {
|
||||||
|
return this.<List<Map<String, Object>>> fromSpec("permissions")
|
||||||
|
.orElse(Collections.emptyList()).stream()
|
||||||
|
.filter(p -> DataPath.get(p, "user").map(u -> u.equals(user))
|
||||||
|
.orElse(false)
|
||||||
|
|| DataPath.get(p, "role").map(roles::contains).orElse(false))
|
||||||
|
.map(p -> DataPath.<List<String>> get(p, "may")
|
||||||
|
.orElse(Collections.emptyList()).stream())
|
||||||
|
.flatMap(Function.identity())
|
||||||
|
.map(Permission::parse).map(Set::stream)
|
||||||
|
.flatMap(Function.identity()).collect(Collectors.toSet());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the display password serial.
|
||||||
|
*
|
||||||
|
* @return the optional
|
||||||
|
*/
|
||||||
|
public Optional<Long> displayPasswordSerial() {
|
||||||
|
return this.<Number> fromStatus("displayPasswordSerial")
|
||||||
|
.map(Number::longValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -20,16 +20,6 @@ package org.jdrupes.vmoperator.common;
|
||||||
|
|
||||||
import com.google.gson.Gson;
|
import com.google.gson.Gson;
|
||||||
import com.google.gson.JsonObject;
|
import com.google.gson.JsonObject;
|
||||||
import com.google.gson.JsonPrimitive;
|
|
||||||
import java.util.Collection;
|
|
||||||
import java.util.EnumSet;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Optional;
|
|
||||||
import java.util.Set;
|
|
||||||
import java.util.function.Function;
|
|
||||||
import java.util.stream.Collectors;
|
|
||||||
import org.jdrupes.vmoperator.util.GsonPtr;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents a VM definition.
|
* Represents a VM definition.
|
||||||
|
|
@ -37,55 +27,6 @@ import org.jdrupes.vmoperator.util.GsonPtr;
|
||||||
@SuppressWarnings("PMD.DataClass")
|
@SuppressWarnings("PMD.DataClass")
|
||||||
public class VmDefinitionModel extends K8sDynamicModel {
|
public class VmDefinitionModel extends K8sDynamicModel {
|
||||||
|
|
||||||
/**
|
|
||||||
* The VM state from the VM definition.
|
|
||||||
*/
|
|
||||||
public enum RequestedVmState {
|
|
||||||
STOPPED, RUNNING
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Permissions for accessing and manipulating the VM.
|
|
||||||
*/
|
|
||||||
public enum Permission {
|
|
||||||
START("start"), STOP("stop"), RESET("reset"),
|
|
||||||
ACCESS_CONSOLE("accessConsole");
|
|
||||||
|
|
||||||
@SuppressWarnings("PMD.UseConcurrentHashMap")
|
|
||||||
private static Map<String, Permission> reprs = new HashMap<>();
|
|
||||||
|
|
||||||
static {
|
|
||||||
for (var value : EnumSet.allOf(Permission.class)) {
|
|
||||||
reprs.put(value.repr, value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private final String repr;
|
|
||||||
|
|
||||||
Permission(String repr) {
|
|
||||||
this.repr = repr;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create permission from representation in CRD.
|
|
||||||
*
|
|
||||||
* @param value the value
|
|
||||||
* @return the permission
|
|
||||||
*/
|
|
||||||
@SuppressWarnings("PMD.AvoidLiteralsInIfCondition")
|
|
||||||
public static Set<Permission> parse(String value) {
|
|
||||||
if ("*".equals(value)) {
|
|
||||||
return EnumSet.allOf(Permission.class);
|
|
||||||
}
|
|
||||||
return Set.of(reprs.get(value));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String toString() {
|
|
||||||
return repr;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Instantiates a new model from the JSON representation.
|
* Instantiates a new model from the JSON representation.
|
||||||
*
|
*
|
||||||
|
|
@ -95,49 +36,4 @@ public class VmDefinitionModel extends K8sDynamicModel {
|
||||||
public VmDefinitionModel(Gson delegate, JsonObject json) {
|
public VmDefinitionModel(Gson delegate, JsonObject json) {
|
||||||
super(delegate, json);
|
super(delegate, json);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Collect all permissions for the given user with the given roles.
|
|
||||||
*
|
|
||||||
* @param user the user
|
|
||||||
* @param roles the roles
|
|
||||||
* @return the sets the
|
|
||||||
*/
|
|
||||||
public Set<Permission> permissionsFor(String user,
|
|
||||||
Collection<String> roles) {
|
|
||||||
return GsonPtr.to(data())
|
|
||||||
.getAsListOf(JsonObject.class, "spec", "permissions")
|
|
||||||
.stream().filter(p -> GsonPtr.to(p).getAsString("user")
|
|
||||||
.map(u -> u.equals(user)).orElse(false)
|
|
||||||
|| GsonPtr.to(p).getAsString("role").map(roles::contains)
|
|
||||||
.orElse(false))
|
|
||||||
.map(p -> GsonPtr.to(p).getAsListOf(JsonPrimitive.class, "may")
|
|
||||||
.stream())
|
|
||||||
.flatMap(Function.identity()).map(p -> p.getAsString())
|
|
||||||
.map(Permission::parse).map(Set::stream)
|
|
||||||
.flatMap(Function.identity()).collect(Collectors.toSet());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return the requested VM state
|
|
||||||
*
|
|
||||||
* @return the string
|
|
||||||
*/
|
|
||||||
public RequestedVmState vmState() {
|
|
||||||
return GsonPtr.to(data()).getAsString("spec", "vm", "state")
|
|
||||||
.map(s -> "Running".equals(s) ? RequestedVmState.RUNNING
|
|
||||||
: RequestedVmState.STOPPED)
|
|
||||||
.orElse(RequestedVmState.STOPPED);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the display password serial.
|
|
||||||
*
|
|
||||||
* @return the optional
|
|
||||||
*/
|
|
||||||
public Optional<Long> displayPasswordSerial() {
|
|
||||||
return GsonPtr.to(status())
|
|
||||||
.get(JsonPrimitive.class, "displayPasswordSerial")
|
|
||||||
.map(JsonPrimitive::getAsLong);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@
|
||||||
package org.jdrupes.vmoperator.manager.events;
|
package org.jdrupes.vmoperator.manager.events;
|
||||||
|
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import org.jdrupes.vmoperator.common.VmDefinitionModel;
|
import org.jdrupes.vmoperator.common.VmDefinition;
|
||||||
import org.jgrapes.core.Event;
|
import org.jgrapes.core.Event;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -28,14 +28,14 @@ import org.jgrapes.core.Event;
|
||||||
@SuppressWarnings("PMD.DataClass")
|
@SuppressWarnings("PMD.DataClass")
|
||||||
public class GetDisplayPassword extends Event<String> {
|
public class GetDisplayPassword extends Event<String> {
|
||||||
|
|
||||||
private final VmDefinitionModel vmDef;
|
private final VmDefinition vmDef;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Instantiates a new returns the display secret.
|
* Instantiates a new returns the display secret.
|
||||||
*
|
*
|
||||||
* @param vmDef the vm name
|
* @param vmDef the vm name
|
||||||
*/
|
*/
|
||||||
public GetDisplayPassword(VmDefinitionModel vmDef) {
|
public GetDisplayPassword(VmDefinition vmDef) {
|
||||||
this.vmDef = vmDef;
|
this.vmDef = vmDef;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -44,7 +44,7 @@ public class GetDisplayPassword extends Event<String> {
|
||||||
*
|
*
|
||||||
* @return the vm definition
|
* @return the vm definition
|
||||||
*/
|
*/
|
||||||
public VmDefinitionModel vmDefinition() {
|
public VmDefinition vmDefinition() {
|
||||||
return vmDef;
|
return vmDef;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@
|
||||||
package org.jdrupes.vmoperator.manager.events;
|
package org.jdrupes.vmoperator.manager.events;
|
||||||
|
|
||||||
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.jgrapes.core.Channel;
|
import org.jgrapes.core.Channel;
|
||||||
import org.jgrapes.core.EventPipeline;
|
import org.jgrapes.core.EventPipeline;
|
||||||
import org.jgrapes.core.Subchannel.DefaultSubchannel;
|
import org.jgrapes.core.Subchannel.DefaultSubchannel;
|
||||||
|
|
@ -32,7 +32,7 @@ public class VmChannel extends DefaultSubchannel {
|
||||||
|
|
||||||
private final EventPipeline pipeline;
|
private final EventPipeline pipeline;
|
||||||
private final K8sClient client;
|
private final K8sClient client;
|
||||||
private VmDefinitionModel vmDefinition;
|
private VmDefinition definition;
|
||||||
private long generation = -1;
|
private long generation = -1;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -56,18 +56,18 @@ public class VmChannel extends DefaultSubchannel {
|
||||||
* @return the watch channel
|
* @return the watch channel
|
||||||
*/
|
*/
|
||||||
@SuppressWarnings("PMD.LinguisticNaming")
|
@SuppressWarnings("PMD.LinguisticNaming")
|
||||||
public VmChannel setVmDefinition(VmDefinitionModel definition) {
|
public VmChannel setVmDefinition(VmDefinition definition) {
|
||||||
this.vmDefinition = definition;
|
this.definition = definition;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the last known definition of the VM.
|
* Returns the last known definition of the VM.
|
||||||
*
|
*
|
||||||
* @return the json object
|
* @return the defintion
|
||||||
*/
|
*/
|
||||||
public VmDefinitionModel vmDefinition() {
|
public VmDefinition vmDefinition() {
|
||||||
return vmDefinition;
|
return definition;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@
|
||||||
package org.jdrupes.vmoperator.manager.events;
|
package org.jdrupes.vmoperator.manager.events;
|
||||||
|
|
||||||
import org.jdrupes.vmoperator.common.K8sObserver;
|
import org.jdrupes.vmoperator.common.K8sObserver;
|
||||||
import org.jdrupes.vmoperator.common.VmDefinitionModel;
|
import org.jdrupes.vmoperator.common.VmDefinition;
|
||||||
import org.jgrapes.core.Channel;
|
import org.jgrapes.core.Channel;
|
||||||
import org.jgrapes.core.Components;
|
import org.jgrapes.core.Components;
|
||||||
import org.jgrapes.core.Event;
|
import org.jgrapes.core.Event;
|
||||||
|
|
@ -36,7 +36,7 @@ public class VmDefChanged extends Event<Void> {
|
||||||
|
|
||||||
private final K8sObserver.ResponseType type;
|
private final K8sObserver.ResponseType type;
|
||||||
private final boolean specChanged;
|
private final boolean specChanged;
|
||||||
private final VmDefinitionModel vmDef;
|
private final VmDefinition vmDefinition;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Instantiates a new VM changed event.
|
* Instantiates a new VM changed event.
|
||||||
|
|
@ -46,10 +46,10 @@ public class VmDefChanged extends Event<Void> {
|
||||||
* @param vmDefinition the VM definition
|
* @param vmDefinition the VM definition
|
||||||
*/
|
*/
|
||||||
public VmDefChanged(K8sObserver.ResponseType type, boolean specChanged,
|
public VmDefChanged(K8sObserver.ResponseType type, boolean specChanged,
|
||||||
VmDefinitionModel vmDefinition) {
|
VmDefinition vmDefinition) {
|
||||||
this.type = type;
|
this.type = type;
|
||||||
this.specChanged = specChanged;
|
this.specChanged = specChanged;
|
||||||
this.vmDef = vmDefinition;
|
this.vmDefinition = vmDefinition;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -69,19 +69,19 @@ public class VmDefChanged extends Event<Void> {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the object.
|
* Return the VM definition.
|
||||||
*
|
*
|
||||||
* @return the object.
|
* @return the VM definition
|
||||||
*/
|
*/
|
||||||
public VmDefinitionModel vmDefinition() {
|
public VmDefinition vmDefinition() {
|
||||||
return vmDef;
|
return vmDefinition;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String toString() {
|
public String toString() {
|
||||||
StringBuilder builder = new StringBuilder();
|
StringBuilder builder = new StringBuilder();
|
||||||
builder.append(Components.objectName(this)).append(" [")
|
builder.append(Components.objectName(this)).append(" [")
|
||||||
.append(vmDef.getMetadata().getName()).append(' ').append(type);
|
.append(vmDefinition.name()).append(' ').append(type);
|
||||||
if (channels() != null) {
|
if (channels() != null) {
|
||||||
builder.append(", channels=").append(Channel.toString(channels()));
|
builder.append(", channels=").append(Channel.toString(channels()));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,19 @@
|
||||||
apiVersion: v1
|
apiVersion: v1
|
||||||
kind: ConfigMap
|
kind: ConfigMap
|
||||||
metadata:
|
metadata:
|
||||||
namespace: ${ cr.metadata.namespace.asString }
|
namespace: ${ cr.namespace() }
|
||||||
name: ${ cr.metadata.name.asString }
|
name: ${ cr.name() }
|
||||||
labels:
|
labels:
|
||||||
app.kubernetes.io/name: ${ constants.APP_NAME }
|
app.kubernetes.io/name: ${ constants.APP_NAME }
|
||||||
app.kubernetes.io/instance: ${ cr.metadata.name.asString }
|
app.kubernetes.io/instance: ${ cr.name() }
|
||||||
app.kubernetes.io/managed-by: ${ constants.VM_OP_NAME }
|
app.kubernetes.io/managed-by: ${ constants.VM_OP_NAME }
|
||||||
annotations:
|
annotations:
|
||||||
vmoperator.jdrupes.org/version: ${ managerVersion }
|
vmoperator.jdrupes.org/version: ${ managerVersion }
|
||||||
ownerReferences:
|
ownerReferences:
|
||||||
- apiVersion: ${ cr.apiVersion.asString }
|
- apiVersion: ${ cr.apiVersion }
|
||||||
kind: ${ constants.VM_OP_KIND_VM }
|
kind: ${ constants.VM_OP_KIND_VM }
|
||||||
name: ${ cr.metadata.name.asString }
|
name: ${ cr.name() }
|
||||||
uid: ${ cr.metadata.uid.asString }
|
uid: ${ cr.metadata().getUid() }
|
||||||
controller: false
|
controller: false
|
||||||
|
|
||||||
data:
|
data:
|
||||||
|
|
@ -21,121 +21,122 @@ data:
|
||||||
"/Runner":
|
"/Runner":
|
||||||
# The directory used to store data files. Defaults to (depending on
|
# The directory used to store data files. Defaults to (depending on
|
||||||
# values available):
|
# values available):
|
||||||
# * $XDG_DATA_HOME/vmrunner/${ cr.metadata.name.asString }
|
# * $XDG_DATA_HOME/vmrunner/${ cr.name() }
|
||||||
# * $HOME/.local/share/vmrunner/${ cr.metadata.name.asString }
|
# * $HOME/.local/share/vmrunner/${ cr.name() }
|
||||||
# * ./${ cr.metadata.name.asString }
|
# * ./${ cr.name() }
|
||||||
dataDir: /var/local/vm-data
|
dataDir: /var/local/vm-data
|
||||||
|
|
||||||
# The directory used to store runtime files. Defaults to (depending on
|
# The directory used to store runtime files. Defaults to (depending on
|
||||||
# values available):
|
# values available):
|
||||||
# * $XDG_RUNTIME_DIR/vmrunner/${ cr.metadata.name.asString }
|
# * $XDG_RUNTIME_DIR/vmrunner/${ cr.name() }
|
||||||
# * /tmp/$USER/vmrunner/${ cr.metadata.name.asString }
|
# * /tmp/$USER/vmrunner/${ cr.name() }
|
||||||
# * /tmp/vmrunner/${ cr.metadata.name.asString }
|
# * /tmp/vmrunner/${ cr.name() }
|
||||||
# runtimeDir: "$XDG_RUNTIME_DIR/vmrunner/${ cr.metadata.name.asString }"
|
# runtimeDir: "$XDG_RUNTIME_DIR/vmrunner/${ cr.name() }"
|
||||||
|
|
||||||
|
<#assign spec = cr.spec() />
|
||||||
# The template to use. Resolved relative to /usr/share/vmrunner/templates.
|
# The template to use. Resolved relative to /usr/share/vmrunner/templates.
|
||||||
# template: "Standard-VM-latest.ftl.yaml"
|
# template: "Standard-VM-latest.ftl.yaml"
|
||||||
<#if cr.spec.runnerTemplate?? && cr.spec.runnerTemplate.source?? >
|
<#if spec.runnerTemplate?? && spec.runnerTemplate.source?? >
|
||||||
template: ${ cr.spec.runnerTemplate.source.asString }
|
template: ${ cm.spec().runnerTemplate.source }
|
||||||
</#if>
|
</#if>
|
||||||
|
|
||||||
# The template is copied to the data diretory when the VM starts for
|
# The template is copied to the data diretory when the VM starts for
|
||||||
# the first time. Subsequent starts use the copy unless this option is set.
|
# the first time. Subsequent starts use the copy unless this option is set.
|
||||||
<#if cr.spec.runnerTemplate?? && cr.spec.runnerTemplate.update?? >
|
<#if spec.runnerTemplate?? && spec.runnerTemplate.update?? >
|
||||||
updateTemplate: ${ cr.spec.runnerTemplate.update.asBoolean?c }
|
updateTemplate: ${ spec.runnerTemplate.update?c }
|
||||||
</#if>
|
</#if>
|
||||||
|
|
||||||
# Whether a shutdown initiated by the guest stops the pod deployment
|
# Whether a shutdown initiated by the guest stops the pod deployment
|
||||||
guestShutdownStops: ${ cr.spec.guestShutdownStops!false?c }
|
guestShutdownStops: ${ (spec.guestShutdownStops!false)?c }
|
||||||
|
|
||||||
# When incremented, the VM is reset. The value has no default value,
|
# When incremented, the VM is reset. The value has no default value,
|
||||||
# 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.resetCount }
|
resetCounter: ${ cr.extra("resetCount")?c }
|
||||||
|
|
||||||
# Forward the cloud-init data if provided
|
# Forward the cloud-init data if provided
|
||||||
<#if cr.spec.cloudInit??>
|
<#if spec.cloudInit??>
|
||||||
cloudInit:
|
cloudInit:
|
||||||
<#if cr.spec.cloudInit.metaData??>
|
<#if spec.cloudInit.metaData??>
|
||||||
metaData: ${ cr.spec.cloudInit.metaData.toString() }
|
metaData: ${ toJson(adjustCloudInitMeta(spec.cloudInit.metaData, cr.metadata())) }
|
||||||
<#else>
|
<#else>
|
||||||
metaData: {}
|
metaData: {}
|
||||||
</#if>
|
</#if>
|
||||||
<#if cr.spec.cloudInit.userData??>
|
<#if spec.cloudInit.userData??>
|
||||||
userData: ${ cr.spec.cloudInit.userData.toString() }
|
userData: ${ toJson(spec.cloudInit.userData) }
|
||||||
<#else>
|
<#else>
|
||||||
userData: {}
|
userData: {}
|
||||||
</#if>
|
</#if>
|
||||||
<#if cr.spec.cloudInit.networkConfig??>
|
<#if spec.cloudInit.networkConfig??>
|
||||||
networkConfig: ${ cr.spec.cloudInit.networkConfig.toString() }
|
networkConfig: ${ toJson(spec.cloudInit.networkConfig) }
|
||||||
</#if>
|
</#if>
|
||||||
</#if>
|
</#if>
|
||||||
|
|
||||||
# Define the VM (required)
|
# Define the VM (required)
|
||||||
vm:
|
vm:
|
||||||
# The VM's name (required)
|
# The VM's name (required)
|
||||||
name: ${ cr.metadata.name.asString }
|
name: ${ cr.name() }
|
||||||
|
|
||||||
# The machine's uuid. If none is specified, a uuid is generated
|
# The machine's uuid. If none is specified, a uuid is generated
|
||||||
# and stored in the data directory. If the uuid is important
|
# and stored in the data directory. If the uuid is important
|
||||||
# (e.g. because licenses depend on it) it is recommaned to specify
|
# (e.g. because licenses depend on it) it is recommaned to specify
|
||||||
# it here explicitly or to carefully backup the data directory.
|
# it here explicitly or to carefully backup the data directory.
|
||||||
# uuid: "generated uuid"
|
# uuid: "generated uuid"
|
||||||
<#if cr.spec.vm.machineUuid??>
|
<#if spec.vm.machineUuid??>
|
||||||
uuid: "${ cr.spec.vm.machineUuid.asString }"
|
uuid: "${ spec.vm.machineUuid }"
|
||||||
</#if>
|
</#if>
|
||||||
|
|
||||||
# Whether to provide a software TPM (defaults to false)
|
# Whether to provide a software TPM (defaults to false)
|
||||||
# useTpm: false
|
# useTpm: false
|
||||||
useTpm: ${ cr.spec.vm.useTpm.asBoolean?c }
|
useTpm: ${ spec.vm.useTpm?c }
|
||||||
|
|
||||||
# How to boot (see https://github.com/mnlipp/VM-Operator/blob/main/org.jdrupes.vmoperator.runner.qemu/resources/org/jdrupes/vmoperator/runner/qemu/defaults.yaml):
|
# How to boot (see https://github.com/mnlipp/VM-Operator/blob/main/org.jdrupes.vmoperator.runner.qemu/resources/org/jdrupes/vmoperator/runner/qemu/defaults.yaml):
|
||||||
# * bios
|
# * bios
|
||||||
# * uefi[-4m]
|
# * uefi[-4m]
|
||||||
# * secure[-4m]
|
# * secure[-4m]
|
||||||
firmware: ${ cr.spec.vm.firmware.asString }
|
firmware: ${ spec.vm.firmware }
|
||||||
|
|
||||||
# Whether to show a boot menu.
|
# Whether to show a boot menu.
|
||||||
# bootMenu: false
|
# bootMenu: false
|
||||||
bootMenu: ${ cr.spec.vm.bootMenu.asBoolean?c }
|
bootMenu: ${ spec.vm.bootMenu?c }
|
||||||
|
|
||||||
# When terminating, a graceful powerdown is attempted. If it
|
# When terminating, a graceful powerdown is attempted. If it
|
||||||
# doesn't succeed within the given timeout (seconds) SIGTERM
|
# doesn't succeed within the given timeout (seconds) SIGTERM
|
||||||
# is sent to Qemu.
|
# is sent to Qemu.
|
||||||
# powerdownTimeout: 900
|
# powerdownTimeout: 900
|
||||||
powerdownTimeout: ${ cr.spec.vm.powerdownTimeout.asLong?c }
|
powerdownTimeout: ${ spec.vm.powerdownTimeout?c }
|
||||||
|
|
||||||
# CPU settings
|
# CPU settings
|
||||||
cpuModel: ${ cr.spec.vm.cpuModel.asString }
|
cpuModel: ${ spec.vm.cpuModel }
|
||||||
# Setting maximumCpus to 1 omits the "-smp" options. The defaults (0)
|
# Setting maximumCpus to 1 omits the "-smp" options. The defaults (0)
|
||||||
# cause the corresponding property to be omitted from the "-smp" option.
|
# cause the corresponding property to be omitted from the "-smp" option.
|
||||||
# If currentCpus is greater than maximumCpus, the latter is adjusted.
|
# If currentCpus is greater than maximumCpus, the latter is adjusted.
|
||||||
<#if cr.spec.vm.maximumCpus?? >
|
<#if spec.vm.maximumCpus?? >
|
||||||
maximumCpus: ${ parseQuantity(cr.spec.vm.maximumCpus.asString)?c }
|
maximumCpus: ${ parseQuantity(spec.vm.maximumCpus)?c }
|
||||||
</#if>
|
</#if>
|
||||||
<#if cr.spec.vm.cpuTopology?? >
|
<#if spec.vm.cpuTopology?? >
|
||||||
sockets: ${ cr.spec.vm.cpuTopology.sockets.asInt?c }
|
sockets: ${ spec.vm.cpuTopology.sockets?c }
|
||||||
diesPerSocket: ${ cr.spec.vm.cpuTopology.diesPerSocket.asInt?c }
|
diesPerSocket: ${ spec.vm.cpuTopology.diesPerSocket?c }
|
||||||
coresPerDie: ${ cr.spec.vm.cpuTopology.coresPerDie.asInt?c }
|
coresPerDie: ${ spec.vm.cpuTopology.coresPerDie?c }
|
||||||
threadsPerCore: ${ cr.spec.vm.cpuTopology.threadsPerCore.asInt?c }
|
threadsPerCore: ${ spec.vm.cpuTopology.threadsPerCore?c }
|
||||||
</#if>
|
</#if>
|
||||||
<#if cr.spec.vm.currentCpus?? >
|
<#if spec.vm.currentCpus?? >
|
||||||
currentCpus: ${ parseQuantity(cr.spec.vm.currentCpus.asString)?c }
|
currentCpus: ${ parseQuantity(spec.vm.currentCpus)?c }
|
||||||
</#if>
|
</#if>
|
||||||
|
|
||||||
# RAM settings
|
# RAM settings
|
||||||
# Maximum defaults to 1G
|
# Maximum defaults to 1G
|
||||||
maximumRam: "${ formatMemory(parseQuantity(cr.spec.vm.maximumRam.asString)) }"
|
maximumRam: "${ formatMemory(parseQuantity(spec.vm.maximumRam)) }"
|
||||||
<#if cr.spec.vm.currentRam?? >
|
<#if spec.vm.currentRam?? >
|
||||||
currentRam: "${ formatMemory(parseQuantity(cr.spec.vm.currentRam.asString)) }"
|
currentRam: "${ formatMemory(parseQuantity(spec.vm.currentRam)) }"
|
||||||
</#if>
|
</#if>
|
||||||
|
|
||||||
# RTC settings.
|
# RTC settings.
|
||||||
# rtcBase: utc
|
# rtcBase: utc
|
||||||
# rtcClock: rt
|
# rtcClock: rt
|
||||||
rtcBase: ${ cr.spec.vm.rtcBase.asString }
|
rtcBase: ${ spec.vm.rtcBase }
|
||||||
rtcClock: ${ cr.spec.vm.rtcClock.asString }
|
rtcClock: ${ spec.vm.rtcClock }
|
||||||
|
|
||||||
# Network settings
|
# Network settings
|
||||||
# Supported types are "tap" and "user" (for debugging). Type "user"
|
# Supported types are "tap" and "user" (for debugging). Type "user"
|
||||||
|
|
@ -147,19 +148,19 @@ data:
|
||||||
# mac: (undefined)
|
# mac: (undefined)
|
||||||
network:
|
network:
|
||||||
<#assign nwCounter = 0/>
|
<#assign nwCounter = 0/>
|
||||||
<#list cr.spec.vm.networks.asList() as itf>
|
<#list spec.vm.networks as itf>
|
||||||
<#if itf.tap??>
|
<#if itf.tap??>
|
||||||
- type: tap
|
- type: tap
|
||||||
device: ${ itf.tap.device.asString }
|
device: ${ itf.tap.device }
|
||||||
bridge: ${ itf.tap.bridge.asString }
|
bridge: ${ itf.tap.bridge }
|
||||||
<#if itf.tap.mac??>
|
<#if itf.tap.mac??>
|
||||||
mac: "${ itf.tap.mac.asString }"
|
mac: "${ itf.tap.mac }"
|
||||||
</#if>
|
</#if>
|
||||||
<#elseif itf.user??>
|
<#elseif itf.user??>
|
||||||
- type: user
|
- type: user
|
||||||
device: ${ itf.user.device.asString }
|
device: ${ itf.user.device }
|
||||||
<#if itf.user.net??>
|
<#if itf.user.net??>
|
||||||
net: "${ itf.user.net.asString }"
|
net: "${ itf.user.net }"
|
||||||
</#if>
|
</#if>
|
||||||
</#if>
|
</#if>
|
||||||
<#assign nwCounter += 1/>
|
<#assign nwCounter += 1/>
|
||||||
|
|
@ -175,11 +176,11 @@ data:
|
||||||
# file: (undefined)
|
# file: (undefined)
|
||||||
drives:
|
drives:
|
||||||
<#assign drvCounter = 0/>
|
<#assign drvCounter = 0/>
|
||||||
<#list cr.spec.vm.disks.asList() as disk>
|
<#list spec.vm.disks as disk>
|
||||||
<#if disk.volumeClaimTemplate??
|
<#if disk.volumeClaimTemplate??
|
||||||
&& disk.volumeClaimTemplate.metadata??
|
&& disk.volumeClaimTemplate.metadata??
|
||||||
&& disk.volumeClaimTemplate.metadata.name??>
|
&& disk.volumeClaimTemplate.metadata.name??>
|
||||||
<#assign diskName = disk.volumeClaimTemplate.metadata.name.asString + "-disk">
|
<#assign diskName = disk.volumeClaimTemplate.metadata.name + "-disk">
|
||||||
<#else>
|
<#else>
|
||||||
<#assign diskName = "disk-" + drvCounter>
|
<#assign diskName = "disk-" + drvCounter>
|
||||||
</#if>
|
</#if>
|
||||||
|
|
@ -187,33 +188,33 @@ data:
|
||||||
- type: raw
|
- type: raw
|
||||||
resource: /dev/${ diskName }
|
resource: /dev/${ diskName }
|
||||||
<#if disk.bootindex??>
|
<#if disk.bootindex??>
|
||||||
bootindex: ${ disk.bootindex.asInt?c }
|
bootindex: ${ disk.bootindex?c }
|
||||||
</#if>
|
</#if>
|
||||||
<#assign drvCounter = drvCounter + 1/>
|
<#assign drvCounter = drvCounter + 1/>
|
||||||
</#if>
|
</#if>
|
||||||
<#if disk.cdrom??>
|
<#if disk.cdrom??>
|
||||||
- type: ide-cd
|
- type: ide-cd
|
||||||
file: "${ disk.cdrom.image.asString }"
|
file: "${ imageLocation(disk.cdrom.image) }"
|
||||||
<#if disk.bootindex??>
|
<#if disk.bootindex??>
|
||||||
bootindex: ${ disk.bootindex.asInt?c }
|
bootindex: ${ disk.bootindex?c }
|
||||||
</#if>
|
</#if>
|
||||||
</#if>
|
</#if>
|
||||||
</#list>
|
</#list>
|
||||||
|
|
||||||
display:
|
display:
|
||||||
<#if cr.spec.vm.display.outputs?? >
|
<#if spec.vm.display.outputs?? >
|
||||||
outputs: ${ cr.spec.vm.display.outputs.asInt?c }
|
outputs: ${ spec.vm.display.outputs?c }
|
||||||
</#if>
|
</#if>
|
||||||
<#if cr.spec.vm.display.spice??>
|
<#if spec.vm.display.spice??>
|
||||||
spice:
|
spice:
|
||||||
port: ${ cr.spec.vm.display.spice.port.asInt?c }
|
port: ${ spec.vm.display.spice.port?c }
|
||||||
<#if cr.spec.vm.display.spice.ticket??>
|
<#if spec.vm.display.spice.ticket??>
|
||||||
ticket: "${ cr.spec.vm.display.spice.ticket.asString }"
|
ticket: "${ spec.vm.display.spice.ticket }"
|
||||||
</#if>
|
</#if>
|
||||||
<#if cr.spec.vm.display.spice.streamingVideo??>
|
<#if spec.vm.display.spice.streamingVideo??>
|
||||||
streaming-video: "${ cr.spec.vm.display.spice.streamingVideo.asString }"
|
streaming-video: "${ spec.vm.display.spice.streamingVideo }"
|
||||||
</#if>
|
</#if>
|
||||||
usbRedirects: ${ cr.spec.vm.display.spice.usbRedirects.asInt?c }
|
usbRedirects: ${ spec.vm.display.spice.usbRedirects?c }
|
||||||
</#if>
|
</#if>
|
||||||
|
|
||||||
logging.properties: |
|
logging.properties: |
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
kind: PersistentVolumeClaim
|
kind: PersistentVolumeClaim
|
||||||
apiVersion: v1
|
apiVersion: v1
|
||||||
metadata:
|
metadata:
|
||||||
namespace: ${ cr.metadata.namespace.asString }
|
namespace: ${ cr.namespace() }
|
||||||
name: ${ runnerDataPvcName }
|
name: ${ runnerDataPvcName }
|
||||||
labels:
|
labels:
|
||||||
app.kubernetes.io/name: ${ constants.APP_NAME }
|
app.kubernetes.io/name: ${ constants.APP_NAME }
|
||||||
app.kubernetes.io/instance: ${ cr.metadata.name.asString }
|
app.kubernetes.io/instance: ${ cr.name() }
|
||||||
app.kubernetes.io/managed-by: ${ constants.VM_OP_NAME }
|
app.kubernetes.io/managed-by: ${ constants.VM_OP_NAME }
|
||||||
spec:
|
spec:
|
||||||
accessModes:
|
accessModes:
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,16 @@
|
||||||
kind: PersistentVolumeClaim
|
kind: PersistentVolumeClaim
|
||||||
apiVersion: v1
|
apiVersion: v1
|
||||||
metadata:
|
metadata:
|
||||||
namespace: ${ cr.metadata.namespace.asString }
|
namespace: ${ cr.namespace() }
|
||||||
name: ${ disk.generatedPvcName }
|
name: ${ disk.generatedPvcName }
|
||||||
labels:
|
labels:
|
||||||
app.kubernetes.io/name: ${ constants.APP_NAME }
|
app.kubernetes.io/name: ${ constants.APP_NAME }
|
||||||
app.kubernetes.io/instance: ${ cr.metadata.name.asString }
|
app.kubernetes.io/instance: ${ cr.name() }
|
||||||
app.kubernetes.io/managed-by: ${ constants.VM_OP_NAME }
|
app.kubernetes.io/managed-by: ${ constants.VM_OP_NAME }
|
||||||
<#if disk.volumeClaimTemplate.metadata??
|
<#if disk.volumeClaimTemplate.metadata??
|
||||||
&& disk.volumeClaimTemplate.metadata.annotations??>
|
&& disk.volumeClaimTemplate.metadata.annotations??>
|
||||||
annotations:
|
annotations:
|
||||||
${ disk.volumeClaimTemplate.metadata.annotations.toString() }
|
${ toJson(disk.volumeClaimTemplate.metadata.annotations) }
|
||||||
</#if>
|
</#if>
|
||||||
spec:
|
spec:
|
||||||
${ disk.volumeClaimTemplate.spec.toString() }
|
${ toJson(disk.volumeClaimTemplate.spec) }
|
||||||
|
|
|
||||||
|
|
@ -1,26 +1,26 @@
|
||||||
apiVersion: v1
|
apiVersion: v1
|
||||||
kind: Service
|
kind: Service
|
||||||
metadata:
|
metadata:
|
||||||
namespace: ${ cr.metadata.namespace.asString }
|
namespace: ${ cr.namespace() }
|
||||||
name: ${ cr.metadata.name.asString }
|
name: ${ cr.name() }
|
||||||
labels:
|
labels:
|
||||||
app.kubernetes.io/name: ${ constants.APP_NAME }
|
app.kubernetes.io/name: ${ constants.APP_NAME }
|
||||||
app.kubernetes.io/instance: ${ cr.metadata.name.asString }
|
app.kubernetes.io/instance: ${ cr.name() }
|
||||||
app.kubernetes.io/managed-by: ${ constants.VM_OP_NAME }
|
app.kubernetes.io/managed-by: ${ constants.VM_OP_NAME }
|
||||||
annotations:
|
annotations:
|
||||||
vmoperator.jdrupes.org/version: ${ managerVersion }
|
vmoperator.jdrupes.org/version: ${ managerVersion }
|
||||||
ownerReferences:
|
ownerReferences:
|
||||||
- apiVersion: ${ cr.apiVersion.asString }
|
- apiVersion: ${ cr.apiVersion }
|
||||||
kind: ${ constants.VM_OP_KIND_VM }
|
kind: ${ constants.VM_OP_KIND_VM }
|
||||||
name: ${ cr.metadata.name.asString }
|
name: ${ cr.name() }
|
||||||
uid: ${ cr.metadata.uid.asString }
|
uid: ${ cr.metadata().getUid() }
|
||||||
controller: false
|
controller: false
|
||||||
|
|
||||||
spec:
|
spec:
|
||||||
type: LoadBalancer
|
type: LoadBalancer
|
||||||
ports:
|
ports:
|
||||||
- name: spice
|
- name: spice
|
||||||
port: ${ cr.spec.vm.display.spice.port.asInt?c }
|
port: ${ cr.spec().vm.display.spice.port?c }
|
||||||
selector:
|
selector:
|
||||||
app.kubernetes.io/name: ${ constants.APP_NAME }
|
app.kubernetes.io/name: ${ constants.APP_NAME }
|
||||||
app.kubernetes.io/instance: ${ cr.metadata.name.asString }
|
app.kubernetes.io/instance: ${ cr.name() }
|
||||||
|
|
|
||||||
|
|
@ -1,42 +1,43 @@
|
||||||
kind: Pod
|
kind: Pod
|
||||||
apiVersion: v1
|
apiVersion: v1
|
||||||
metadata:
|
metadata:
|
||||||
namespace: ${ cr.metadata.namespace.asString }
|
namespace: ${ cr.namespace() }
|
||||||
name: ${ cr.metadata.name.asString }
|
name: ${ cr.name() }
|
||||||
labels:
|
labels:
|
||||||
app.kubernetes.io/name: ${ constants.APP_NAME }
|
app.kubernetes.io/name: ${ constants.APP_NAME }
|
||||||
app.kubernetes.io/instance: ${ cr.metadata.name.asString }
|
app.kubernetes.io/instance: ${ cr.name() }
|
||||||
app.kubernetes.io/component: ${ constants.APP_NAME }
|
app.kubernetes.io/component: ${ constants.APP_NAME }
|
||||||
app.kubernetes.io/managed-by: ${ constants.VM_OP_NAME }
|
app.kubernetes.io/managed-by: ${ constants.VM_OP_NAME }
|
||||||
annotations:
|
annotations:
|
||||||
# Triggers update of config map mounted in pod
|
# Triggers update of config map mounted in pod
|
||||||
# See https://ahmet.im/blog/kubernetes-secret-volumes-delay/
|
# See https://ahmet.im/blog/kubernetes-secret-volumes-delay/
|
||||||
vmrunner.jdrupes.org/cmVersion: "${ cm.metadata.resourceVersion.asString }"
|
vmrunner.jdrupes.org/cmVersion: "${ cm.metadata.resourceVersion }"
|
||||||
vmoperator.jdrupes.org/version: ${ managerVersion }
|
vmoperator.jdrupes.org/version: ${ managerVersion }
|
||||||
ownerReferences:
|
ownerReferences:
|
||||||
- apiVersion: ${ cr.apiVersion.asString }
|
- apiVersion: ${ cr.apiVersion }
|
||||||
kind: ${ constants.VM_OP_KIND_VM }
|
kind: ${ constants.VM_OP_KIND_VM }
|
||||||
name: ${ cr.metadata.name.asString }
|
name: ${ cr.name() }
|
||||||
uid: ${ cr.metadata.uid.asString }
|
uid: ${ cr.metadata().getUid() }
|
||||||
blockOwnerDeletion: true
|
blockOwnerDeletion: true
|
||||||
controller: false
|
controller: false
|
||||||
|
<#assign spec = cr.spec() />
|
||||||
spec:
|
spec:
|
||||||
containers:
|
containers:
|
||||||
- name: ${ cr.metadata.name.asString }
|
- name: ${ cr.name() }
|
||||||
<#assign image = cr.spec.image>
|
<#assign image = spec.image>
|
||||||
<#if image.source??>
|
<#if image.source??>
|
||||||
image: ${ image.source.asString }
|
image: ${ image.source }
|
||||||
<#else>
|
<#else>
|
||||||
image: ${ image.repository.asString }/${ image.path.asString }<#if image.version??>:${ image.version.asString }</#if>
|
image: ${ image.repository }/${ image.path }<#if image.version??>:${ image.version }</#if>
|
||||||
</#if>
|
</#if>
|
||||||
<#if image.pullPolicy??>
|
<#if image.pullPolicy??>
|
||||||
imagePullPolicy: ${ image.pullPolicy.asString }
|
imagePullPolicy: ${ image.pullPolicy }
|
||||||
</#if>
|
</#if>
|
||||||
<#if cr.spec.vm.display.spice??>
|
<#if spec.vm.display.spice??>
|
||||||
ports:
|
ports:
|
||||||
<#if cr.spec.vm.display.spice??>
|
<#if spec.vm.display.spice??>
|
||||||
- name: spice
|
- name: spice
|
||||||
containerPort: ${ cr.spec.vm.display.spice.port.asInt?c }
|
containerPort: ${ spec.vm.display.spice.port?c }
|
||||||
protocol: TCP
|
protocol: TCP
|
||||||
</#if>
|
</#if>
|
||||||
</#if>
|
</#if>
|
||||||
|
|
@ -55,33 +56,33 @@ spec:
|
||||||
- name: vmop-image-repository
|
- name: vmop-image-repository
|
||||||
mountPath: ${ constants.IMAGE_REPO_PATH }
|
mountPath: ${ constants.IMAGE_REPO_PATH }
|
||||||
volumeDevices:
|
volumeDevices:
|
||||||
<#list cr.spec.vm.disks.asList() as disk>
|
<#list spec.vm.disks as disk>
|
||||||
<#if disk.volumeClaimTemplate??>
|
<#if disk.volumeClaimTemplate??>
|
||||||
- name: ${ disk.generatedDiskName.asString }
|
- name: ${ disk.generatedDiskName }
|
||||||
devicePath: /dev/${ disk.generatedDiskName.asString }
|
devicePath: /dev/${ disk.generatedDiskName }
|
||||||
</#if>
|
</#if>
|
||||||
</#list>
|
</#list>
|
||||||
securityContext:
|
securityContext:
|
||||||
privileged: true
|
privileged: true
|
||||||
<#if cr.spec.resources??>
|
<#if spec.resources??>
|
||||||
resources: ${ cr.spec.resources.toString() }
|
resources: ${ toJson(spec.resources) }
|
||||||
<#else>
|
<#else>
|
||||||
<#if cr.spec.vm.currentCpus?? || cr.spec.vm.currentRam?? >
|
<#if spec.vm.currentCpus?? || spec.vm.currentRam?? >
|
||||||
resources:
|
resources:
|
||||||
requests:
|
requests:
|
||||||
<#if cr.spec.vm.currentCpus?? >
|
<#if spec.vm.currentCpus?? >
|
||||||
<#assign factor = 2.0 />
|
<#assign factor = 2.0 />
|
||||||
<#if reconciler.cpuOvercommit??>
|
<#if reconciler.cpuOvercommit??>
|
||||||
<#assign factor = reconciler.cpuOvercommit * 1.0 />
|
<#assign factor = reconciler.cpuOvercommit * 1.0 />
|
||||||
</#if>
|
</#if>
|
||||||
cpu: ${ (parseQuantity(cr.spec.vm.currentCpus.asString) / factor)?c }
|
cpu: ${ (parseQuantity(spec.vm.currentCpus) / factor)?c }
|
||||||
</#if>
|
</#if>
|
||||||
<#if cr.spec.vm.currentRam?? >
|
<#if spec.vm.currentRam?? >
|
||||||
<#assign factor = 1.25 />
|
<#assign factor = 1.25 />
|
||||||
<#if reconciler.ramOvercommit??>
|
<#if reconciler.ramOvercommit??>
|
||||||
<#assign factor = reconciler.ramOvercommit * 1.0 />
|
<#assign factor = reconciler.ramOvercommit * 1.0 />
|
||||||
</#if>
|
</#if>
|
||||||
memory: ${ (parseQuantity(cr.spec.vm.currentRam.asString) / factor)?floor?c }
|
memory: ${ (parseQuantity(spec.vm.currentRam) / factor)?floor?c }
|
||||||
</#if>
|
</#if>
|
||||||
</#if>
|
</#if>
|
||||||
</#if>
|
</#if>
|
||||||
|
|
@ -102,7 +103,7 @@ spec:
|
||||||
projected:
|
projected:
|
||||||
sources:
|
sources:
|
||||||
- configMap:
|
- configMap:
|
||||||
name: ${ cr.metadata.name.asString }
|
name: ${ cr.name() }
|
||||||
<#if displaySecret??>
|
<#if displaySecret??>
|
||||||
- secret:
|
- secret:
|
||||||
name: ${ displaySecret }
|
name: ${ displaySecret }
|
||||||
|
|
@ -113,22 +114,22 @@ spec:
|
||||||
- name: runner-data
|
- name: runner-data
|
||||||
persistentVolumeClaim:
|
persistentVolumeClaim:
|
||||||
claimName: ${ runnerDataPvcName }
|
claimName: ${ runnerDataPvcName }
|
||||||
<#list cr.spec.vm.disks.asList() as disk>
|
<#list spec.vm.disks as disk>
|
||||||
<#if disk.volumeClaimTemplate??>
|
<#if disk.volumeClaimTemplate??>
|
||||||
- name: ${ disk.generatedDiskName.asString }
|
- name: ${ disk.generatedDiskName }
|
||||||
persistentVolumeClaim:
|
persistentVolumeClaim:
|
||||||
claimName: ${ disk.generatedPvcName.asString }
|
claimName: ${ disk.generatedPvcName }
|
||||||
</#if>
|
</#if>
|
||||||
</#list>
|
</#list>
|
||||||
hostNetwork: true
|
hostNetwork: true
|
||||||
terminationGracePeriodSeconds: ${ (cr.spec.vm.powerdownTimeout.asInt + 5)?c }
|
terminationGracePeriodSeconds: ${ (spec.vm.powerdownTimeout + 5)?c }
|
||||||
<#if cr.spec.nodeName??>
|
<#if spec.nodeName??>
|
||||||
nodeName: ${ cr.spec.nodeName.asString }
|
nodeName: ${ spec.nodeName }
|
||||||
</#if>
|
</#if>
|
||||||
<#if cr.spec.nodeSelector??>
|
<#if spec.nodeSelector??>
|
||||||
nodeSelector: ${ cr.spec.nodeSelector.toString() }
|
nodeSelector: ${ toJson(spec.nodeSelector) }
|
||||||
</#if>
|
</#if>
|
||||||
<#if cr.spec.affinity??>
|
<#if spec.affinity??>
|
||||||
affinity: ${ cr.spec.affinity.toString() }
|
affinity: ${ toJson(spec.affinity) }
|
||||||
</#if>
|
</#if>
|
||||||
serviceAccountName: vm-runner
|
serviceAccountName: vm-runner
|
||||||
|
|
|
||||||
|
|
@ -1,194 +0,0 @@
|
||||||
apiVersion: apps/v1
|
|
||||||
kind: StatefulSet
|
|
||||||
metadata:
|
|
||||||
namespace: ${ cr.metadata.namespace.asString }
|
|
||||||
name: ${ cr.metadata.name.asString }
|
|
||||||
labels:
|
|
||||||
app.kubernetes.io/name: ${ constants.APP_NAME }
|
|
||||||
app.kubernetes.io/instance: ${ cr.metadata.name.asString }
|
|
||||||
app.kubernetes.io/managed-by: ${ constants.VM_OP_NAME }
|
|
||||||
annotations:
|
|
||||||
vmoperator.jdrupes.org/version: ${ managerVersion }
|
|
||||||
ownerReferences:
|
|
||||||
- apiVersion: ${ cr.apiVersion.asString }
|
|
||||||
kind: ${ constants.VM_OP_KIND_VM }
|
|
||||||
name: ${ cr.metadata.name.asString }
|
|
||||||
uid: ${ cr.metadata.uid.asString }
|
|
||||||
blockOwnerDeletion: true
|
|
||||||
controller: false
|
|
||||||
|
|
||||||
spec:
|
|
||||||
selector:
|
|
||||||
matchLabels:
|
|
||||||
app.kubernetes.io/name: ${ constants.APP_NAME }
|
|
||||||
app.kubernetes.io/instance: ${ cr.metadata.name.asString }
|
|
||||||
replicas: ${ (cr.spec.vm.state.asString == "Running")?then(1, 0) }
|
|
||||||
updateStrategy:
|
|
||||||
type: OnDelete
|
|
||||||
template:
|
|
||||||
metadata:
|
|
||||||
namespace: ${ cr.metadata.namespace.asString }
|
|
||||||
name: ${ cr.metadata.name.asString }
|
|
||||||
labels:
|
|
||||||
app.kubernetes.io/name: ${ constants.APP_NAME }
|
|
||||||
app.kubernetes.io/instance: ${ cr.metadata.name.asString }
|
|
||||||
app.kubernetes.io/component: ${ constants.APP_NAME }
|
|
||||||
app.kubernetes.io/managed-by: ${ constants.VM_OP_NAME }
|
|
||||||
annotations:
|
|
||||||
# Triggers update of config map mounted in pod
|
|
||||||
# See https://ahmet.im/blog/kubernetes-secret-volumes-delay/
|
|
||||||
vmrunner.jdrupes.org/cmVersion: "${ cm.metadata.resourceVersion.asString }"
|
|
||||||
vmoperator.jdrupes.org/version: ${ managerVersion }
|
|
||||||
spec:
|
|
||||||
containers:
|
|
||||||
- name: ${ cr.metadata.name.asString }
|
|
||||||
<#assign image = cr.spec.image>
|
|
||||||
<#if image.source??>
|
|
||||||
image: ${ image.source.asString }
|
|
||||||
<#else>
|
|
||||||
image: ${ image.repository.asString }/${ image.path.asString }<#if image.version??>:${ image.version.asString }</#if>
|
|
||||||
</#if>
|
|
||||||
<#if image.pullPolicy??>
|
|
||||||
imagePullPolicy: ${ image.pullPolicy.asString }
|
|
||||||
</#if>
|
|
||||||
<#if cr.spec.vm.display.spice??>
|
|
||||||
ports:
|
|
||||||
<#if cr.spec.vm.display.spice??>
|
|
||||||
- name: spice
|
|
||||||
containerPort: ${ cr.spec.vm.display.spice.port.asInt?c }
|
|
||||||
protocol: TCP
|
|
||||||
</#if>
|
|
||||||
</#if>
|
|
||||||
volumeMounts:
|
|
||||||
# Not needed because pod is priviledged:
|
|
||||||
# - mountPath: /dev/kvm
|
|
||||||
# name: dev-kvm
|
|
||||||
# - mountPath: /dev/net/tun
|
|
||||||
# name: dev-tun
|
|
||||||
# - mountPath: /sys/fs/cgroup
|
|
||||||
# name: cgroup
|
|
||||||
- name: config
|
|
||||||
mountPath: /etc/opt/vmrunner
|
|
||||||
- name: runner-data
|
|
||||||
mountPath: /var/local/vm-data
|
|
||||||
- name: vmop-image-repository
|
|
||||||
mountPath: ${ constants.IMAGE_REPO_PATH }
|
|
||||||
volumeDevices:
|
|
||||||
<#assign diskCounter = 0/>
|
|
||||||
<#list cr.spec.vm.disks.asList() as disk>
|
|
||||||
<#if disk.volumeClaimTemplate??>
|
|
||||||
<#if disk.volumeClaimTemplate.metadata??
|
|
||||||
&& disk.volumeClaimTemplate.metadata.name??>
|
|
||||||
<#assign diskName = disk.volumeClaimTemplate.metadata.name.asString + "-disk">
|
|
||||||
<#else>
|
|
||||||
<#assign diskName = "disk-" + diskCounter>
|
|
||||||
</#if>
|
|
||||||
- name: ${ diskName }
|
|
||||||
devicePath: /dev/${ diskName }
|
|
||||||
<#assign diskCounter = diskCounter + 1/>
|
|
||||||
</#if>
|
|
||||||
</#list>
|
|
||||||
securityContext:
|
|
||||||
privileged: true
|
|
||||||
<#if cr.spec.resources??>
|
|
||||||
resources: ${ cr.spec.resources.toString() }
|
|
||||||
<#else>
|
|
||||||
<#if cr.spec.vm.currentCpus?? || cr.spec.vm.currentRam?? >
|
|
||||||
resources:
|
|
||||||
requests:
|
|
||||||
<#if cr.spec.vm.currentCpus?? >
|
|
||||||
<#assign factor = 2.0 />
|
|
||||||
<#if reconciler.cpuOvercommit??>
|
|
||||||
<#assign factor = reconciler.cpuOvercommit * 1.0 />
|
|
||||||
</#if>
|
|
||||||
cpu: ${ (parseQuantity(cr.spec.vm.currentCpus.asString) / factor)?c }
|
|
||||||
</#if>
|
|
||||||
<#if cr.spec.vm.currentRam?? >
|
|
||||||
<#assign factor = 1.25 />
|
|
||||||
<#if reconciler.ramOvercommit??>
|
|
||||||
<#assign factor = reconciler.ramOvercommit * 1.0 />
|
|
||||||
</#if>
|
|
||||||
memory: ${ (parseQuantity(cr.spec.vm.currentRam.asString) / factor)?floor?c }
|
|
||||||
</#if>
|
|
||||||
</#if>
|
|
||||||
</#if>
|
|
||||||
volumes:
|
|
||||||
# Not needed because pod is priviledged:
|
|
||||||
# - name: dev-kvm
|
|
||||||
# hostPath:
|
|
||||||
# path: /dev/kvm
|
|
||||||
# type: CharDevice
|
|
||||||
# - hostPath:
|
|
||||||
# path: /dev/net/tun
|
|
||||||
# type: CharDevice
|
|
||||||
# name: dev-tun
|
|
||||||
# - name: cgroup
|
|
||||||
# hostPath:
|
|
||||||
# path: /sys/fs/cgroup
|
|
||||||
- name: config
|
|
||||||
projected:
|
|
||||||
sources:
|
|
||||||
- configMap:
|
|
||||||
name: ${ cr.metadata.name.asString }
|
|
||||||
<#if displaySecret??>
|
|
||||||
- secret:
|
|
||||||
name: ${ displaySecret }
|
|
||||||
</#if>
|
|
||||||
- name: vmop-image-repository
|
|
||||||
persistentVolumeClaim:
|
|
||||||
claimName: vmop-image-repository
|
|
||||||
hostNetwork: true
|
|
||||||
terminationGracePeriodSeconds: ${ (cr.spec.vm.powerdownTimeout.asInt + 5)?c }
|
|
||||||
<#if cr.spec.nodeName??>
|
|
||||||
nodeName: ${ cr.spec.nodeName.asString }
|
|
||||||
</#if>
|
|
||||||
<#if cr.spec.nodeSelector??>
|
|
||||||
nodeSelector: ${ cr.spec.nodeSelector.toString() }
|
|
||||||
</#if>
|
|
||||||
<#if cr.spec.affinity??>
|
|
||||||
affinity: ${ cr.spec.affinity.toString() }
|
|
||||||
</#if>
|
|
||||||
serviceAccountName: vm-runner
|
|
||||||
volumeClaimTemplates:
|
|
||||||
- metadata:
|
|
||||||
namespace: ${ cr.metadata.namespace.asString }
|
|
||||||
name: runner-data
|
|
||||||
labels:
|
|
||||||
app.kubernetes.io/name: ${ constants.APP_NAME }
|
|
||||||
app.kubernetes.io/instance: ${ cr.metadata.name.asString }
|
|
||||||
app.kubernetes.io/managed-by: ${ constants.VM_OP_NAME }
|
|
||||||
spec:
|
|
||||||
accessModes:
|
|
||||||
- ReadWriteOnce
|
|
||||||
<#if reconciler.runnerDataPvc?? && reconciler.runnerDataPvc.storageClassName??>
|
|
||||||
storageClassName: ${ reconciler.runnerDataPvc.storageClassName }
|
|
||||||
</#if>
|
|
||||||
resources:
|
|
||||||
requests:
|
|
||||||
storage: 1Mi
|
|
||||||
<#assign diskCounter = 0/>
|
|
||||||
<#list cr.spec.vm.disks.asList() as disk>
|
|
||||||
<#if disk.volumeClaimTemplate??>
|
|
||||||
<#if disk.volumeClaimTemplate.metadata??
|
|
||||||
&& disk.volumeClaimTemplate.metadata.name??>
|
|
||||||
<#assign diskName = disk.volumeClaimTemplate.metadata.name.asString + "-disk">
|
|
||||||
<#else>
|
|
||||||
<#assign diskName = "disk-" + diskCounter>
|
|
||||||
</#if>
|
|
||||||
- metadata:
|
|
||||||
namespace: ${ cr.metadata.namespace.asString }
|
|
||||||
name: ${ diskName }
|
|
||||||
labels:
|
|
||||||
app.kubernetes.io/name: ${ constants.APP_NAME }
|
|
||||||
app.kubernetes.io/instance: ${ cr.metadata.name.asString }
|
|
||||||
app.kubernetes.io/managed-by: ${ constants.VM_OP_NAME }
|
|
||||||
<#if disk.volumeClaimTemplate.metadata??
|
|
||||||
&& disk.volumeClaimTemplate.metadata.annotations??>
|
|
||||||
annotations:
|
|
||||||
${ disk.volumeClaimTemplate.metadata.annotations.toString() }
|
|
||||||
</#if>
|
|
||||||
spec:
|
|
||||||
${ disk.volumeClaimTemplate.spec.toString() }
|
|
||||||
<#assign diskCounter = diskCounter + 1/>
|
|
||||||
</#if>
|
|
||||||
</#list>
|
|
||||||
|
|
@ -68,7 +68,7 @@ import org.yaml.snakeyaml.constructor.SafeConstructor;
|
||||||
* @throws TemplateException the template exception
|
* @throws TemplateException the template exception
|
||||||
* @throws ApiException the api exception
|
* @throws ApiException the api exception
|
||||||
*/
|
*/
|
||||||
public DynamicKubernetesObject reconcile(Map<String, Object> model,
|
public Map<String, Object> reconcile(Map<String, Object> model,
|
||||||
VmChannel channel)
|
VmChannel channel)
|
||||||
throws IOException, TemplateException, ApiException {
|
throws IOException, TemplateException, ApiException {
|
||||||
// Get API
|
// Get API
|
||||||
|
|
@ -87,7 +87,10 @@ import org.yaml.snakeyaml.constructor.SafeConstructor;
|
||||||
// Apply and maybe force pod update
|
// Apply and maybe force pod update
|
||||||
var newState = K8s.apply(cmApi, mapDef, out.toString());
|
var newState = K8s.apply(cmApi, mapDef, out.toString());
|
||||||
maybeForceUpdate(channel.client(), newState);
|
maybeForceUpdate(channel.client(), newState);
|
||||||
return newState;
|
@SuppressWarnings("unchecked")
|
||||||
|
var res = (Map<String, Object>) channel.client().getJSON().getGson()
|
||||||
|
.fromJson(newState.getRaw(), Map.class);
|
||||||
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -256,10 +256,9 @@ public class DisplaySecretMonitor
|
||||||
@SuppressWarnings("PMD.AvoidSynchronizedStatement")
|
@SuppressWarnings("PMD.AvoidSynchronizedStatement")
|
||||||
public void onVmDefChanged(VmDefChanged event, Channel channel) {
|
public void onVmDefChanged(VmDefChanged event, Channel channel) {
|
||||||
synchronized (pendingGets) {
|
synchronized (pendingGets) {
|
||||||
String vmName = event.vmDefinition().metadata().getName();
|
String vmName = event.vmDefinition().name();
|
||||||
for (var pending : pendingGets) {
|
for (var pending : pendingGets) {
|
||||||
if (pending.event.vmDefinition().metadata().getName()
|
if (pending.event.vmDefinition().name().equals(vmName)
|
||||||
.equals(vmName)
|
|
||||||
&& event.vmDefinition().displayPasswordSerial()
|
&& event.vmDefinition().displayPasswordSerial()
|
||||||
.map(s -> s >= pending.expectedSerial).orElse(false)) {
|
.map(s -> s >= pending.expectedSerial).orElse(false)) {
|
||||||
pending.lock.remove();
|
pending.lock.remove();
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,6 @@
|
||||||
|
|
||||||
package org.jdrupes.vmoperator.manager;
|
package org.jdrupes.vmoperator.manager;
|
||||||
|
|
||||||
import com.google.gson.JsonPrimitive;
|
|
||||||
import freemarker.template.TemplateException;
|
import freemarker.template.TemplateException;
|
||||||
import io.kubernetes.client.openapi.ApiException;
|
import io.kubernetes.client.openapi.ApiException;
|
||||||
import io.kubernetes.client.openapi.models.V1ObjectMeta;
|
import io.kubernetes.client.openapi.models.V1ObjectMeta;
|
||||||
|
|
@ -36,7 +35,7 @@ import static org.jdrupes.vmoperator.manager.Constants.DATA_DISPLAY_PASSWORD;
|
||||||
import static org.jdrupes.vmoperator.manager.Constants.DATA_PASSWORD_EXPIRY;
|
import static org.jdrupes.vmoperator.manager.Constants.DATA_PASSWORD_EXPIRY;
|
||||||
import org.jdrupes.vmoperator.manager.events.VmChannel;
|
import org.jdrupes.vmoperator.manager.events.VmChannel;
|
||||||
import org.jdrupes.vmoperator.manager.events.VmDefChanged;
|
import org.jdrupes.vmoperator.manager.events.VmDefChanged;
|
||||||
import org.jdrupes.vmoperator.util.GsonPtr;
|
import org.jdrupes.vmoperator.util.DataPath;
|
||||||
import org.jose4j.base64url.Base64;
|
import org.jose4j.base64url.Base64;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -61,32 +60,31 @@ import org.jose4j.base64url.Base64;
|
||||||
Map<String, Object> model, VmChannel channel)
|
Map<String, Object> model, VmChannel channel)
|
||||||
throws IOException, TemplateException, ApiException {
|
throws IOException, TemplateException, ApiException {
|
||||||
// Secret needed at all?
|
// Secret needed at all?
|
||||||
var display = GsonPtr.to(event.vmDefinition().data()).to("spec", "vm",
|
var display = event.vmDefinition().fromVm("display").get();
|
||||||
"display");
|
if (!DataPath.<Boolean> get(display, "spice", "generateSecret")
|
||||||
if (!display.get(JsonPrimitive.class, "spice", "generateSecret")
|
.orElse(true)) {
|
||||||
.map(JsonPrimitive::getAsBoolean).orElse(true)) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if exists
|
// Check if exists
|
||||||
var metadata = event.vmDefinition().getMetadata();
|
var vmDef = event.vmDefinition();
|
||||||
ListOptions options = new ListOptions();
|
ListOptions options = new ListOptions();
|
||||||
options.setLabelSelector("app.kubernetes.io/name=" + APP_NAME + ","
|
options.setLabelSelector("app.kubernetes.io/name=" + APP_NAME + ","
|
||||||
+ "app.kubernetes.io/component=" + COMP_DISPLAY_SECRET + ","
|
+ "app.kubernetes.io/component=" + COMP_DISPLAY_SECRET + ","
|
||||||
+ "app.kubernetes.io/instance=" + metadata.getName());
|
+ "app.kubernetes.io/instance=" + vmDef.name());
|
||||||
var stubs = K8sV1SecretStub.list(channel.client(),
|
var stubs = K8sV1SecretStub.list(channel.client(), vmDef.namespace(),
|
||||||
metadata.getNamespace(), options);
|
options);
|
||||||
if (!stubs.isEmpty()) {
|
if (!stubs.isEmpty()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create secret
|
// Create secret
|
||||||
var secret = new V1Secret();
|
var secret = new V1Secret();
|
||||||
secret.setMetadata(new V1ObjectMeta().namespace(metadata.getNamespace())
|
secret.setMetadata(new V1ObjectMeta().namespace(vmDef.namespace())
|
||||||
.name(metadata.getName() + "-" + COMP_DISPLAY_SECRET)
|
.name(vmDef.name() + "-" + COMP_DISPLAY_SECRET)
|
||||||
.putLabelsItem("app.kubernetes.io/name", APP_NAME)
|
.putLabelsItem("app.kubernetes.io/name", APP_NAME)
|
||||||
.putLabelsItem("app.kubernetes.io/component", COMP_DISPLAY_SECRET)
|
.putLabelsItem("app.kubernetes.io/component", COMP_DISPLAY_SECRET)
|
||||||
.putLabelsItem("app.kubernetes.io/instance", metadata.getName()));
|
.putLabelsItem("app.kubernetes.io/instance", vmDef.name()));
|
||||||
secret.setType("Opaque");
|
secret.setType("Opaque");
|
||||||
SecureRandom random = null;
|
SecureRandom random = null;
|
||||||
try {
|
try {
|
||||||
|
|
|
||||||
|
|
@ -18,22 +18,23 @@
|
||||||
|
|
||||||
package org.jdrupes.vmoperator.manager;
|
package org.jdrupes.vmoperator.manager;
|
||||||
|
|
||||||
import com.google.gson.JsonObject;
|
import com.google.gson.Gson;
|
||||||
import freemarker.template.Configuration;
|
import freemarker.template.Configuration;
|
||||||
import freemarker.template.TemplateException;
|
import freemarker.template.TemplateException;
|
||||||
import io.kubernetes.client.openapi.ApiException;
|
import io.kubernetes.client.openapi.ApiException;
|
||||||
import io.kubernetes.client.openapi.models.V1APIService;
|
import io.kubernetes.client.openapi.models.V1APIService;
|
||||||
import io.kubernetes.client.openapi.models.V1ObjectMeta;
|
import io.kubernetes.client.openapi.models.V1ObjectMeta;
|
||||||
import io.kubernetes.client.util.generic.dynamic.DynamicKubernetesApi;
|
|
||||||
import io.kubernetes.client.util.generic.dynamic.DynamicKubernetesObject;
|
import io.kubernetes.client.util.generic.dynamic.DynamicKubernetesObject;
|
||||||
import io.kubernetes.client.util.generic.dynamic.Dynamics;
|
import io.kubernetes.client.util.generic.dynamic.Dynamics;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.StringWriter;
|
import java.io.StringWriter;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.logging.Logger;
|
import java.util.logging.Logger;
|
||||||
import org.jdrupes.vmoperator.common.K8s;
|
import org.jdrupes.vmoperator.common.K8sV1ServiceStub;
|
||||||
import org.jdrupes.vmoperator.common.K8sDynamicModel;
|
import org.jdrupes.vmoperator.common.VmDefinition;
|
||||||
import org.jdrupes.vmoperator.manager.events.VmChannel;
|
import org.jdrupes.vmoperator.manager.events.VmChannel;
|
||||||
import org.jdrupes.vmoperator.manager.events.VmDefChanged;
|
import org.jdrupes.vmoperator.manager.events.VmDefChanged;
|
||||||
import org.jdrupes.vmoperator.util.GsonPtr;
|
import org.jdrupes.vmoperator.util.GsonPtr;
|
||||||
|
|
@ -92,11 +93,13 @@ import org.yaml.snakeyaml.constructor.SafeConstructor;
|
||||||
if (lbsDef instanceof Boolean isOn && !isOn) {
|
if (lbsDef instanceof Boolean isOn && !isOn) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
JsonObject cfgMeta = new JsonObject();
|
|
||||||
if (lbsDef instanceof Map) {
|
// Load balancer can also be turned off for VM
|
||||||
var json = channel.client().getJSON();
|
var vmDef = event.vmDefinition();
|
||||||
cfgMeta
|
if (vmDef
|
||||||
= json.deserialize(json.serialize(lbsDef), JsonObject.class);
|
.<Map<String, Map<String, String>>> fromSpec(LOAD_BALANCER_SERVICE)
|
||||||
|
.map(m -> m.isEmpty()).orElse(false)) {
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Combine template and data and parse result
|
// Combine template and data and parse result
|
||||||
|
|
@ -107,53 +110,78 @@ import org.yaml.snakeyaml.constructor.SafeConstructor;
|
||||||
// https://github.com/kubernetes-client/java/issues/2741
|
// https://github.com/kubernetes-client/java/issues/2741
|
||||||
var svcDef = Dynamics.newFromYaml(
|
var svcDef = Dynamics.newFromYaml(
|
||||||
new Yaml(new SafeConstructor(new LoaderOptions())), out.toString());
|
new Yaml(new SafeConstructor(new LoaderOptions())), out.toString());
|
||||||
mergeMetadata(svcDef, cfgMeta, event.vmDefinition());
|
@SuppressWarnings("unchecked")
|
||||||
|
var defaults = lbsDef instanceof Map
|
||||||
|
? (Map<String, Map<String, String>>) lbsDef
|
||||||
|
: null;
|
||||||
|
var client = channel.client();
|
||||||
|
mergeMetadata(client.getJSON().getGson(), svcDef, defaults, vmDef);
|
||||||
|
|
||||||
// Apply
|
// Apply
|
||||||
DynamicKubernetesApi svcApi = new DynamicKubernetesApi("", "v1",
|
var svcStub = K8sV1ServiceStub
|
||||||
"services", channel.client());
|
.get(client, vmDef.namespace(), vmDef.name());
|
||||||
K8s.apply(svcApi, svcDef, svcDef.getRaw().toString());
|
if (svcStub.apply(svcDef).isEmpty()) {
|
||||||
|
logger.warning(
|
||||||
|
() -> "Could not patch service for " + svcStub.name());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void mergeMetadata(DynamicKubernetesObject svcDef,
|
private void mergeMetadata(Gson gson, DynamicKubernetesObject svcDef,
|
||||||
JsonObject cfgMeta, K8sDynamicModel vmDefinition) {
|
Map<String, Map<String, String>> defaults,
|
||||||
// Get metadata from VM definition
|
VmDefinition vmDefinition) {
|
||||||
var vmMeta = GsonPtr.to(vmDefinition.data()).to("spec")
|
// Get specific load balancer metadata from VM definition
|
||||||
.get(JsonObject.class, LOAD_BALANCER_SERVICE)
|
var vmLbMeta = vmDefinition
|
||||||
.map(JsonObject::deepCopy).orElseGet(() -> new JsonObject());
|
.<Map<String, Map<String, String>>> fromSpec(LOAD_BALANCER_SERVICE)
|
||||||
|
.orElse(Collections.emptyMap());
|
||||||
|
|
||||||
// Merge Data from VM definition into config data
|
// Merge
|
||||||
mergeReplace(GsonPtr.to(cfgMeta).to(LABELS).get(JsonObject.class),
|
var svcMeta = svcDef.getMetadata();
|
||||||
GsonPtr.to(vmMeta).to(LABELS).get(JsonObject.class));
|
var svcJsonMeta = GsonPtr.to(svcDef.getRaw()).to(METADATA);
|
||||||
mergeReplace(
|
Optional.ofNullable(mergeIfAbsent(svcMeta.getLabels(),
|
||||||
GsonPtr.to(cfgMeta).to(ANNOTATIONS).get(JsonObject.class),
|
mergeReplace(defaults.get(LABELS), vmLbMeta.get(LABELS))))
|
||||||
GsonPtr.to(vmMeta).to(ANNOTATIONS).get(JsonObject.class));
|
.ifPresent(lbls -> svcJsonMeta.set(LABELS, gson.toJsonTree(lbls)));
|
||||||
|
Optional.ofNullable(mergeIfAbsent(svcMeta.getAnnotations(),
|
||||||
// Merge additional data into service definition
|
mergeReplace(defaults.get(ANNOTATIONS), vmLbMeta.get(ANNOTATIONS))))
|
||||||
var svcMeta = GsonPtr.to(svcDef.getRaw()).to(METADATA);
|
.ifPresent(as -> svcJsonMeta.set(ANNOTATIONS, gson.toJsonTree(as)));
|
||||||
mergeIfAbsent(svcMeta.to(LABELS).get(JsonObject.class),
|
|
||||||
GsonPtr.to(cfgMeta).to(LABELS).get(JsonObject.class));
|
|
||||||
mergeIfAbsent(svcMeta.to(ANNOTATIONS).get(JsonObject.class),
|
|
||||||
GsonPtr.to(cfgMeta).to(ANNOTATIONS).get(JsonObject.class));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void mergeReplace(JsonObject dest, JsonObject src) {
|
private Map<String, String> mergeReplace(Map<String, String> dest,
|
||||||
|
Map<String, String> src) {
|
||||||
|
if (src == null) {
|
||||||
|
return dest;
|
||||||
|
}
|
||||||
|
if (dest == null) {
|
||||||
|
dest = new LinkedHashMap<>();
|
||||||
|
} else {
|
||||||
|
dest = new LinkedHashMap<>(dest);
|
||||||
|
}
|
||||||
for (var e : src.entrySet()) {
|
for (var e : src.entrySet()) {
|
||||||
if (e.getValue().isJsonNull()) {
|
if (e.getValue() == null) {
|
||||||
dest.remove(e.getKey());
|
dest.remove(e.getKey());
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
dest.add(e.getKey(), e.getValue());
|
dest.put(e.getKey(), e.getValue());
|
||||||
}
|
}
|
||||||
|
return dest;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void mergeIfAbsent(JsonObject dest, JsonObject src) {
|
private Map<String, String> mergeIfAbsent(Map<String, String> dest,
|
||||||
|
Map<String, String> src) {
|
||||||
|
if (src == null) {
|
||||||
|
return dest;
|
||||||
|
}
|
||||||
|
if (dest == null) {
|
||||||
|
dest = new LinkedHashMap<>();
|
||||||
|
} else {
|
||||||
|
dest = new LinkedHashMap<>(dest);
|
||||||
|
}
|
||||||
for (var e : src.entrySet()) {
|
for (var e : src.entrySet()) {
|
||||||
if (dest.has(e.getKey())) {
|
if (dest.containsKey(e.getKey())) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
dest.add(e.getKey(), e.getValue());
|
dest.put(e.getKey(), e.getValue());
|
||||||
}
|
}
|
||||||
|
return dest;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,6 @@ package org.jdrupes.vmoperator.manager;
|
||||||
|
|
||||||
import freemarker.template.Configuration;
|
import freemarker.template.Configuration;
|
||||||
import freemarker.template.TemplateException;
|
import freemarker.template.TemplateException;
|
||||||
import io.kubernetes.client.custom.V1Patch;
|
|
||||||
import io.kubernetes.client.openapi.ApiException;
|
import io.kubernetes.client.openapi.ApiException;
|
||||||
import io.kubernetes.client.util.generic.dynamic.Dynamics;
|
import io.kubernetes.client.util.generic.dynamic.Dynamics;
|
||||||
import io.kubernetes.client.util.generic.options.PatchOptions;
|
import io.kubernetes.client.util.generic.options.PatchOptions;
|
||||||
|
|
@ -29,7 +28,7 @@ import java.io.StringWriter;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.logging.Logger;
|
import java.util.logging.Logger;
|
||||||
import org.jdrupes.vmoperator.common.K8sV1PodStub;
|
import org.jdrupes.vmoperator.common.K8sV1PodStub;
|
||||||
import org.jdrupes.vmoperator.common.VmDefinitionModel.RequestedVmState;
|
import org.jdrupes.vmoperator.common.VmDefinition.RequestedVmState;
|
||||||
import org.jdrupes.vmoperator.manager.events.VmChannel;
|
import org.jdrupes.vmoperator.manager.events.VmChannel;
|
||||||
import org.jdrupes.vmoperator.manager.events.VmDefChanged;
|
import org.jdrupes.vmoperator.manager.events.VmDefChanged;
|
||||||
import org.yaml.snakeyaml.LoaderOptions;
|
import org.yaml.snakeyaml.LoaderOptions;
|
||||||
|
|
@ -73,18 +72,18 @@ import org.yaml.snakeyaml.constructor.SafeConstructor;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get pod stub.
|
// Get pod stub.
|
||||||
var metadata = event.vmDefinition().getMetadata();
|
var vmDef = event.vmDefinition();
|
||||||
var podStub = K8sV1PodStub.get(channel.client(),
|
var podStub = K8sV1PodStub.get(channel.client(), vmDef.namespace(),
|
||||||
metadata.getNamespace(), metadata.getName());
|
vmDef.name());
|
||||||
|
|
||||||
// Nothing to do if exists and should be running
|
// Nothing to do if exists and should be running
|
||||||
if (event.vmDefinition().vmState() == RequestedVmState.RUNNING
|
if (vmDef.vmState() == RequestedVmState.RUNNING
|
||||||
&& podStub.model().isPresent()) {
|
&& podStub.model().isPresent()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete if running but should be stopped
|
// Delete if running but should be stopped
|
||||||
if (event.vmDefinition().vmState() == RequestedVmState.STOPPED) {
|
if (vmDef.vmState() == RequestedVmState.STOPPED) {
|
||||||
if (podStub.model().isPresent()) {
|
if (podStub.model().isPresent()) {
|
||||||
podStub.delete();
|
podStub.delete();
|
||||||
}
|
}
|
||||||
|
|
@ -104,9 +103,7 @@ import org.yaml.snakeyaml.constructor.SafeConstructor;
|
||||||
PatchOptions opts = new PatchOptions();
|
PatchOptions opts = new PatchOptions();
|
||||||
opts.setForce(true);
|
opts.setForce(true);
|
||||||
opts.setFieldManager("kubernetes-java-kubectl-apply");
|
opts.setFieldManager("kubernetes-java-kubectl-apply");
|
||||||
if (podStub.patch(V1Patch.PATCH_FORMAT_APPLY_YAML,
|
if (podStub.apply(podDef).isEmpty()) {
|
||||||
new V1Patch(channel.client().getJSON().serialize(podDef)), opts)
|
|
||||||
.isEmpty()) {
|
|
||||||
logger.warning(
|
logger.warning(
|
||||||
() -> "Could not patch pod for " + podStub.name());
|
() -> "Could not patch pod for " + podStub.name());
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,8 +18,6 @@
|
||||||
|
|
||||||
package org.jdrupes.vmoperator.manager;
|
package org.jdrupes.vmoperator.manager;
|
||||||
|
|
||||||
import com.google.gson.JsonElement;
|
|
||||||
import com.google.gson.JsonObject;
|
|
||||||
import freemarker.core.ParseException;
|
import freemarker.core.ParseException;
|
||||||
import freemarker.template.Configuration;
|
import freemarker.template.Configuration;
|
||||||
import freemarker.template.MalformedTemplateNameException;
|
import freemarker.template.MalformedTemplateNameException;
|
||||||
|
|
@ -32,6 +30,7 @@ import io.kubernetes.client.util.generic.options.ListOptions;
|
||||||
import io.kubernetes.client.util.generic.options.PatchOptions;
|
import io.kubernetes.client.util.generic.options.PatchOptions;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.StringWriter;
|
import java.io.StringWriter;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.logging.Logger;
|
import java.util.logging.Logger;
|
||||||
|
|
@ -41,7 +40,7 @@ import static org.jdrupes.vmoperator.common.Constants.VM_OP_NAME;
|
||||||
import org.jdrupes.vmoperator.common.K8sV1PvcStub;
|
import org.jdrupes.vmoperator.common.K8sV1PvcStub;
|
||||||
import org.jdrupes.vmoperator.manager.events.VmChannel;
|
import org.jdrupes.vmoperator.manager.events.VmChannel;
|
||||||
import org.jdrupes.vmoperator.manager.events.VmDefChanged;
|
import org.jdrupes.vmoperator.manager.events.VmDefChanged;
|
||||||
import org.jdrupes.vmoperator.util.GsonPtr;
|
import org.jdrupes.vmoperator.util.DataPath;
|
||||||
import org.yaml.snakeyaml.LoaderOptions;
|
import org.yaml.snakeyaml.LoaderOptions;
|
||||||
import org.yaml.snakeyaml.Yaml;
|
import org.yaml.snakeyaml.Yaml;
|
||||||
import org.yaml.snakeyaml.constructor.SafeConstructor;
|
import org.yaml.snakeyaml.constructor.SafeConstructor;
|
||||||
|
|
@ -78,16 +77,16 @@ import org.yaml.snakeyaml.constructor.SafeConstructor;
|
||||||
public void reconcile(VmDefChanged event, Map<String, Object> model,
|
public void reconcile(VmDefChanged event, Map<String, Object> model,
|
||||||
VmChannel channel)
|
VmChannel channel)
|
||||||
throws IOException, TemplateException, ApiException {
|
throws IOException, TemplateException, ApiException {
|
||||||
var metadata = event.vmDefinition().getMetadata();
|
var vmDef = event.vmDefinition();
|
||||||
|
|
||||||
// Existing disks
|
// Existing disks
|
||||||
ListOptions listOpts = new ListOptions();
|
ListOptions listOpts = new ListOptions();
|
||||||
listOpts.setLabelSelector(
|
listOpts.setLabelSelector(
|
||||||
"app.kubernetes.io/managed-by=" + VM_OP_NAME + ","
|
"app.kubernetes.io/managed-by=" + VM_OP_NAME + ","
|
||||||
+ "app.kubernetes.io/name=" + APP_NAME + ","
|
+ "app.kubernetes.io/name=" + APP_NAME + ","
|
||||||
+ "app.kubernetes.io/instance=" + metadata.getName());
|
+ "app.kubernetes.io/instance=" + vmDef.name());
|
||||||
var knownDisks = K8sV1PvcStub.list(channel.client(),
|
var knownDisks = K8sV1PvcStub.list(channel.client(),
|
||||||
metadata.getNamespace(), listOpts);
|
vmDef.namespace(), listOpts);
|
||||||
var knownPvcs = knownDisks.stream().map(K8sV1PvcStub::name)
|
var knownPvcs = knownDisks.stream().map(K8sV1PvcStub::name)
|
||||||
.collect(Collectors.toSet());
|
.collect(Collectors.toSet());
|
||||||
|
|
||||||
|
|
@ -95,23 +94,23 @@ import org.yaml.snakeyaml.constructor.SafeConstructor;
|
||||||
reconcileRunnerDataPvc(event, model, channel, knownPvcs);
|
reconcileRunnerDataPvc(event, model, channel, knownPvcs);
|
||||||
|
|
||||||
// Reconcile pvcs for defined disks
|
// Reconcile pvcs for defined disks
|
||||||
var diskDefs = GsonPtr.to((JsonObject) model.get("cr"))
|
var diskDefs = vmDef.<List<Map<String, Object>>> fromVm("disks")
|
||||||
.getAsListOf(JsonObject.class, "spec", "vm", "disks");
|
.orElse(List.of());
|
||||||
var diskCounter = 0;
|
var diskCounter = 0;
|
||||||
for (var diskDef : diskDefs) {
|
for (var diskDef : diskDefs) {
|
||||||
if (!diskDef.has("volumeClaimTemplate")) {
|
if (!diskDef.containsKey("volumeClaimTemplate")) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
var diskName = GsonPtr.to(diskDef)
|
var diskName = DataPath.get(diskDef, "volumeClaimTemplate",
|
||||||
.getAsString("volumeClaimTemplate", "metadata", "name")
|
"metadata", "name").map(name -> name + "-disk")
|
||||||
.map(name -> name + "-disk").orElse("disk-" + diskCounter);
|
.orElse("disk-" + diskCounter);
|
||||||
diskCounter += 1;
|
diskCounter += 1;
|
||||||
diskDef.addProperty("generatedDiskName", diskName);
|
diskDef.put("generatedDiskName", diskName);
|
||||||
|
|
||||||
// Don't do anything if pvc with old (sts generated) name exists.
|
// Don't do anything if pvc with old (sts generated) name exists.
|
||||||
var stsDiskPvcName = diskName + "-" + metadata.getName() + "-0";
|
var stsDiskPvcName = diskName + "-" + vmDef.name() + "-0";
|
||||||
if (knownPvcs.contains(stsDiskPvcName)) {
|
if (knownPvcs.contains(stsDiskPvcName)) {
|
||||||
diskDef.addProperty("generatedPvcName", stsDiskPvcName);
|
diskDef.put("generatedPvcName", stsDiskPvcName);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -127,18 +126,18 @@ import org.yaml.snakeyaml.constructor.SafeConstructor;
|
||||||
Set<String> knownPvcs)
|
Set<String> knownPvcs)
|
||||||
throws TemplateNotFoundException, MalformedTemplateNameException,
|
throws TemplateNotFoundException, MalformedTemplateNameException,
|
||||||
ParseException, IOException, TemplateException, ApiException {
|
ParseException, IOException, TemplateException, ApiException {
|
||||||
var metadata = event.vmDefinition().getMetadata();
|
var vmDef = event.vmDefinition();
|
||||||
|
|
||||||
// Look for old (sts generated) name.
|
// Look for old (sts generated) name.
|
||||||
var stsRunnerDataPvcName
|
var stsRunnerDataPvcName
|
||||||
= "runner-data" + "-" + metadata.getName() + "-0";
|
= "runner-data" + "-" + vmDef.name() + "-0";
|
||||||
if (knownPvcs.contains(stsRunnerDataPvcName)) {
|
if (knownPvcs.contains(stsRunnerDataPvcName)) {
|
||||||
model.put("runnerDataPvcName", stsRunnerDataPvcName);
|
model.put("runnerDataPvcName", stsRunnerDataPvcName);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate PVC
|
// Generate PVC
|
||||||
model.put("runnerDataPvcName", metadata.getName() + "-runner-data");
|
model.put("runnerDataPvcName", vmDef.name() + "-runner-data");
|
||||||
var fmTemplate = fmConfig.getTemplate("runnerDataPvc.ftl.yaml");
|
var fmTemplate = fmConfig.getTemplate("runnerDataPvc.ftl.yaml");
|
||||||
StringWriter out = new StringWriter();
|
StringWriter out = new StringWriter();
|
||||||
fmTemplate.process(model, out);
|
fmTemplate.process(model, out);
|
||||||
|
|
@ -149,7 +148,7 @@ import org.yaml.snakeyaml.constructor.SafeConstructor;
|
||||||
|
|
||||||
// Do apply changes
|
// Do apply changes
|
||||||
var pvcStub = K8sV1PvcStub.get(channel.client(),
|
var pvcStub = K8sV1PvcStub.get(channel.client(),
|
||||||
metadata.getNamespace(), (String) model.get("runnerDataPvcName"));
|
vmDef.namespace(), (String) model.get("runnerDataPvcName"));
|
||||||
PatchOptions opts = new PatchOptions();
|
PatchOptions opts = new PatchOptions();
|
||||||
opts.setForce(true);
|
opts.setForce(true);
|
||||||
opts.setFieldManager("kubernetes-java-kubectl-apply");
|
opts.setFieldManager("kubernetes-java-kubectl-apply");
|
||||||
|
|
@ -165,13 +164,13 @@ import org.yaml.snakeyaml.constructor.SafeConstructor;
|
||||||
Map<String, Object> model, VmChannel channel)
|
Map<String, Object> model, VmChannel channel)
|
||||||
throws TemplateNotFoundException, MalformedTemplateNameException,
|
throws TemplateNotFoundException, MalformedTemplateNameException,
|
||||||
ParseException, IOException, TemplateException, ApiException {
|
ParseException, IOException, TemplateException, ApiException {
|
||||||
var metadata = event.vmDefinition().getMetadata();
|
var vmDef = event.vmDefinition();
|
||||||
|
|
||||||
// Generate PVC
|
// Generate PVC
|
||||||
var diskDef = GsonPtr.to((JsonElement) model.get("disk"));
|
@SuppressWarnings("unchecked")
|
||||||
var pvcName = metadata.getName() + "-"
|
var diskDef = (Map<String, Object>) model.get("disk");
|
||||||
+ diskDef.getAsString("generatedDiskName").get();
|
var pvcName = vmDef.name() + "-" + diskDef.get("generatedDiskName");
|
||||||
diskDef.set("generatedPvcName", pvcName);
|
diskDef.put("generatedPvcName", pvcName);
|
||||||
var fmTemplate = fmConfig.getTemplate("runnerDiskPvc.ftl.yaml");
|
var fmTemplate = fmConfig.getTemplate("runnerDiskPvc.ftl.yaml");
|
||||||
StringWriter out = new StringWriter();
|
StringWriter out = new StringWriter();
|
||||||
fmTemplate.process(model, out);
|
fmTemplate.process(model, out);
|
||||||
|
|
@ -181,9 +180,8 @@ import org.yaml.snakeyaml.constructor.SafeConstructor;
|
||||||
new Yaml(new SafeConstructor(new LoaderOptions())), out.toString());
|
new Yaml(new SafeConstructor(new LoaderOptions())), out.toString());
|
||||||
|
|
||||||
// Do apply changes
|
// Do apply changes
|
||||||
var pvcStub = K8sV1PvcStub.get(channel.client(),
|
var pvcStub
|
||||||
metadata.getNamespace(), GsonPtr.to((JsonElement) model.get("disk"))
|
= K8sV1PvcStub.get(channel.client(), vmDef.namespace(), pvcName);
|
||||||
.getAsString("generatedPvcName").get());
|
|
||||||
PatchOptions opts = new PatchOptions();
|
PatchOptions opts = new PatchOptions();
|
||||||
opts.setForce(true);
|
opts.setForce(true);
|
||||||
opts.setFieldManager("kubernetes-java-kubectl-apply");
|
opts.setFieldManager("kubernetes-java-kubectl-apply");
|
||||||
|
|
|
||||||
|
|
@ -18,11 +18,13 @@
|
||||||
|
|
||||||
package org.jdrupes.vmoperator.manager;
|
package org.jdrupes.vmoperator.manager;
|
||||||
|
|
||||||
import com.google.gson.JsonArray;
|
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||||
import com.google.gson.JsonObject;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import freemarker.template.AdapterTemplateModel;
|
||||||
import freemarker.template.Configuration;
|
import freemarker.template.Configuration;
|
||||||
import freemarker.template.DefaultObjectWrapperBuilder;
|
import freemarker.template.DefaultObjectWrapperBuilder;
|
||||||
import freemarker.template.SimpleNumber;
|
import freemarker.template.SimpleNumber;
|
||||||
|
import freemarker.template.SimpleScalar;
|
||||||
import freemarker.template.TemplateException;
|
import freemarker.template.TemplateException;
|
||||||
import freemarker.template.TemplateExceptionHandler;
|
import freemarker.template.TemplateExceptionHandler;
|
||||||
import freemarker.template.TemplateHashModel;
|
import freemarker.template.TemplateHashModel;
|
||||||
|
|
@ -30,7 +32,7 @@ import freemarker.template.TemplateMethodModelEx;
|
||||||
import freemarker.template.TemplateModelException;
|
import freemarker.template.TemplateModelException;
|
||||||
import io.kubernetes.client.custom.Quantity;
|
import io.kubernetes.client.custom.Quantity;
|
||||||
import io.kubernetes.client.openapi.ApiException;
|
import io.kubernetes.client.openapi.ApiException;
|
||||||
import io.kubernetes.client.util.generic.dynamic.DynamicKubernetesObject;
|
import io.kubernetes.client.openapi.models.V1ObjectMeta;
|
||||||
import io.kubernetes.client.util.generic.options.ListOptions;
|
import io.kubernetes.client.util.generic.options.ListOptions;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
|
|
@ -44,15 +46,15 @@ import java.util.Optional;
|
||||||
import static org.jdrupes.vmoperator.common.Constants.APP_NAME;
|
import static org.jdrupes.vmoperator.common.Constants.APP_NAME;
|
||||||
import org.jdrupes.vmoperator.common.Convertions;
|
import org.jdrupes.vmoperator.common.Convertions;
|
||||||
import org.jdrupes.vmoperator.common.K8sClient;
|
import org.jdrupes.vmoperator.common.K8sClient;
|
||||||
import org.jdrupes.vmoperator.common.K8sDynamicModel;
|
|
||||||
import org.jdrupes.vmoperator.common.K8sObserver;
|
import org.jdrupes.vmoperator.common.K8sObserver;
|
||||||
import org.jdrupes.vmoperator.common.K8sV1SecretStub;
|
import org.jdrupes.vmoperator.common.K8sV1SecretStub;
|
||||||
|
import org.jdrupes.vmoperator.common.VmDefinition;
|
||||||
import static org.jdrupes.vmoperator.manager.Constants.COMP_DISPLAY_SECRET;
|
import static org.jdrupes.vmoperator.manager.Constants.COMP_DISPLAY_SECRET;
|
||||||
import org.jdrupes.vmoperator.manager.events.ResetVm;
|
import org.jdrupes.vmoperator.manager.events.ResetVm;
|
||||||
import org.jdrupes.vmoperator.manager.events.VmChannel;
|
import org.jdrupes.vmoperator.manager.events.VmChannel;
|
||||||
import org.jdrupes.vmoperator.manager.events.VmDefChanged;
|
import org.jdrupes.vmoperator.manager.events.VmDefChanged;
|
||||||
|
import org.jdrupes.vmoperator.util.DataPath;
|
||||||
import org.jdrupes.vmoperator.util.ExtendedObjectWrapper;
|
import org.jdrupes.vmoperator.util.ExtendedObjectWrapper;
|
||||||
import org.jdrupes.vmoperator.util.GsonPtr;
|
|
||||||
import org.jgrapes.core.Channel;
|
import org.jgrapes.core.Channel;
|
||||||
import org.jgrapes.core.Component;
|
import org.jgrapes.core.Component;
|
||||||
import org.jgrapes.core.annotation.Handler;
|
import org.jgrapes.core.annotation.Handler;
|
||||||
|
|
@ -135,6 +137,10 @@ import org.jgrapes.util.events.ConfigurationUpdate;
|
||||||
"PMD.AvoidDuplicateLiterals" })
|
"PMD.AvoidDuplicateLiterals" })
|
||||||
public class Reconciler extends Component {
|
public class Reconciler extends Component {
|
||||||
|
|
||||||
|
/** The Constant mapper. */
|
||||||
|
@SuppressWarnings("PMD.FieldNamingConventions")
|
||||||
|
protected static final ObjectMapper mapper = new ObjectMapper();
|
||||||
|
|
||||||
@SuppressWarnings("PMD.SingularField")
|
@SuppressWarnings("PMD.SingularField")
|
||||||
private final Configuration fmConfig;
|
private final Configuration fmConfig;
|
||||||
private final ConfigMapReconciler cmReconciler;
|
private final ConfigMapReconciler cmReconciler;
|
||||||
|
|
@ -203,17 +209,17 @@ public class Reconciler extends Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ownership relationships takes care of deletions
|
// Ownership relationships takes care of deletions
|
||||||
var defMeta = event.vmDefinition().getMetadata();
|
|
||||||
if (event.type() == K8sObserver.ResponseType.DELETED) {
|
if (event.type() == K8sObserver.ResponseType.DELETED) {
|
||||||
logger.fine(() -> "VM \"" + defMeta.getName() + "\" deleted");
|
logger.fine(
|
||||||
|
() -> "VM \"" + event.vmDefinition().name() + "\" deleted");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reconcile, use "augmented" vm definition for model
|
// Create model for processing templates
|
||||||
Map<String, Object> model
|
Map<String, Object> model
|
||||||
= prepareModel(channel.client(), patchCr(event.vmDefinition()));
|
= prepareModel(channel.client(), event.vmDefinition());
|
||||||
var configMap = cmReconciler.reconcile(model, channel);
|
var configMap = cmReconciler.reconcile(model, channel);
|
||||||
model.put("cm", configMap.getRaw());
|
model.put("cm", configMap);
|
||||||
dsReconciler.reconcile(event, model, channel);
|
dsReconciler.reconcile(event, model, channel);
|
||||||
// Manage (eventual) removal of stateful set.
|
// Manage (eventual) removal of stateful set.
|
||||||
stsReconciler.reconcile(event, model, channel);
|
stsReconciler.reconcile(event, model, channel);
|
||||||
|
|
@ -235,81 +241,22 @@ public class Reconciler extends Component {
|
||||||
@Handler
|
@Handler
|
||||||
public void onResetVm(ResetVm event, VmChannel channel)
|
public void onResetVm(ResetVm event, VmChannel channel)
|
||||||
throws ApiException, IOException, TemplateException {
|
throws ApiException, IOException, TemplateException {
|
||||||
var defRoot
|
var vmDef = channel.vmDefinition();
|
||||||
= GsonPtr.to(channel.vmDefinition().data()).get(JsonObject.class);
|
vmDef.extra("resetCount", vmDef.<Long> extra("resetCount") + 1);
|
||||||
defRoot.addProperty("resetCount",
|
|
||||||
defRoot.get("resetCount").getAsLong() + 1);
|
|
||||||
Map<String, Object> model
|
Map<String, Object> model
|
||||||
= prepareModel(channel.client(), patchCr(channel.vmDefinition()));
|
= prepareModel(channel.client(), channel.vmDefinition());
|
||||||
cmReconciler.reconcile(model, channel);
|
cmReconciler.reconcile(model, channel);
|
||||||
}
|
}
|
||||||
|
|
||||||
private DynamicKubernetesObject patchCr(K8sDynamicModel vmDef) {
|
@SuppressWarnings({ "PMD.CognitiveComplexity", "PMD.NPathComplexity" })
|
||||||
var json = vmDef.data().deepCopy();
|
|
||||||
// Adjust cdromImage path
|
|
||||||
adjustCdRomPaths(json);
|
|
||||||
|
|
||||||
// Adjust cloud-init data
|
|
||||||
adjustCloudInitData(json);
|
|
||||||
|
|
||||||
return new DynamicKubernetesObject(json);
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressWarnings("PMD.AvoidLiteralsInIfCondition")
|
|
||||||
private void adjustCdRomPaths(JsonObject json) {
|
|
||||||
var disks
|
|
||||||
= GsonPtr.to(json).to("spec", "vm", "disks").get(JsonArray.class);
|
|
||||||
for (var disk : disks) {
|
|
||||||
var cdrom = (JsonObject) ((JsonObject) disk).get("cdrom");
|
|
||||||
if (cdrom == null) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
String image = cdrom.get("image").getAsString();
|
|
||||||
if (image.isEmpty()) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
@SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops")
|
|
||||||
var imageUri = new URI("file://" + Constants.IMAGE_REPO_PATH
|
|
||||||
+ "/").resolve(image);
|
|
||||||
if ("file".equals(imageUri.getScheme())) {
|
|
||||||
cdrom.addProperty("image", imageUri.getPath());
|
|
||||||
} else {
|
|
||||||
cdrom.addProperty("image", imageUri.toString());
|
|
||||||
}
|
|
||||||
} catch (URISyntaxException e) {
|
|
||||||
logger.warning(() -> "Invalid CDROM image: " + image);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void adjustCloudInitData(JsonObject json) {
|
|
||||||
var spec = GsonPtr.to(json).to("spec").get(JsonObject.class);
|
|
||||||
if (!spec.has("cloudInit")) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
var metaData = GsonPtr.to(spec).to("cloudInit", "metaData");
|
|
||||||
if (metaData.getAsString("instance-id").isEmpty()) {
|
|
||||||
metaData.set("instance-id",
|
|
||||||
GsonPtr.to(json).getAsString("metadata", "resourceVersion")
|
|
||||||
.map(s -> "v" + s).orElse("v1"));
|
|
||||||
}
|
|
||||||
if (metaData.getAsString("local-hostname").isEmpty()) {
|
|
||||||
metaData.set("local-hostname",
|
|
||||||
GsonPtr.to(json).getAsString("metadata", "name").get());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressWarnings("PMD.CognitiveComplexity")
|
|
||||||
private Map<String, Object> prepareModel(K8sClient client,
|
private Map<String, Object> prepareModel(K8sClient client,
|
||||||
DynamicKubernetesObject vmDef)
|
VmDefinition vmDef) throws TemplateModelException, ApiException {
|
||||||
throws TemplateModelException, ApiException {
|
|
||||||
@SuppressWarnings("PMD.UseConcurrentHashMap")
|
@SuppressWarnings("PMD.UseConcurrentHashMap")
|
||||||
Map<String, Object> model = new HashMap<>();
|
Map<String, Object> model = new HashMap<>();
|
||||||
model.put("managerVersion",
|
model.put("managerVersion",
|
||||||
Optional.ofNullable(Reconciler.class.getPackage()
|
Optional.ofNullable(Reconciler.class.getPackage()
|
||||||
.getImplementationVersion()).orElse("(Unknown)"));
|
.getImplementationVersion()).orElse("(Unknown)"));
|
||||||
model.put("cr", vmDef.getRaw());
|
model.put("cr", vmDef);
|
||||||
model.put("constants",
|
model.put("constants",
|
||||||
(TemplateHashModel) new DefaultObjectWrapperBuilder(
|
(TemplateHashModel) new DefaultObjectWrapperBuilder(
|
||||||
Configuration.VERSION_2_3_32)
|
Configuration.VERSION_2_3_32)
|
||||||
|
|
@ -321,9 +268,10 @@ public class Reconciler extends Component {
|
||||||
ListOptions options = new ListOptions();
|
ListOptions options = new ListOptions();
|
||||||
options.setLabelSelector("app.kubernetes.io/name=" + APP_NAME + ","
|
options.setLabelSelector("app.kubernetes.io/name=" + APP_NAME + ","
|
||||||
+ "app.kubernetes.io/component=" + COMP_DISPLAY_SECRET + ","
|
+ "app.kubernetes.io/component=" + COMP_DISPLAY_SECRET + ","
|
||||||
+ "app.kubernetes.io/instance=" + vmDef.getMetadata().getName());
|
+ "app.kubernetes.io/instance=" + vmDef.name());
|
||||||
var dsStub = K8sV1SecretStub
|
var dsStub = K8sV1SecretStub
|
||||||
.list(client, vmDef.getMetadata().getNamespace(), options).stream()
|
.list(client, vmDef.namespace(), options)
|
||||||
|
.stream()
|
||||||
.findFirst();
|
.findFirst();
|
||||||
if (dsStub.isPresent()) {
|
if (dsStub.isPresent()) {
|
||||||
dsStub.get().model().ifPresent(m -> {
|
dsStub.get().model().ifPresent(m -> {
|
||||||
|
|
@ -332,14 +280,23 @@ public class Reconciler extends Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Methods
|
// Methods
|
||||||
model.put("parseQuantity", new TemplateMethodModelEx() {
|
model.put("parseQuantity", parseQuantityModel);
|
||||||
|
model.put("formatMemory", formatMemoryModel);
|
||||||
|
model.put("imageLocation", imgageLocationModel);
|
||||||
|
model.put("adjustCloudInitMeta", adjustCloudInitMetaModel);
|
||||||
|
model.put("toJson", toJsonModel);
|
||||||
|
return model;
|
||||||
|
}
|
||||||
|
|
||||||
|
private final TemplateMethodModelEx parseQuantityModel
|
||||||
|
= new TemplateMethodModelEx() {
|
||||||
@Override
|
@Override
|
||||||
@SuppressWarnings("PMD.PreserveStackTrace")
|
@SuppressWarnings("PMD.PreserveStackTrace")
|
||||||
public Object exec(@SuppressWarnings("rawtypes") List arguments)
|
public Object exec(@SuppressWarnings("rawtypes") List arguments)
|
||||||
throws TemplateModelException {
|
throws TemplateModelException {
|
||||||
var arg = arguments.get(0);
|
var arg = arguments.get(0);
|
||||||
if (arg instanceof Number number) {
|
if (arg instanceof SimpleNumber number) {
|
||||||
return number;
|
return number.getAsNumber();
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
return Quantity.fromString(arg.toString()).getNumber();
|
return Quantity.fromString(arg.toString()).getNumber();
|
||||||
|
|
@ -348,8 +305,10 @@ public class Reconciler extends Component {
|
||||||
+ "specified as \"" + arg + "\": " + e.getMessage());
|
+ "specified as \"" + arg + "\": " + e.getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
};
|
||||||
model.put("formatMemory", new TemplateMethodModelEx() {
|
|
||||||
|
private final TemplateMethodModelEx formatMemoryModel
|
||||||
|
= new TemplateMethodModelEx() {
|
||||||
@Override
|
@Override
|
||||||
@SuppressWarnings("PMD.PreserveStackTrace")
|
@SuppressWarnings("PMD.PreserveStackTrace")
|
||||||
public Object exec(@SuppressWarnings("rawtypes") List arguments)
|
public Object exec(@SuppressWarnings("rawtypes") List arguments)
|
||||||
|
|
@ -376,7 +335,71 @@ public class Reconciler extends Component {
|
||||||
}
|
}
|
||||||
return Convertions.formatMemory(bigInt);
|
return Convertions.formatMemory(bigInt);
|
||||||
}
|
}
|
||||||
});
|
};
|
||||||
return model;
|
|
||||||
|
private final TemplateMethodModelEx imgageLocationModel
|
||||||
|
= new TemplateMethodModelEx() {
|
||||||
|
@Override
|
||||||
|
@SuppressWarnings({ "PMD.PreserveStackTrace",
|
||||||
|
"PMD.AvoidLiteralsInIfCondition" })
|
||||||
|
public Object exec(@SuppressWarnings("rawtypes") List arguments)
|
||||||
|
throws TemplateModelException {
|
||||||
|
var image = ((SimpleScalar) arguments.get(0)).getAsString();
|
||||||
|
if (image.isEmpty()) {
|
||||||
|
return "";
|
||||||
}
|
}
|
||||||
|
try {
|
||||||
|
var imageUri = new URI("file://" + Constants.IMAGE_REPO_PATH
|
||||||
|
+ "/").resolve(image);
|
||||||
|
if ("file".equals(imageUri.getScheme())) {
|
||||||
|
return imageUri.getPath();
|
||||||
|
}
|
||||||
|
return imageUri.toString();
|
||||||
|
} catch (URISyntaxException e) {
|
||||||
|
logger.warning(() -> "Invalid CDROM image: " + image);
|
||||||
|
}
|
||||||
|
return image;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private final TemplateMethodModelEx adjustCloudInitMetaModel
|
||||||
|
= new TemplateMethodModelEx() {
|
||||||
|
@Override
|
||||||
|
@SuppressWarnings("PMD.PreserveStackTrace")
|
||||||
|
public Object exec(@SuppressWarnings("rawtypes") List arguments)
|
||||||
|
throws TemplateModelException {
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
var res = (Map<String, Object>) DataPath
|
||||||
|
.deepCopy(((AdapterTemplateModel) arguments.get(0))
|
||||||
|
.getAdaptedObject(Object.class));
|
||||||
|
var metadata
|
||||||
|
= (V1ObjectMeta) ((AdapterTemplateModel) arguments.get(1))
|
||||||
|
.getAdaptedObject(Object.class);
|
||||||
|
if (!res.containsKey("instance-id")) {
|
||||||
|
res.put("instance-id",
|
||||||
|
Optional.ofNullable(metadata.getResourceVersion())
|
||||||
|
.map(s -> "v" + s).orElse("v1"));
|
||||||
|
}
|
||||||
|
if (!res.containsKey("local-hostname")) {
|
||||||
|
res.put("local-hostname", metadata.getName());
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private final TemplateMethodModelEx toJsonModel
|
||||||
|
= new TemplateMethodModelEx() {
|
||||||
|
@Override
|
||||||
|
@SuppressWarnings("PMD.PreserveStackTrace")
|
||||||
|
public Object exec(@SuppressWarnings("rawtypes") List arguments)
|
||||||
|
throws TemplateModelException {
|
||||||
|
try {
|
||||||
|
return mapper.writeValueAsString(
|
||||||
|
((AdapterTemplateModel) arguments.get(0))
|
||||||
|
.getAdaptedObject(Object.class));
|
||||||
|
} catch (JsonProcessingException e) {
|
||||||
|
return "{}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -22,19 +22,14 @@ import freemarker.template.Configuration;
|
||||||
import freemarker.template.TemplateException;
|
import freemarker.template.TemplateException;
|
||||||
import io.kubernetes.client.custom.V1Patch;
|
import io.kubernetes.client.custom.V1Patch;
|
||||||
import io.kubernetes.client.openapi.ApiException;
|
import io.kubernetes.client.openapi.ApiException;
|
||||||
import io.kubernetes.client.util.generic.dynamic.Dynamics;
|
|
||||||
import io.kubernetes.client.util.generic.options.PatchOptions;
|
import io.kubernetes.client.util.generic.options.PatchOptions;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.StringWriter;
|
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.logging.Logger;
|
import java.util.logging.Logger;
|
||||||
import org.jdrupes.vmoperator.common.K8sV1StatefulSetStub;
|
import org.jdrupes.vmoperator.common.K8sV1StatefulSetStub;
|
||||||
|
import org.jdrupes.vmoperator.common.VmDefinition.RequestedVmState;
|
||||||
import org.jdrupes.vmoperator.manager.events.VmChannel;
|
import org.jdrupes.vmoperator.manager.events.VmChannel;
|
||||||
import org.jdrupes.vmoperator.manager.events.VmDefChanged;
|
import org.jdrupes.vmoperator.manager.events.VmDefChanged;
|
||||||
import org.jdrupes.vmoperator.util.GsonPtr;
|
|
||||||
import org.yaml.snakeyaml.LoaderOptions;
|
|
||||||
import org.yaml.snakeyaml.Yaml;
|
|
||||||
import org.yaml.snakeyaml.constructor.SafeConstructor;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Before version 3.4, the pod running the VM was created by a stateful set.
|
* Before version 3.4, the pod running the VM was created by a stateful set.
|
||||||
|
|
@ -45,15 +40,15 @@ import org.yaml.snakeyaml.constructor.SafeConstructor;
|
||||||
/* default */ class StatefulSetReconciler {
|
/* default */ class StatefulSetReconciler {
|
||||||
|
|
||||||
protected final Logger logger = Logger.getLogger(getClass().getName());
|
protected final Logger logger = Logger.getLogger(getClass().getName());
|
||||||
private final Configuration fmConfig;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Instantiates a new stateful set reconciler.
|
* Instantiates a new stateful set reconciler.
|
||||||
*
|
*
|
||||||
* @param fmConfig the fm config
|
* @param fmConfig the fm config
|
||||||
*/
|
*/
|
||||||
|
@SuppressWarnings("PMD.UnusedFormalParameter")
|
||||||
public StatefulSetReconciler(Configuration fmConfig) {
|
public StatefulSetReconciler(Configuration fmConfig) {
|
||||||
this.fmConfig = fmConfig;
|
// Nothing to do
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -70,12 +65,11 @@ import org.yaml.snakeyaml.constructor.SafeConstructor;
|
||||||
public void reconcile(VmDefChanged event, Map<String, Object> model,
|
public void reconcile(VmDefChanged event, Map<String, Object> model,
|
||||||
VmChannel channel)
|
VmChannel channel)
|
||||||
throws IOException, TemplateException, ApiException {
|
throws IOException, TemplateException, ApiException {
|
||||||
var metadata = event.vmDefinition().getMetadata();
|
|
||||||
model.put("usingSts", false);
|
model.put("usingSts", false);
|
||||||
|
|
||||||
// If exists, delete when not running or supposed to be not running.
|
// If exists, delete when not running or supposed to be not running.
|
||||||
var stsStub = K8sV1StatefulSetStub.get(channel.client(),
|
var stsStub = K8sV1StatefulSetStub.get(channel.client(),
|
||||||
metadata.getNamespace(), metadata.getName());
|
event.vmDefinition().namespace(), event.vmDefinition().name());
|
||||||
if (stsStub.model().isEmpty()) {
|
if (stsStub.model().isEmpty()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -94,16 +88,7 @@ import org.yaml.snakeyaml.constructor.SafeConstructor;
|
||||||
// Check if VM is supposed to be stopped. If so,
|
// Check if VM is supposed to be stopped. If so,
|
||||||
// set replicas to 0. This is the first step of the transition,
|
// set replicas to 0. This is the first step of the transition,
|
||||||
// the stateful set will be deleted when the VM is restarted.
|
// the stateful set will be deleted when the VM is restarted.
|
||||||
var fmTemplate = fmConfig.getTemplate("runnerSts.ftl.yaml");
|
if (event.vmDefinition().vmState() == RequestedVmState.RUNNING) {
|
||||||
StringWriter out = new StringWriter();
|
|
||||||
fmTemplate.process(model, out);
|
|
||||||
// Avoid Yaml.load due to
|
|
||||||
// https://github.com/kubernetes-client/java/issues/2741
|
|
||||||
var stsDef = Dynamics.newFromYaml(
|
|
||||||
new Yaml(new SafeConstructor(new LoaderOptions())), out.toString());
|
|
||||||
var desired = GsonPtr.to(stsDef.getRaw())
|
|
||||||
.to("spec").getAsInt("replicas").orElse(1);
|
|
||||||
if (desired == 1) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -111,12 +96,12 @@ import org.yaml.snakeyaml.constructor.SafeConstructor;
|
||||||
PatchOptions opts = new PatchOptions();
|
PatchOptions opts = new PatchOptions();
|
||||||
opts.setForce(true);
|
opts.setForce(true);
|
||||||
opts.setFieldManager("kubernetes-java-kubectl-apply");
|
opts.setFieldManager("kubernetes-java-kubectl-apply");
|
||||||
if (stsStub.patch(V1Patch.PATCH_FORMAT_APPLY_YAML,
|
if (stsStub.patch(V1Patch.PATCH_FORMAT_JSON_PATCH,
|
||||||
new V1Patch(channel.client().getJSON().serialize(stsDef)), opts)
|
new V1Patch("[{\"op\": \"replace\", \"path\": \"/spec/replicas"
|
||||||
.isEmpty()) {
|
+ "\", \"value\": 0}]"),
|
||||||
|
channel.client().defaultPatchOptions()).isEmpty()) {
|
||||||
logger.warning(
|
logger.warning(
|
||||||
() -> "Could not patch stateful set for " + stsStub.name());
|
() -> "Could not patch stateful set for " + stsStub.name());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,13 +18,15 @@
|
||||||
|
|
||||||
package org.jdrupes.vmoperator.manager;
|
package org.jdrupes.vmoperator.manager;
|
||||||
|
|
||||||
import com.google.gson.JsonArray;
|
|
||||||
import com.google.gson.JsonObject;
|
|
||||||
import io.kubernetes.client.openapi.ApiException;
|
import io.kubernetes.client.openapi.ApiException;
|
||||||
import io.kubernetes.client.openapi.models.V1ObjectMeta;
|
import io.kubernetes.client.openapi.models.V1ObjectMeta;
|
||||||
import io.kubernetes.client.util.Watch;
|
import io.kubernetes.client.util.Watch;
|
||||||
import io.kubernetes.client.util.generic.options.ListOptions;
|
import io.kubernetes.client.util.generic.options.ListOptions;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.logging.Level;
|
import java.util.logging.Level;
|
||||||
|
|
@ -37,6 +39,7 @@ import org.jdrupes.vmoperator.common.K8sObserver.ResponseType;
|
||||||
import org.jdrupes.vmoperator.common.K8sV1ConfigMapStub;
|
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.VmDefinitionModel;
|
import org.jdrupes.vmoperator.common.VmDefinitionModel;
|
||||||
import org.jdrupes.vmoperator.common.VmDefinitionModels;
|
import org.jdrupes.vmoperator.common.VmDefinitionModels;
|
||||||
import org.jdrupes.vmoperator.common.VmDefinitionStub;
|
import org.jdrupes.vmoperator.common.VmDefinitionStub;
|
||||||
|
|
@ -46,7 +49,7 @@ import static org.jdrupes.vmoperator.manager.Constants.VM_OP_NAME;
|
||||||
import org.jdrupes.vmoperator.manager.events.ChannelManager;
|
import org.jdrupes.vmoperator.manager.events.ChannelManager;
|
||||||
import org.jdrupes.vmoperator.manager.events.VmChannel;
|
import org.jdrupes.vmoperator.manager.events.VmChannel;
|
||||||
import org.jdrupes.vmoperator.manager.events.VmDefChanged;
|
import org.jdrupes.vmoperator.manager.events.VmDefChanged;
|
||||||
import org.jdrupes.vmoperator.util.GsonPtr;
|
import org.jdrupes.vmoperator.util.DataPath;
|
||||||
import org.jgrapes.core.Channel;
|
import org.jgrapes.core.Channel;
|
||||||
import org.jgrapes.core.Event;
|
import org.jgrapes.core.Event;
|
||||||
|
|
||||||
|
|
@ -116,42 +119,47 @@ public class VmMonitor extends
|
||||||
V1ObjectMeta metadata = response.object.getMetadata();
|
V1ObjectMeta metadata = response.object.getMetadata();
|
||||||
VmChannel channel = channelManager.channelGet(metadata.getName());
|
VmChannel channel = channelManager.channelGet(metadata.getName());
|
||||||
|
|
||||||
|
// Remove from channel manager if deleted
|
||||||
|
if (ResponseType.valueOf(response.type) == ResponseType.DELETED) {
|
||||||
|
channelManager.remove(metadata.getName());
|
||||||
|
}
|
||||||
|
|
||||||
// Get full definition and associate with channel as backup
|
// Get full definition and associate with channel as backup
|
||||||
var vmDef = response.object;
|
var vmModel = response.object;
|
||||||
if (vmDef.data() == null) {
|
if (vmModel.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
|
||||||
vmDef = getModel(client, vmDef);
|
vmModel = getModel(client, vmModel);
|
||||||
}
|
}
|
||||||
if (vmDef.data() != null) {
|
VmDefinition vmDef = null;
|
||||||
|
if (vmModel.data() != null) {
|
||||||
// New data, augment and save
|
// New data, augment and save
|
||||||
|
vmDef = client.getJSON().getGson().fromJson(vmModel.data(),
|
||||||
|
VmDefinition.class);
|
||||||
addDynamicData(channel.client(), vmDef, channel.vmDefinition());
|
addDynamicData(channel.client(), vmDef, channel.vmDefinition());
|
||||||
channel.setVmDefinition(vmDef);
|
channel.setVmDefinition(vmDef);
|
||||||
} else {
|
}
|
||||||
// Reuse cached
|
if (vmDef == null) {
|
||||||
|
// Reuse cached (e.g. if deleted)
|
||||||
vmDef = channel.vmDefinition();
|
vmDef = channel.vmDefinition();
|
||||||
}
|
}
|
||||||
if (vmDef == null) {
|
if (vmDef == null) {
|
||||||
logger.warning(
|
logger.warning(() -> "Cannot get defintion for "
|
||||||
() -> "Cannot get model for " + response.object.getMetadata());
|
+ response.object.getMetadata());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (ResponseType.valueOf(response.type) == ResponseType.DELETED) {
|
|
||||||
channelManager.remove(metadata.getName());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create and fire changed event. Remove channel from channel
|
// Create and fire changed event. Remove channel from channel
|
||||||
// manager on completion.
|
// manager on completion.
|
||||||
channel.pipeline()
|
channel.pipeline()
|
||||||
.fire(Event.onCompletion(
|
.fire(Event.onCompletion(
|
||||||
new VmDefChanged(ResponseType.valueOf(response.type),
|
new VmDefChanged(ResponseType.valueOf(response.type),
|
||||||
channel.setGeneration(
|
channel.setGeneration(response.object.getMetadata()
|
||||||
response.object.getMetadata().getGeneration()),
|
.getGeneration()),
|
||||||
vmDef),
|
vmDef),
|
||||||
e -> {
|
e -> {
|
||||||
if (e.type() == ResponseType.DELETED) {
|
if (e.type() == ResponseType.DELETED) {
|
||||||
channelManager
|
channelManager.remove(e.vmDefinition().name());
|
||||||
.remove(e.vmDefinition().metadata().getName());
|
|
||||||
}
|
}
|
||||||
}), channel);
|
}), channel);
|
||||||
}
|
}
|
||||||
|
|
@ -166,28 +174,30 @@ public class VmMonitor extends
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void addDynamicData(K8sClient client, VmDefinitionModel vmState,
|
@SuppressWarnings("PMD.AvoidDuplicateLiterals")
|
||||||
VmDefinitionModel prevState) {
|
private void addDynamicData(K8sClient client, VmDefinition vmDef,
|
||||||
var rootNode = GsonPtr.to(vmState.data()).get(JsonObject.class);
|
VmDefinition prevState) {
|
||||||
|
|
||||||
// Maintain (or initialize) the resetCount
|
// Maintain (or initialize) the resetCount
|
||||||
rootNode.addProperty("resetCount", Optional.ofNullable(prevState)
|
vmDef.extra("resetCount",
|
||||||
.map(ps -> GsonPtr.to(ps.data()))
|
Optional.ofNullable(prevState).map(d -> d.extra("resetCount"))
|
||||||
.flatMap(d -> d.getAsLong("resetCount")).orElse(0L));
|
.orElse(0L));
|
||||||
|
|
||||||
|
// Node information
|
||||||
// Add defaults in case the VM is not running
|
// Add defaults in case the VM is not running
|
||||||
rootNode.addProperty("nodeName", "");
|
vmDef.extra("nodeName", "");
|
||||||
rootNode.addProperty("nodeAddress", "");
|
vmDef.extra("nodeAddress", "");
|
||||||
|
|
||||||
// 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
|
||||||
// VM which is irritating. So check condition first.
|
// VM which is irritating. So check condition first.
|
||||||
var isRunning = GsonPtr.to(rootNode).to("status", "conditions")
|
@SuppressWarnings("PMD.LambdaCanBeMethodReference")
|
||||||
.get(JsonArray.class)
|
var isRunning
|
||||||
.asList().stream().filter(el -> "Running"
|
= vmDef.<List<Map<String, Object>>> fromStatus("conditions")
|
||||||
.equals(((JsonObject) el).get("type").getAsString()))
|
.orElse(Collections.emptyList()).stream()
|
||||||
.findFirst().map(el -> "True"
|
.filter(cond -> DataPath.get(cond, "type")
|
||||||
.equals(((JsonObject) el).get("status").getAsString()))
|
.map(t -> "Running".equals(t)).orElse(false))
|
||||||
|
.findFirst().map(cond -> DataPath.get(cond, "status")
|
||||||
|
.map(s -> "True".equals(s)).orElse(false))
|
||||||
.orElse(false);
|
.orElse(false);
|
||||||
if (!isRunning) {
|
if (!isRunning) {
|
||||||
return;
|
return;
|
||||||
|
|
@ -195,22 +205,22 @@ public class VmMonitor extends
|
||||||
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
|
||||||
+ ",app.kubernetes.io/instance=" + vmState.getMetadata().getName());
|
+ ",app.kubernetes.io/instance=" + vmDef.name());
|
||||||
try {
|
try {
|
||||||
var podList
|
var podList
|
||||||
= 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();
|
||||||
rootNode.addProperty("nodeName", nodeName);
|
vmDef.extra("nodeName", nodeName);
|
||||||
logger.fine(() -> "Added node name " + nodeName
|
logger.fine(() -> "Added node name " + nodeName
|
||||||
+ " to VM info for " + vmState.getMetadata().getName());
|
+ " to VM info for " + vmDef.name());
|
||||||
@SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops")
|
@SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops")
|
||||||
var addrs = new JsonArray();
|
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);
|
||||||
rootNode.add("nodeAddresses", addrs);
|
vmDef.extra("nodeAddresses", addrs);
|
||||||
logger.fine(() -> "Added node addresses " + addrs
|
logger.fine(() -> "Added node addresses " + addrs
|
||||||
+ " to VM info for " + vmState.getMetadata().getName());
|
+ " to VM info for " + vmDef.name());
|
||||||
}
|
}
|
||||||
} catch (ApiException e) {
|
} catch (ApiException e) {
|
||||||
logger.log(Level.WARNING, e,
|
logger.log(Level.WARNING, e,
|
||||||
|
|
|
||||||
64
org.jdrupes.vmoperator.manager/test-resources/basic-vm.yaml
Normal file
64
org.jdrupes.vmoperator.manager/test-resources/basic-vm.yaml
Normal file
|
|
@ -0,0 +1,64 @@
|
||||||
|
apiVersion: "vmoperator.jdrupes.org/v1"
|
||||||
|
kind: VirtualMachine
|
||||||
|
metadata:
|
||||||
|
namespace: vmop-dev
|
||||||
|
name: unittest-vm
|
||||||
|
spec:
|
||||||
|
image:
|
||||||
|
repository: docker-registry.lan.mnl.de
|
||||||
|
path: vmoperator/this.will.never.start
|
||||||
|
version: 0.0.0
|
||||||
|
|
||||||
|
cloudInit:
|
||||||
|
metaData: {}
|
||||||
|
|
||||||
|
vm:
|
||||||
|
# state: Running
|
||||||
|
maximumRam: 4Gi
|
||||||
|
currentRam: 2Gi
|
||||||
|
maximumCpus: 4
|
||||||
|
currentCpus: 2
|
||||||
|
powerdownTimeout: 1
|
||||||
|
|
||||||
|
networks:
|
||||||
|
- user: {}
|
||||||
|
disks:
|
||||||
|
- cdrom:
|
||||||
|
image: https://test.com/test.iso
|
||||||
|
bootindex: 0
|
||||||
|
- cdrom:
|
||||||
|
image: "image.iso"
|
||||||
|
- volumeClaimTemplate:
|
||||||
|
metadata:
|
||||||
|
name: system
|
||||||
|
annotations:
|
||||||
|
use_as: system-disk
|
||||||
|
spec:
|
||||||
|
storageClassName: local-path
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
storage: 1Gi
|
||||||
|
- volumeClaimTemplate:
|
||||||
|
spec:
|
||||||
|
storageClassName: local-path
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
storage: 1Gi
|
||||||
|
|
||||||
|
display:
|
||||||
|
outputs: 2
|
||||||
|
spice:
|
||||||
|
port: 5812
|
||||||
|
usbRedirects: 2
|
||||||
|
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
cpu: 1
|
||||||
|
memory: 2Gi
|
||||||
|
|
||||||
|
loadBalancerService:
|
||||||
|
labels:
|
||||||
|
label2: replaced
|
||||||
|
label3: added
|
||||||
|
annotations:
|
||||||
|
anno1: added
|
||||||
|
|
@ -1,35 +0,0 @@
|
||||||
apiVersion: "vmoperator.jdrupes.org/v1"
|
|
||||||
kind: VirtualMachine
|
|
||||||
metadata:
|
|
||||||
namespace: vmop-dev
|
|
||||||
name: unittest-vm
|
|
||||||
spec:
|
|
||||||
resources:
|
|
||||||
requests:
|
|
||||||
cpu: 1
|
|
||||||
memory: 2Gi
|
|
||||||
|
|
||||||
loadBalancerService:
|
|
||||||
labels:
|
|
||||||
test2: null
|
|
||||||
test3: added
|
|
||||||
|
|
||||||
vm:
|
|
||||||
# state: Running
|
|
||||||
maximumRam: 4Gi
|
|
||||||
currentRam: 2Gi
|
|
||||||
maximumCpus: 4
|
|
||||||
currentCpus: 2
|
|
||||||
powerdownTimeout: 1
|
|
||||||
|
|
||||||
networks:
|
|
||||||
- user: {}
|
|
||||||
disks:
|
|
||||||
- cdrom:
|
|
||||||
# image: ""
|
|
||||||
image: https://download.fedoraproject.org/pub/fedora/linux/releases/38/Workstation/x86_64/iso/Fedora-Workstation-Live-x86_64-38-1.6.iso
|
|
||||||
# image: "Fedora-Workstation-Live-x86_64-38-1.6.iso"
|
|
||||||
|
|
||||||
display:
|
|
||||||
spice:
|
|
||||||
port: 5812
|
|
||||||
|
|
@ -1,12 +1,19 @@
|
||||||
package org.jdrupes.vmoperator.manager;
|
package org.jdrupes.vmoperator.manager;
|
||||||
|
|
||||||
import io.kubernetes.client.Discovery.APIResource;
|
import io.kubernetes.client.Discovery.APIResource;
|
||||||
|
import io.kubernetes.client.custom.Quantity;
|
||||||
|
import io.kubernetes.client.custom.V1Patch;
|
||||||
import io.kubernetes.client.openapi.ApiException;
|
import io.kubernetes.client.openapi.ApiException;
|
||||||
import io.kubernetes.client.util.generic.options.ListOptions;
|
import io.kubernetes.client.util.generic.options.ListOptions;
|
||||||
|
import io.kubernetes.client.util.generic.options.PatchOptions;
|
||||||
import java.io.FileReader;
|
import java.io.FileReader;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import static org.jdrupes.vmoperator.common.Constants.APP_NAME;
|
import static org.jdrupes.vmoperator.common.Constants.APP_NAME;
|
||||||
|
import static org.jdrupes.vmoperator.common.Constants.COMP_DISPLAY_SECRET;
|
||||||
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 static org.jdrupes.vmoperator.common.Constants.VM_OP_NAME;
|
import static org.jdrupes.vmoperator.common.Constants.VM_OP_NAME;
|
||||||
|
|
@ -15,7 +22,11 @@ import org.jdrupes.vmoperator.common.K8sClient;
|
||||||
import org.jdrupes.vmoperator.common.K8sDynamicStub;
|
import org.jdrupes.vmoperator.common.K8sDynamicStub;
|
||||||
import org.jdrupes.vmoperator.common.K8sV1ConfigMapStub;
|
import org.jdrupes.vmoperator.common.K8sV1ConfigMapStub;
|
||||||
import org.jdrupes.vmoperator.common.K8sV1DeploymentStub;
|
import org.jdrupes.vmoperator.common.K8sV1DeploymentStub;
|
||||||
|
import org.jdrupes.vmoperator.common.K8sV1PodStub;
|
||||||
import org.jdrupes.vmoperator.common.K8sV1PvcStub;
|
import org.jdrupes.vmoperator.common.K8sV1PvcStub;
|
||||||
|
import org.jdrupes.vmoperator.common.K8sV1SecretStub;
|
||||||
|
import org.jdrupes.vmoperator.common.K8sV1ServiceStub;
|
||||||
|
import org.jdrupes.vmoperator.util.DataPath;
|
||||||
import org.junit.jupiter.api.AfterAll;
|
import org.junit.jupiter.api.AfterAll;
|
||||||
import static org.junit.jupiter.api.Assertions.*;
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
import org.junit.jupiter.api.BeforeAll;
|
import org.junit.jupiter.api.BeforeAll;
|
||||||
|
|
@ -29,6 +40,9 @@ class BasicTests {
|
||||||
private static K8sClient client;
|
private static K8sClient client;
|
||||||
private static APIResource vmsContext;
|
private static APIResource vmsContext;
|
||||||
private static K8sV1DeploymentStub mgrDeployment;
|
private static K8sV1DeploymentStub mgrDeployment;
|
||||||
|
private static K8sDynamicStub vmStub;
|
||||||
|
private static final String VM_NAME = "unittest-vm";
|
||||||
|
private static final Object EXISTS = new Object();
|
||||||
|
|
||||||
@BeforeAll
|
@BeforeAll
|
||||||
static void setUpBeforeClass() throws Exception {
|
static void setUpBeforeClass() throws Exception {
|
||||||
|
|
@ -38,23 +52,40 @@ class BasicTests {
|
||||||
// Get client
|
// Get client
|
||||||
client = new K8sClient();
|
client = new K8sClient();
|
||||||
|
|
||||||
|
// Update manager pod by scaling deployment
|
||||||
|
mgrDeployment
|
||||||
|
= K8sV1DeploymentStub.get(client, "vmop-dev", "vm-operator");
|
||||||
|
mgrDeployment.scale(0);
|
||||||
|
mgrDeployment.scale(1);
|
||||||
|
waitForManager();
|
||||||
|
|
||||||
// Context for working with our CR
|
// Context for working with our CR
|
||||||
var apiRes = K8s.context(client, VM_OP_GROUP, null, VM_OP_KIND_VM);
|
var apiRes = K8s.context(client, VM_OP_GROUP, null, VM_OP_KIND_VM);
|
||||||
assertTrue(apiRes.isPresent());
|
assertTrue(apiRes.isPresent());
|
||||||
vmsContext = apiRes.get();
|
vmsContext = apiRes.get();
|
||||||
|
|
||||||
// Cleanup existing VM
|
// Cleanup existing VM
|
||||||
K8sDynamicStub.get(client, vmsContext, "vmop-dev", "unittest-vm")
|
K8sDynamicStub.get(client, vmsContext, "vmop-dev", VM_NAME)
|
||||||
.delete();
|
.delete();
|
||||||
|
ListOptions listOpts = new ListOptions();
|
||||||
|
listOpts.setLabelSelector("app.kubernetes.io/name=" + APP_NAME + ","
|
||||||
|
+ "app.kubernetes.io/instance=" + VM_NAME + ","
|
||||||
|
+ "app.kubernetes.io/component=" + COMP_DISPLAY_SECRET);
|
||||||
|
var secrets = K8sV1SecretStub.list(client, "vmop-dev", listOpts);
|
||||||
|
for (var secret : secrets) {
|
||||||
|
secret.delete();
|
||||||
|
}
|
||||||
|
deletePvcs();
|
||||||
|
|
||||||
// Update manager pod by scaling deployment
|
// Load from Yaml
|
||||||
mgrDeployment
|
var rdr = new FileReader("test-resources/basic-vm.yaml");
|
||||||
= K8sV1DeploymentStub.get(client, "vmop-dev", "vm-operator");
|
vmStub = K8sDynamicStub.createFromYaml(client, vmsContext, rdr);
|
||||||
mgrDeployment.scale(0);
|
assertTrue(vmStub.model().isPresent());
|
||||||
mgrDeployment.scale(1);
|
}
|
||||||
|
|
||||||
|
private static void waitForManager()
|
||||||
|
throws ApiException, InterruptedException {
|
||||||
// Wait until available
|
// Wait until available
|
||||||
|
|
||||||
for (int i = 0; i < 10; i++) {
|
for (int i = 0; i < 10; i++) {
|
||||||
if (mgrDeployment.model().get().getStatus().getConditions()
|
if (mgrDeployment.model().get().getStatus().getConditions()
|
||||||
.stream().filter(c -> "Available".equals(c.getType())).findAny()
|
.stream().filter(c -> "Available".equals(c.getType())).findAny()
|
||||||
|
|
@ -66,70 +97,250 @@ class BasicTests {
|
||||||
fail("vm-operator not deployed.");
|
fail("vm-operator not deployed.");
|
||||||
}
|
}
|
||||||
|
|
||||||
@AfterAll
|
private static void deletePvcs() throws ApiException {
|
||||||
static void tearDownAfterClass() throws Exception {
|
|
||||||
// Bring down manager
|
|
||||||
mgrDeployment.scale(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void test() throws IOException, InterruptedException, ApiException {
|
|
||||||
// Load from Yaml
|
|
||||||
var rdr = new FileReader("test-resources/unittest-vm.yaml");
|
|
||||||
var vmStub = K8sDynamicStub.createFromYaml(client, vmsContext, rdr);
|
|
||||||
assertTrue(vmStub.model().isPresent());
|
|
||||||
|
|
||||||
// Wait for created resources
|
|
||||||
assertTrue(waitForConfigMap(client));
|
|
||||||
assertTrue(waitForPvc(client));
|
|
||||||
|
|
||||||
// Check config map
|
|
||||||
var config = K8sV1ConfigMapStub.get(client, "vmop-dev", "unittest-vm")
|
|
||||||
.model().get();
|
|
||||||
var yaml = new Yaml(new SafeConstructor(new LoaderOptions()))
|
|
||||||
.load(config.getData().get("config.yaml"));
|
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
var maximumRam = ((Map<String, Map<String, Map<String, String>>>) yaml)
|
|
||||||
.get("/Runner").get("vm").get("maximumRam");
|
|
||||||
assertEquals("4 GiB", maximumRam);
|
|
||||||
|
|
||||||
// Cleanup
|
|
||||||
K8sDynamicStub.get(client, vmsContext, "vmop-dev", "unittest-vm")
|
|
||||||
.delete();
|
|
||||||
ListOptions listOpts = new ListOptions();
|
ListOptions listOpts = new ListOptions();
|
||||||
listOpts.setLabelSelector(
|
listOpts.setLabelSelector(
|
||||||
"app.kubernetes.io/managed-by=" + VM_OP_NAME + ","
|
"app.kubernetes.io/managed-by=" + VM_OP_NAME + ","
|
||||||
+ "app.kubernetes.io/name=" + APP_NAME + ","
|
+ "app.kubernetes.io/name=" + APP_NAME + ","
|
||||||
+ "app.kubernetes.io/instance=unittest-vm");
|
+ "app.kubernetes.io/instance=" + VM_NAME);
|
||||||
var knownPvcs = K8sV1PvcStub.list(client, "vmop-dev", listOpts);
|
var knownPvcs = K8sV1PvcStub.list(client, "vmop-dev", listOpts);
|
||||||
for (var pvc : knownPvcs) {
|
for (var pvc : knownPvcs) {
|
||||||
pvc.delete();
|
pvc.delete();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean waitForConfigMap(K8sClient client)
|
@AfterAll
|
||||||
throws InterruptedException, ApiException {
|
static void tearDownAfterClass() throws Exception {
|
||||||
var stub = K8sV1ConfigMapStub.get(client, "vmop-dev", "unittest-vm");
|
// Cleanup
|
||||||
for (int i = 0; i < 10; i++) {
|
K8sDynamicStub.get(client, vmsContext, "vmop-dev", VM_NAME)
|
||||||
if (stub.model().isPresent()) {
|
.delete();
|
||||||
return true;
|
deletePvcs();
|
||||||
}
|
|
||||||
Thread.sleep(1000);
|
// Bring down manager
|
||||||
}
|
mgrDeployment.scale(0);
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean waitForPvc(K8sClient client)
|
@Test
|
||||||
throws InterruptedException, ApiException {
|
void testConfigMap()
|
||||||
var stub
|
throws IOException, InterruptedException, ApiException {
|
||||||
= K8sV1PvcStub.get(client, "vmop-dev", "unittest-vm-runner-data");
|
K8sV1ConfigMapStub stub
|
||||||
|
= K8sV1ConfigMapStub.get(client, "vmop-dev", VM_NAME);
|
||||||
for (int i = 0; i < 10; i++) {
|
for (int i = 0; i < 10; i++) {
|
||||||
if (stub.model().isPresent()) {
|
if (stub.model().isPresent()) {
|
||||||
return true;
|
break;
|
||||||
}
|
}
|
||||||
Thread.sleep(1000);
|
Thread.sleep(1000);
|
||||||
}
|
}
|
||||||
return false;
|
// Check config map
|
||||||
|
var config = stub.model().get();
|
||||||
|
Map<List<? extends Object>, Object> toCheck = Map.of(
|
||||||
|
List.of("namespace"), "vmop-dev",
|
||||||
|
List.of("name"), VM_NAME,
|
||||||
|
List.of("labels", "app.kubernetes.io/name"), Constants.APP_NAME,
|
||||||
|
List.of("labels", "app.kubernetes.io/instance"), VM_NAME,
|
||||||
|
List.of("labels", "app.kubernetes.io/managed-by"),
|
||||||
|
Constants.VM_OP_NAME,
|
||||||
|
List.of("annotations", "vmoperator.jdrupes.org/version"), EXISTS,
|
||||||
|
List.of("ownerReferences", 0, "apiVersion"),
|
||||||
|
vmsContext.getGroup() + "/" + vmsContext.getVersions().get(0),
|
||||||
|
List.of("ownerReferences", 0, "kind"), Constants.VM_OP_KIND_VM,
|
||||||
|
List.of("ownerReferences", 0, "name"), VM_NAME,
|
||||||
|
List.of("ownerReferences", 0, "uid"), EXISTS);
|
||||||
|
checkProps(config.getMetadata(), toCheck);
|
||||||
|
|
||||||
|
toCheck = new LinkedHashMap<>();
|
||||||
|
toCheck.put(List.of("/Runner", "guestShutdownStops"), false);
|
||||||
|
toCheck.put(List.of("/Runner", "cloudInit", "metaData", "instance-id"),
|
||||||
|
EXISTS);
|
||||||
|
toCheck.put(
|
||||||
|
List.of("/Runner", "cloudInit", "metaData", "local-hostname"),
|
||||||
|
VM_NAME);
|
||||||
|
toCheck.put(List.of("/Runner", "cloudInit", "userData"), Map.of());
|
||||||
|
toCheck.put(List.of("/Runner", "vm", "maximumRam"), "4 GiB");
|
||||||
|
toCheck.put(List.of("/Runner", "vm", "currentRam"), "2 GiB");
|
||||||
|
toCheck.put(List.of("/Runner", "vm", "maximumCpus"), 4);
|
||||||
|
toCheck.put(List.of("/Runner", "vm", "currentCpus"), 2);
|
||||||
|
toCheck.put(List.of("/Runner", "vm", "powerdownTimeout"), 1);
|
||||||
|
toCheck.put(List.of("/Runner", "vm", "network", 0, "type"), "user");
|
||||||
|
toCheck.put(List.of("/Runner", "vm", "drives", 0, "type"), "ide-cd");
|
||||||
|
toCheck.put(List.of("/Runner", "vm", "drives", 0, "file"),
|
||||||
|
"https://test.com/test.iso");
|
||||||
|
toCheck.put(List.of("/Runner", "vm", "drives", 0, "bootindex"), 0);
|
||||||
|
toCheck.put(List.of("/Runner", "vm", "drives", 1, "type"), "ide-cd");
|
||||||
|
toCheck.put(List.of("/Runner", "vm", "drives", 1, "file"),
|
||||||
|
"/var/local/vmop-image-repository/image.iso");
|
||||||
|
toCheck.put(List.of("/Runner", "vm", "drives", 2, "type"), "raw");
|
||||||
|
toCheck.put(List.of("/Runner", "vm", "drives", 2, "resource"),
|
||||||
|
"/dev/system-disk");
|
||||||
|
toCheck.put(List.of("/Runner", "vm", "drives", 3, "type"), "raw");
|
||||||
|
toCheck.put(List.of("/Runner", "vm", "drives", 3, "resource"),
|
||||||
|
"/dev/disk-1");
|
||||||
|
toCheck.put(List.of("/Runner", "vm", "display", "outputs"), 2);
|
||||||
|
toCheck.put(List.of("/Runner", "vm", "display", "spice", "port"), 5812);
|
||||||
|
toCheck.put(
|
||||||
|
List.of("/Runner", "vm", "display", "spice", "usbRedirects"), 2);
|
||||||
|
var cm = new Yaml(new SafeConstructor(new LoaderOptions()))
|
||||||
|
.load(config.getData().get("config.yaml"));
|
||||||
|
checkProps(cm, toCheck);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testDisplaySecret() throws ApiException, InterruptedException {
|
||||||
|
ListOptions listOpts = new ListOptions();
|
||||||
|
listOpts.setLabelSelector("app.kubernetes.io/name=" + APP_NAME + ","
|
||||||
|
+ "app.kubernetes.io/instance=" + VM_NAME + ","
|
||||||
|
+ "app.kubernetes.io/component=" + COMP_DISPLAY_SECRET);
|
||||||
|
Collection<K8sV1SecretStub> secrets = null;
|
||||||
|
for (int i = 0; i < 10; i++) {
|
||||||
|
secrets = K8sV1SecretStub.list(client, "vmop-dev", listOpts);
|
||||||
|
if (secrets.size() > 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
Thread.sleep(1000);
|
||||||
|
}
|
||||||
|
assertEquals(1, secrets.size());
|
||||||
|
var secretData = secrets.iterator().next().model().get().getData();
|
||||||
|
checkProps(secretData, Map.of(
|
||||||
|
List.of("display-password"), EXISTS));
|
||||||
|
assertEquals("now", new String(secretData.get("password-expiry")));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testRunnerPvc() throws ApiException, InterruptedException {
|
||||||
|
var stub
|
||||||
|
= K8sV1PvcStub.get(client, "vmop-dev", VM_NAME + "-runner-data");
|
||||||
|
for (int i = 0; i < 10; i++) {
|
||||||
|
if (stub.model().isPresent()) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
Thread.sleep(1000);
|
||||||
|
}
|
||||||
|
var pvc = stub.model().get();
|
||||||
|
checkProps(pvc.getMetadata(), Map.of(
|
||||||
|
List.of("labels", "app.kubernetes.io/name"), Constants.APP_NAME,
|
||||||
|
List.of("labels", "app.kubernetes.io/instance"), VM_NAME,
|
||||||
|
List.of("labels", "app.kubernetes.io/managed-by"),
|
||||||
|
Constants.VM_OP_NAME));
|
||||||
|
checkProps(pvc.getSpec(), Map.of(
|
||||||
|
List.of("resources", "requests", "storage"),
|
||||||
|
Quantity.fromString("1Mi")));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testSystemDiskPvc() throws ApiException, InterruptedException {
|
||||||
|
var stub
|
||||||
|
= K8sV1PvcStub.get(client, "vmop-dev", VM_NAME + "-system-disk");
|
||||||
|
for (int i = 0; i < 10; i++) {
|
||||||
|
if (stub.model().isPresent()) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
Thread.sleep(1000);
|
||||||
|
}
|
||||||
|
var pvc = stub.model().get();
|
||||||
|
checkProps(pvc.getMetadata(), Map.of(
|
||||||
|
List.of("labels", "app.kubernetes.io/name"), Constants.APP_NAME,
|
||||||
|
List.of("labels", "app.kubernetes.io/instance"), VM_NAME,
|
||||||
|
List.of("labels", "app.kubernetes.io/managed-by"),
|
||||||
|
Constants.VM_OP_NAME,
|
||||||
|
List.of("annotations", "use_as"), "system-disk"));
|
||||||
|
checkProps(pvc.getSpec(), Map.of(
|
||||||
|
List.of("resources", "requests", "storage"),
|
||||||
|
Quantity.fromString("1Gi")));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testDisk1Pvc() throws ApiException, InterruptedException {
|
||||||
|
var stub
|
||||||
|
= K8sV1PvcStub.get(client, "vmop-dev", VM_NAME + "-disk-1");
|
||||||
|
for (int i = 0; i < 10; i++) {
|
||||||
|
if (stub.model().isPresent()) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
Thread.sleep(1000);
|
||||||
|
}
|
||||||
|
var pvc = stub.model().get();
|
||||||
|
checkProps(pvc.getMetadata(), Map.of(
|
||||||
|
List.of("labels", "app.kubernetes.io/name"), Constants.APP_NAME,
|
||||||
|
List.of("labels", "app.kubernetes.io/instance"), VM_NAME,
|
||||||
|
List.of("labels", "app.kubernetes.io/managed-by"),
|
||||||
|
Constants.VM_OP_NAME));
|
||||||
|
checkProps(pvc.getSpec(), Map.of(
|
||||||
|
List.of("resources", "requests", "storage"),
|
||||||
|
Quantity.fromString("1Gi")));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testPod() throws ApiException, InterruptedException {
|
||||||
|
PatchOptions opts = new PatchOptions();
|
||||||
|
opts.setForce(true);
|
||||||
|
opts.setFieldManager("kubernetes-java-kubectl-apply");
|
||||||
|
assertTrue(vmStub.patch(V1Patch.PATCH_FORMAT_JSON_PATCH,
|
||||||
|
new V1Patch("[{\"op\": \"replace\", \"path\": \"/spec/vm/state"
|
||||||
|
+ "\", \"value\": \"Running\"}]"),
|
||||||
|
client.defaultPatchOptions()).isPresent());
|
||||||
|
var stub = K8sV1PodStub.get(client, "vmop-dev", VM_NAME);
|
||||||
|
for (int i = 0; i < 20; i++) {
|
||||||
|
if (stub.model().isPresent()) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
Thread.sleep(1000);
|
||||||
|
}
|
||||||
|
var pod = stub.model().get();
|
||||||
|
checkProps(pod.getMetadata(), Map.of(
|
||||||
|
List.of("labels", "app.kubernetes.io/name"), APP_NAME,
|
||||||
|
List.of("labels", "app.kubernetes.io/instance"), VM_NAME,
|
||||||
|
List.of("labels", "app.kubernetes.io/component"), APP_NAME,
|
||||||
|
List.of("labels", "app.kubernetes.io/managed-by"),
|
||||||
|
Constants.VM_OP_NAME,
|
||||||
|
List.of("annotations", "vmrunner.jdrupes.org/cmVersion"), EXISTS,
|
||||||
|
List.of("annotations", "vmoperator.jdrupes.org/version"), EXISTS,
|
||||||
|
List.of("ownerReferences", 0, "apiVersion"),
|
||||||
|
vmsContext.getGroup() + "/" + vmsContext.getVersions().get(0),
|
||||||
|
List.of("ownerReferences", 0, "kind"), Constants.VM_OP_KIND_VM,
|
||||||
|
List.of("ownerReferences", 0, "name"), VM_NAME,
|
||||||
|
List.of("ownerReferences", 0, "uid"), EXISTS));
|
||||||
|
checkProps(pod.getSpec(), Map.of(
|
||||||
|
List.of("containers", 0, "image"), EXISTS,
|
||||||
|
List.of("containers", 0, "name"), VM_NAME,
|
||||||
|
List.of("containers", 0, "resources", "requests", "cpu"),
|
||||||
|
Quantity.fromString("1")));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testLoadBalancer() throws ApiException, InterruptedException {
|
||||||
|
var stub = K8sV1ServiceStub.get(client, "vmop-dev", VM_NAME);
|
||||||
|
for (int i = 0; i < 10; i++) {
|
||||||
|
if (stub.model().isPresent()) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
Thread.sleep(1000);
|
||||||
|
}
|
||||||
|
var svc = stub.model().get();
|
||||||
|
checkProps(svc.getMetadata(), Map.of(
|
||||||
|
List.of("labels", "app.kubernetes.io/name"), APP_NAME,
|
||||||
|
List.of("labels", "app.kubernetes.io/instance"), VM_NAME,
|
||||||
|
List.of("labels", "app.kubernetes.io/managed-by"), VM_OP_NAME,
|
||||||
|
List.of("labels", "label1"), "label1",
|
||||||
|
List.of("labels", "label2"), "replaced",
|
||||||
|
List.of("labels", "label3"), "added",
|
||||||
|
List.of("annotations", "metallb.universe.tf/loadBalancerIPs"),
|
||||||
|
"192.168.168.1",
|
||||||
|
List.of("annotations", "anno1"), "added"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void checkProps(Object obj,
|
||||||
|
Map<? extends List<? extends Object>, Object> toCheck) {
|
||||||
|
for (var entry : toCheck.entrySet()) {
|
||||||
|
var prop = DataPath.get(obj, entry.getKey().toArray());
|
||||||
|
assertTrue(prop.isPresent(), () -> "Property " + entry.getKey()
|
||||||
|
+ " not found in " + obj);
|
||||||
|
|
||||||
|
// Check for existance only
|
||||||
|
if (entry.getValue() == EXISTS) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
assertEquals(entry.getValue(), prop.get());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,176 @@
|
||||||
|
/*
|
||||||
|
* VM-Operator
|
||||||
|
* Copyright (C) 2023 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.util;
|
||||||
|
|
||||||
|
import java.lang.reflect.InvocationTargetException;
|
||||||
|
import java.lang.reflect.Method;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.logging.Logger;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility class that supports navigation through arbitrary data structures.
|
||||||
|
*/
|
||||||
|
public final class DataPath {
|
||||||
|
|
||||||
|
private static final Logger logger
|
||||||
|
= Logger.getLogger(DataPath.class.getName());
|
||||||
|
|
||||||
|
private DataPath() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply the given selectors on the given object and return the
|
||||||
|
* value reached.
|
||||||
|
*
|
||||||
|
* Selectors can be if type {@link String} or {@link Number}. The
|
||||||
|
* former are used to access a property of an object, the latter to
|
||||||
|
* access an element in an array or a {@link List}.
|
||||||
|
*
|
||||||
|
* Depending on the object currently visited, a {@link String} can
|
||||||
|
* be the key of a {@link Map}, the property part of a getter method
|
||||||
|
* or the name of a method that has an empty parameter list.
|
||||||
|
*
|
||||||
|
* @param <T> the generic type
|
||||||
|
* @param from the from
|
||||||
|
* @param selectors the selectors
|
||||||
|
* @return the result
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("PMD.UseLocaleWithCaseConversions")
|
||||||
|
public static <T> Optional<T> get(Object from, Object... selectors) {
|
||||||
|
Object cur = from;
|
||||||
|
for (var selector : selectors) {
|
||||||
|
if (cur == null) {
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
if (selector instanceof String && cur instanceof Map map) {
|
||||||
|
cur = map.get(selector);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (selector instanceof Number index && cur instanceof List list) {
|
||||||
|
cur = list.get(index.intValue());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (selector instanceof String property) {
|
||||||
|
var retrieved = tryAccess(cur, property);
|
||||||
|
if (retrieved.isEmpty()) {
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
cur = retrieved.get();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
var result = Optional.ofNullable((T) cur);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("PMD.UseLocaleWithCaseConversions")
|
||||||
|
private static Optional<Object> tryAccess(Object obj, String property) {
|
||||||
|
Method acc = null;
|
||||||
|
try {
|
||||||
|
// Try getter
|
||||||
|
acc = obj.getClass().getMethod("get" + property.substring(0, 1)
|
||||||
|
.toUpperCase() + property.substring(1));
|
||||||
|
} catch (SecurityException e) {
|
||||||
|
return Optional.empty();
|
||||||
|
} catch (NoSuchMethodException e) { // NOPMD
|
||||||
|
// Can happen...
|
||||||
|
}
|
||||||
|
if (acc == null) {
|
||||||
|
try {
|
||||||
|
// Try method
|
||||||
|
acc = obj.getClass().getMethod(property);
|
||||||
|
} catch (SecurityException | NoSuchMethodException e) {
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (acc != null) {
|
||||||
|
try {
|
||||||
|
return Optional.ofNullable(acc.invoke(obj));
|
||||||
|
} catch (IllegalAccessException
|
||||||
|
| InvocationTargetException e) {
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attempts to make a as-deep-as-possible copy of the given
|
||||||
|
* container. New containers will be created for Maps, Lists and
|
||||||
|
* Arrays. The method is invoked recursively for the entries/items.
|
||||||
|
*
|
||||||
|
* If invoked with an object that is neither a map, list or array,
|
||||||
|
* the methods checks if the object implements {@link Cloneable}
|
||||||
|
* and if it does, invokes its {@link Object#clone()} method.
|
||||||
|
* Else the method return the object.
|
||||||
|
*
|
||||||
|
* @param <T> the generic type
|
||||||
|
* @param object the container
|
||||||
|
* @return the t
|
||||||
|
*/
|
||||||
|
@SuppressWarnings({ "PMD.CognitiveComplexity", "unchecked" })
|
||||||
|
public static <T> T deepCopy(T object) {
|
||||||
|
if (object instanceof Map map) {
|
||||||
|
@SuppressWarnings("PMD.UseConcurrentHashMap")
|
||||||
|
Map<Object, Object> copy;
|
||||||
|
try {
|
||||||
|
copy = (Map<Object, Object>) object.getClass().getConstructor()
|
||||||
|
.newInstance();
|
||||||
|
} catch (InstantiationException | IllegalAccessException
|
||||||
|
| IllegalArgumentException | InvocationTargetException
|
||||||
|
| NoSuchMethodException | SecurityException e) {
|
||||||
|
logger.severe(
|
||||||
|
() -> "Cannot create new instance of " + object.getClass());
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
for (var entry : ((Map<?, ?>) map).entrySet()) {
|
||||||
|
copy.put(entry.getKey(),
|
||||||
|
deepCopy(entry.getValue()));
|
||||||
|
}
|
||||||
|
return (T) copy;
|
||||||
|
}
|
||||||
|
if (object instanceof List list) {
|
||||||
|
List<Object> copy = new ArrayList<>();
|
||||||
|
for (var item : list) {
|
||||||
|
copy.add(deepCopy(item));
|
||||||
|
}
|
||||||
|
return (T) copy;
|
||||||
|
}
|
||||||
|
if (object.getClass().isArray()) {
|
||||||
|
var copy = new ArrayList<>();
|
||||||
|
for (var item : (Object[]) object) {
|
||||||
|
copy.add(deepCopy(item));
|
||||||
|
}
|
||||||
|
return (T) copy.toArray();
|
||||||
|
}
|
||||||
|
if (object instanceof Cloneable) {
|
||||||
|
try {
|
||||||
|
return (T) object.getClass().getMethod("clone")
|
||||||
|
.invoke(object);
|
||||||
|
} catch (IllegalAccessException | InvocationTargetException
|
||||||
|
| NoSuchMethodException | SecurityException e) {
|
||||||
|
return object;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return object;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -5,7 +5,7 @@ plugins {
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation project(':org.jdrupes.vmoperator.manager.events')
|
implementation project(':org.jdrupes.vmoperator.manager.events')
|
||||||
|
|
||||||
implementation 'org.jgrapes:org.jgrapes.webconsole.base:[2.0.0.3)'
|
implementation 'org.jgrapes:org.jgrapes.webconsole.base:[2.1.0,3)'
|
||||||
implementation 'org.jgrapes:org.jgrapes.webconsole.provider.vue:[1,2)'
|
implementation 'org.jgrapes:org.jgrapes.webconsole.provider.vue:[1,2)'
|
||||||
implementation 'org.jgrapes:org.jgrapes.webconsole.provider.jgwcvuecomponents:[1.2,2)'
|
implementation 'org.jgrapes:org.jgrapes.webconsole.provider.jgwcvuecomponents:[1.2,2)'
|
||||||
implementation 'org.jgrapes:org.jgrapes.webconsole.provider.chartjs:[1.2,2)'
|
implementation 'org.jgrapes:org.jgrapes.webconsole.provider.chartjs:[1.2,2)'
|
||||||
|
|
|
||||||
|
|
@ -18,8 +18,6 @@
|
||||||
|
|
||||||
package org.jdrupes.vmoperator.vmconlet;
|
package org.jdrupes.vmoperator.vmconlet;
|
||||||
|
|
||||||
import com.google.gson.Gson;
|
|
||||||
import com.google.gson.JsonObject;
|
|
||||||
import freemarker.core.ParseException;
|
import freemarker.core.ParseException;
|
||||||
import freemarker.template.MalformedTemplateNameException;
|
import freemarker.template.MalformedTemplateNameException;
|
||||||
import freemarker.template.Template;
|
import freemarker.template.Template;
|
||||||
|
|
@ -31,16 +29,19 @@ import java.math.BigDecimal;
|
||||||
import java.math.BigInteger;
|
import java.math.BigInteger;
|
||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
|
import java.util.Collections;
|
||||||
import java.util.EnumSet;
|
import java.util.EnumSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import org.jdrupes.vmoperator.common.K8sObserver;
|
import org.jdrupes.vmoperator.common.K8sObserver;
|
||||||
import org.jdrupes.vmoperator.common.VmDefinitionModel;
|
import org.jdrupes.vmoperator.common.VmDefinition;
|
||||||
import org.jdrupes.vmoperator.manager.events.ChannelTracker;
|
import org.jdrupes.vmoperator.manager.events.ChannelTracker;
|
||||||
import org.jdrupes.vmoperator.manager.events.ModifyVm;
|
import org.jdrupes.vmoperator.manager.events.ModifyVm;
|
||||||
import org.jdrupes.vmoperator.manager.events.VmChannel;
|
import org.jdrupes.vmoperator.manager.events.VmChannel;
|
||||||
import org.jdrupes.vmoperator.manager.events.VmDefChanged;
|
import org.jdrupes.vmoperator.manager.events.VmDefChanged;
|
||||||
import org.jdrupes.vmoperator.util.GsonPtr;
|
import org.jdrupes.vmoperator.util.DataPath;
|
||||||
import org.jgrapes.core.Channel;
|
import org.jgrapes.core.Channel;
|
||||||
import org.jgrapes.core.Event;
|
import org.jgrapes.core.Event;
|
||||||
import org.jgrapes.core.Manager;
|
import org.jgrapes.core.Manager;
|
||||||
|
|
@ -62,13 +63,14 @@ import org.jgrapes.webconsole.base.freemarker.FreeMarkerConlet;
|
||||||
/**
|
/**
|
||||||
* The Class VmConlet.
|
* The Class VmConlet.
|
||||||
*/
|
*/
|
||||||
@SuppressWarnings("PMD.DataflowAnomalyAnalysis")
|
@SuppressWarnings({ "PMD.DataflowAnomalyAnalysis",
|
||||||
|
"PMD.CouplingBetweenObjects" })
|
||||||
public class VmConlet extends FreeMarkerConlet<VmConlet.VmsModel> {
|
public class VmConlet extends FreeMarkerConlet<VmConlet.VmsModel> {
|
||||||
|
|
||||||
private static final Set<RenderMode> MODES = RenderMode.asSet(
|
private static final Set<RenderMode> MODES = RenderMode.asSet(
|
||||||
RenderMode.Preview, RenderMode.View);
|
RenderMode.Preview, RenderMode.View);
|
||||||
private final ChannelTracker<String, VmChannel,
|
private final ChannelTracker<String, VmChannel,
|
||||||
VmDefinitionModel> channelTracker = new ChannelTracker<>();
|
VmDefinition> channelTracker = new ChannelTracker<>();
|
||||||
private final TimeSeries summarySeries = new TimeSeries(Duration.ofDays(1));
|
private final TimeSeries summarySeries = new TimeSeries(Duration.ofDays(1));
|
||||||
private Summary cachedSummary;
|
private Summary cachedSummary;
|
||||||
|
|
||||||
|
|
@ -160,22 +162,45 @@ public class VmConlet extends FreeMarkerConlet<VmConlet.VmsModel> {
|
||||||
}
|
}
|
||||||
if (sendVmInfos) {
|
if (sendVmInfos) {
|
||||||
for (var item : channelTracker.values()) {
|
for (var item : channelTracker.values()) {
|
||||||
Gson gson = item.channel().client().getJSON().getGson();
|
|
||||||
var def = gson.fromJson(item.associated().data(), Object.class);
|
|
||||||
channel.respond(new NotifyConletView(type(),
|
channel.respond(new NotifyConletView(type(),
|
||||||
conletId, "updateVm", def));
|
conletId, "updateVm",
|
||||||
|
simplifiedVmDefinition(item.associated())));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return renderedAs;
|
return renderedAs;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("PMD.AvoidDuplicateLiterals")
|
||||||
|
private Map<String, Object> simplifiedVmDefinition(VmDefinition vmDef) {
|
||||||
|
// Convert RAM sizes to unitless numbers
|
||||||
|
var spec = DataPath.deepCopy(vmDef.spec());
|
||||||
|
var vmSpec = DataPath.<Map<String, Object>> get(spec, "vm").get();
|
||||||
|
vmSpec.put("maximumRam", Quantity.fromString(
|
||||||
|
DataPath.<String> get(vmSpec, "maximumRam").orElse("0")).getNumber()
|
||||||
|
.toBigInteger());
|
||||||
|
vmSpec.put("currentRam", Quantity.fromString(
|
||||||
|
DataPath.<String> get(vmSpec, "currentRam").orElse("0")).getNumber()
|
||||||
|
.toBigInteger());
|
||||||
|
var status = DataPath.deepCopy(vmDef.status());
|
||||||
|
status.put("ram", Quantity.fromString(
|
||||||
|
DataPath.<String> get(status, "ram").orElse("0")).getNumber()
|
||||||
|
.toBigInteger());
|
||||||
|
|
||||||
|
// Build result
|
||||||
|
return Map.of("metadata",
|
||||||
|
Map.of("namespace", vmDef.namespace(),
|
||||||
|
"name", vmDef.name()),
|
||||||
|
"spec", spec,
|
||||||
|
"status", status,
|
||||||
|
"nodeName", vmDef.extra("nodeName"));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Track the VM definitions.
|
* Track the VM definitions.
|
||||||
*
|
*
|
||||||
* @param event the event
|
* @param event the event
|
||||||
* @param channel the channel
|
* @param channel the channel
|
||||||
* @throws JsonDecodeException the json decode exception
|
|
||||||
* @throws IOException
|
* @throws IOException
|
||||||
*/
|
*/
|
||||||
@Handler(namedChannels = "manager")
|
@Handler(namedChannels = "manager")
|
||||||
|
|
@ -184,7 +209,7 @@ public class VmConlet extends FreeMarkerConlet<VmConlet.VmsModel> {
|
||||||
"PMD.ConfusingArgumentToVarargsMethod" })
|
"PMD.ConfusingArgumentToVarargsMethod" })
|
||||||
public void onVmDefChanged(VmDefChanged event, VmChannel channel)
|
public void onVmDefChanged(VmDefChanged event, VmChannel channel)
|
||||||
throws IOException {
|
throws IOException {
|
||||||
var vmName = event.vmDefinition().getMetadata().getName();
|
var vmName = event.vmDefinition().name();
|
||||||
if (event.type() == K8sObserver.ResponseType.DELETED) {
|
if (event.type() == K8sObserver.ResponseType.DELETED) {
|
||||||
channelTracker.remove(vmName);
|
channelTracker.remove(vmName);
|
||||||
for (var entry : conletIdsByConsoleConnection().entrySet()) {
|
for (var entry : conletIdsByConsoleConnection().entrySet()) {
|
||||||
|
|
@ -194,15 +219,12 @@ public class VmConlet extends FreeMarkerConlet<VmConlet.VmsModel> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
var gson = channel.client().getJSON().getGson();
|
var vmDef = event.vmDefinition();
|
||||||
var vmDef = new VmDefinitionModel(gson,
|
|
||||||
cleanup(event.vmDefinition().data()));
|
|
||||||
channelTracker.put(vmName, channel, vmDef);
|
channelTracker.put(vmName, channel, vmDef);
|
||||||
var def = gson.fromJson(vmDef.data(), Object.class);
|
|
||||||
for (var entry : conletIdsByConsoleConnection().entrySet()) {
|
for (var entry : conletIdsByConsoleConnection().entrySet()) {
|
||||||
for (String conletId : entry.getValue()) {
|
for (String conletId : entry.getValue()) {
|
||||||
entry.getKey().respond(new NotifyConletView(type(),
|
entry.getKey().respond(new NotifyConletView(type(),
|
||||||
conletId, "updateVm", def));
|
conletId, "updateVm", simplifiedVmDefinition(vmDef)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -217,28 +239,6 @@ public class VmConlet extends FreeMarkerConlet<VmConlet.VmsModel> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressWarnings("PMD.AvoidDuplicateLiterals")
|
|
||||||
private JsonObject cleanup(JsonObject vmDef) {
|
|
||||||
// Clone and remove managed fields
|
|
||||||
var json = vmDef.deepCopy();
|
|
||||||
GsonPtr.to(json).to("metadata").get(JsonObject.class)
|
|
||||||
.remove("managedFields");
|
|
||||||
|
|
||||||
// Convert RAM sizes to unitless numbers
|
|
||||||
var vmSpec = GsonPtr.to(json).to("spec", "vm");
|
|
||||||
vmSpec.set("maximumRam", Quantity.fromString(
|
|
||||||
vmSpec.getAsString("maximumRam").orElse("0")).getNumber()
|
|
||||||
.toBigInteger());
|
|
||||||
vmSpec.set("currentRam", Quantity.fromString(
|
|
||||||
vmSpec.getAsString("currentRam").orElse("0")).getNumber()
|
|
||||||
.toBigInteger());
|
|
||||||
var status = GsonPtr.to(json).to("status");
|
|
||||||
status.set("ram", Quantity.fromString(
|
|
||||||
status.getAsString("ram").orElse("0")).getNumber()
|
|
||||||
.toBigInteger());
|
|
||||||
return json;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handle the periodic update event by sending {@link NotifyConletView}
|
* Handle the periodic update event by sending {@link NotifyConletView}
|
||||||
* events.
|
* events.
|
||||||
|
|
@ -267,10 +267,10 @@ public class VmConlet extends FreeMarkerConlet<VmConlet.VmsModel> {
|
||||||
public int totalVms;
|
public int totalVms;
|
||||||
|
|
||||||
/** The running vms. */
|
/** The running vms. */
|
||||||
public int runningVms;
|
public long runningVms;
|
||||||
|
|
||||||
/** The used cpus. */
|
/** The used cpus. */
|
||||||
public int usedCpus;
|
public long usedCpus;
|
||||||
|
|
||||||
/** The used ram. */
|
/** The used ram. */
|
||||||
public BigInteger usedRam = BigInteger.ZERO;
|
public BigInteger usedRam = BigInteger.ZERO;
|
||||||
|
|
@ -289,7 +289,7 @@ public class VmConlet extends FreeMarkerConlet<VmConlet.VmsModel> {
|
||||||
*
|
*
|
||||||
* @return the runningVms
|
* @return the runningVms
|
||||||
*/
|
*/
|
||||||
public int getRunningVms() {
|
public long getRunningVms() {
|
||||||
return runningVms;
|
return runningVms;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -298,7 +298,7 @@ public class VmConlet extends FreeMarkerConlet<VmConlet.VmsModel> {
|
||||||
*
|
*
|
||||||
* @return the usedCpus
|
* @return the usedCpus
|
||||||
*/
|
*/
|
||||||
public int getUsedCpus() {
|
public long getUsedCpus() {
|
||||||
return usedCpus;
|
return usedCpus;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -313,7 +313,8 @@ public class VmConlet extends FreeMarkerConlet<VmConlet.VmsModel> {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressWarnings("PMD.AvoidLiteralsInIfCondition")
|
@SuppressWarnings({ "PMD.AvoidLiteralsInIfCondition",
|
||||||
|
"PMD.LambdaCanBeMethodReference" })
|
||||||
private Summary evaluateSummary(boolean force) {
|
private Summary evaluateSummary(boolean force) {
|
||||||
if (!force && cachedSummary != null) {
|
if (!force && cachedSummary != null) {
|
||||||
return cachedSummary;
|
return cachedSummary;
|
||||||
|
|
@ -321,18 +322,20 @@ public class VmConlet extends FreeMarkerConlet<VmConlet.VmsModel> {
|
||||||
Summary summary = new Summary();
|
Summary summary = new Summary();
|
||||||
for (var vmDef : channelTracker.associated()) {
|
for (var vmDef : channelTracker.associated()) {
|
||||||
summary.totalVms += 1;
|
summary.totalVms += 1;
|
||||||
var status = GsonPtr.to(vmDef.data()).to("status");
|
summary.usedCpus += vmDef.<Number> fromStatus("cpus")
|
||||||
summary.usedCpus += status.getAsInt("cpus").orElse(0);
|
.map(Number::intValue).orElse(0);
|
||||||
summary.usedRam = summary.usedRam.add(status.getAsString("ram")
|
summary.usedRam = summary.usedRam
|
||||||
.map(BigInteger::new).orElse(BigInteger.ZERO));
|
.add(vmDef.<String> fromStatus("ram")
|
||||||
for (var c : status.getAsListOf(JsonObject.class, "conditions")) {
|
.map(r -> Quantity.fromString(r).getNumber().toBigInteger())
|
||||||
if ("Running".equals(GsonPtr.to(c).getAsString("type")
|
.orElse(BigInteger.ZERO));
|
||||||
.orElse(null))
|
summary.runningVms
|
||||||
&& "True".equals(GsonPtr.to(c).getAsString("status")
|
= vmDef.<List<Map<String, Object>>> fromStatus("conditions")
|
||||||
.orElse(null))) {
|
.orElse(Collections.emptyList()).stream()
|
||||||
summary.runningVms += 1;
|
.filter(cond -> DataPath.get(cond, "type")
|
||||||
}
|
.map(t -> "Running".equals(t)).orElse(false)
|
||||||
}
|
&& DataPath.get(cond, "status")
|
||||||
|
.map(s -> "True".equals(s)).orElse(false))
|
||||||
|
.count();
|
||||||
}
|
}
|
||||||
cachedSummary = summary;
|
cachedSummary = summary;
|
||||||
return summary;
|
return summary;
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ plugins {
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation project(':org.jdrupes.vmoperator.manager.events')
|
implementation project(':org.jdrupes.vmoperator.manager.events')
|
||||||
|
|
||||||
implementation 'org.jgrapes:org.jgrapes.webconsole.base:[2.0.0,3)'
|
implementation 'org.jgrapes:org.jgrapes.webconsole.base:[2.1.0,3)'
|
||||||
implementation 'org.jgrapes:org.jgrapes.webconsole.provider.vue:[1,2)'
|
implementation 'org.jgrapes:org.jgrapes.webconsole.provider.vue:[1,2)'
|
||||||
implementation 'org.jgrapes:org.jgrapes.webconsole.provider.jgwcvuecomponents:[1.2,2)'
|
implementation 'org.jgrapes:org.jgrapes.webconsole.provider.jgwcvuecomponents:[1.2,2)'
|
||||||
implementation 'org.jgrapes:org.jgrapes.webconsole.provider.chartjs:[1.2,2)'
|
implementation 'org.jgrapes:org.jgrapes.webconsole.provider.chartjs:[1.2,2)'
|
||||||
|
|
|
||||||
|
|
@ -23,8 +23,6 @@ import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||||
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.JsonObject;
|
|
||||||
import com.google.gson.JsonPrimitive;
|
|
||||||
import com.google.gson.JsonSyntaxException;
|
import com.google.gson.JsonSyntaxException;
|
||||||
import freemarker.core.ParseException;
|
import freemarker.core.ParseException;
|
||||||
import freemarker.template.MalformedTemplateNameException;
|
import freemarker.template.MalformedTemplateNameException;
|
||||||
|
|
@ -48,17 +46,16 @@ import java.util.ResourceBundle;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.logging.Level;
|
import java.util.logging.Level;
|
||||||
import org.bouncycastle.util.Objects;
|
import org.bouncycastle.util.Objects;
|
||||||
import org.jdrupes.vmoperator.common.K8sDynamicModel;
|
|
||||||
import org.jdrupes.vmoperator.common.K8sObserver;
|
import org.jdrupes.vmoperator.common.K8sObserver;
|
||||||
import org.jdrupes.vmoperator.common.VmDefinitionModel;
|
import org.jdrupes.vmoperator.common.VmDefinition;
|
||||||
import org.jdrupes.vmoperator.common.VmDefinitionModel.Permission;
|
import org.jdrupes.vmoperator.common.VmDefinition.Permission;
|
||||||
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;
|
||||||
import org.jdrupes.vmoperator.manager.events.ResetVm;
|
import org.jdrupes.vmoperator.manager.events.ResetVm;
|
||||||
import org.jdrupes.vmoperator.manager.events.VmChannel;
|
import org.jdrupes.vmoperator.manager.events.VmChannel;
|
||||||
import org.jdrupes.vmoperator.manager.events.VmDefChanged;
|
import org.jdrupes.vmoperator.manager.events.VmDefChanged;
|
||||||
import org.jdrupes.vmoperator.util.GsonPtr;
|
import org.jdrupes.vmoperator.util.DataPath;
|
||||||
import org.jgrapes.core.Channel;
|
import org.jgrapes.core.Channel;
|
||||||
import org.jgrapes.core.Components;
|
import org.jgrapes.core.Components;
|
||||||
import org.jgrapes.core.Event;
|
import org.jgrapes.core.Event;
|
||||||
|
|
@ -122,7 +119,7 @@ public class VmViewer extends FreeMarkerConlet<VmViewer.ViewerModel> {
|
||||||
private static final Set<RenderMode> MODES_FOR_GENERATED = RenderMode.asSet(
|
private static final Set<RenderMode> MODES_FOR_GENERATED = RenderMode.asSet(
|
||||||
RenderMode.Preview, RenderMode.StickyPreview);
|
RenderMode.Preview, RenderMode.StickyPreview);
|
||||||
private final ChannelTracker<String, VmChannel,
|
private final ChannelTracker<String, VmChannel,
|
||||||
VmDefinitionModel> channelTracker = new ChannelTracker<>();
|
VmDefinition> channelTracker = new ChannelTracker<>();
|
||||||
private static ObjectMapper objectMapper
|
private static ObjectMapper objectMapper
|
||||||
= new ObjectMapper().registerModule(new JavaTimeModule());
|
= new ObjectMapper().registerModule(new JavaTimeModule());
|
||||||
private Class<?> preferredIpVersion = Inet4Address.class;
|
private Class<?> preferredIpVersion = Inet4Address.class;
|
||||||
|
|
@ -399,8 +396,7 @@ public class VmViewer extends FreeMarkerConlet<VmViewer.ViewerModel> {
|
||||||
.map(d -> d.getMetadata().getName()).sorted().toList();
|
.map(d -> d.getMetadata().getName()).sorted().toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
private Set<Permission> permissions(VmDefinitionModel vmDef,
|
private Set<Permission> permissions(VmDefinition vmDef, Session session) {
|
||||||
Session session) {
|
|
||||||
var user = WebConsoleUtils.userFromSession(session)
|
var user = WebConsoleUtils.userFromSession(session)
|
||||||
.map(ConsoleUser::getName).orElse(null);
|
.map(ConsoleUser::getName).orElse(null);
|
||||||
var roles = WebConsoleUtils.rolesFromSession(session)
|
var roles = WebConsoleUtils.rolesFromSession(session)
|
||||||
|
|
@ -421,15 +417,16 @@ public class VmViewer extends FreeMarkerConlet<VmViewer.ViewerModel> {
|
||||||
channelTracker.value(model.vmName()).ifPresent(item -> {
|
channelTracker.value(model.vmName()).ifPresent(item -> {
|
||||||
try {
|
try {
|
||||||
var vmDef = item.associated();
|
var vmDef = item.associated();
|
||||||
@SuppressWarnings("unchecked")
|
var data = Map.of("metadata",
|
||||||
var def = (Map<String, Object>) item.channel().client()
|
Map.of("namespace", vmDef.namespace(),
|
||||||
.getJSON().getGson()
|
"name", vmDef.name()),
|
||||||
.fromJson(vmDef.data().toString(), Map.class);
|
"spec", vmDef.spec(),
|
||||||
def.put("userPermissions",
|
"status", vmDef.getStatus(),
|
||||||
|
"userPermissions",
|
||||||
permissions(vmDef, channel.session()).stream()
|
permissions(vmDef, channel.session()).stream()
|
||||||
.map(Permission::toString).toList());
|
.map(Permission::toString).toList());
|
||||||
channel.respond(new NotifyConletView(type(),
|
channel.respond(new NotifyConletView(type(),
|
||||||
model.getConletId(), "updateVmDefinition", def));
|
model.getConletId(), "updateVmDefinition", data));
|
||||||
} 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");
|
||||||
|
|
@ -452,7 +449,6 @@ public class VmViewer extends FreeMarkerConlet<VmViewer.ViewerModel> {
|
||||||
*
|
*
|
||||||
* @param event the event
|
* @param event the event
|
||||||
* @param channel the channel
|
* @param channel the channel
|
||||||
* @throws JsonDecodeException the json decode exception
|
|
||||||
* @throws IOException
|
* @throws IOException
|
||||||
*/
|
*/
|
||||||
@Handler(namedChannels = "manager")
|
@Handler(namedChannels = "manager")
|
||||||
|
|
@ -461,11 +457,8 @@ public class VmViewer extends FreeMarkerConlet<VmViewer.ViewerModel> {
|
||||||
"PMD.ConfusingArgumentToVarargsMethod" })
|
"PMD.ConfusingArgumentToVarargsMethod" })
|
||||||
public void onVmDefChanged(VmDefChanged event, VmChannel channel)
|
public void onVmDefChanged(VmDefChanged event, VmChannel channel)
|
||||||
throws IOException {
|
throws IOException {
|
||||||
var vmDef = new VmDefinitionModel(channel.client().getJSON()
|
var vmDef = event.vmDefinition();
|
||||||
.getGson(), event.vmDefinition().data());
|
var vmName = vmDef.name();
|
||||||
GsonPtr.to(vmDef.data()).to("metadata").get(JsonObject.class)
|
|
||||||
.remove("managedFields");
|
|
||||||
var vmName = vmDef.getMetadata().getName();
|
|
||||||
if (event.type() == K8sObserver.ResponseType.DELETED) {
|
if (event.type() == K8sObserver.ResponseType.DELETED) {
|
||||||
channelTracker.remove(vmName);
|
channelTracker.remove(vmName);
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -567,23 +560,22 @@ public class VmViewer extends FreeMarkerConlet<VmViewer.ViewerModel> {
|
||||||
logger.severe(() -> "Failed to find display IP for " + vmName);
|
logger.severe(() -> "Failed to find display IP for " + vmName);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
var port = GsonPtr.to(vmDef.data()).get(JsonPrimitive.class, "spec",
|
var port = vmDef.<Number> fromVm("display", "spice", "port")
|
||||||
"vm", "display", "spice", "port");
|
.map(Number::longValue);
|
||||||
if (port.isEmpty()) {
|
if (port.isEmpty()) {
|
||||||
logger.severe(() -> "No port defined for display of " + vmName);
|
logger.severe(() -> "No port defined for display of " + vmName);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
var proxyUrl = GsonPtr.to(vmDef.data()).get(JsonPrimitive.class, "spec",
|
|
||||||
"vm", "display", "spice", "proxyUrl");
|
|
||||||
StringBuffer data = new StringBuffer(100)
|
StringBuffer data = new StringBuffer(100)
|
||||||
.append("[virt-viewer]\ntype=spice\nhost=")
|
.append("[virt-viewer]\ntype=spice\nhost=")
|
||||||
.append(addr.get().getHostAddress()).append("\nport=")
|
.append(addr.get().getHostAddress()).append("\nport=")
|
||||||
.append(Integer.toString(port.get().getAsInt()))
|
.append(port.get().toString())
|
||||||
.append('\n');
|
.append('\n');
|
||||||
if (password != null) {
|
if (password != null) {
|
||||||
data.append("password=").append(password).append('\n');
|
data.append("password=").append(password).append('\n');
|
||||||
}
|
}
|
||||||
proxyUrl.map(JsonPrimitive::getAsString).ifPresent(u -> {
|
vmDef.<String> fromVm("display", "spice", "proxyUrl")
|
||||||
|
.ifPresent(u -> {
|
||||||
if (!Strings.isNullOrEmpty(u)) {
|
if (!Strings.isNullOrEmpty(u)) {
|
||||||
data.append("proxy=").append(u).append('\n');
|
data.append("proxy=").append(u).append('\n');
|
||||||
}
|
}
|
||||||
|
|
@ -596,11 +588,10 @@ public class VmViewer extends FreeMarkerConlet<VmViewer.ViewerModel> {
|
||||||
Base64.getEncoder().encodeToString(data.toString().getBytes())));
|
Base64.getEncoder().encodeToString(data.toString().getBytes())));
|
||||||
}
|
}
|
||||||
|
|
||||||
private Optional<InetAddress> displayIp(K8sDynamicModel vmDef) {
|
private Optional<InetAddress> displayIp(VmDefinition vmDef) {
|
||||||
var server = GsonPtr.to(vmDef.data()).get(JsonPrimitive.class, "spec",
|
Optional<String> server = vmDef.fromVm("display", "spice", "server");
|
||||||
"vm", "display", "spice", "server");
|
|
||||||
if (server.isPresent()) {
|
if (server.isPresent()) {
|
||||||
var srv = server.get().getAsString();
|
var srv = server.get();
|
||||||
try {
|
try {
|
||||||
var addr = InetAddress.getByName(srv);
|
var addr = InetAddress.getByName(srv);
|
||||||
logger.fine(() -> "Using IP address from CRD for "
|
logger.fine(() -> "Using IP address from CRD for "
|
||||||
|
|
@ -612,8 +603,8 @@ public class VmViewer extends FreeMarkerConlet<VmViewer.ViewerModel> {
|
||||||
return Optional.empty();
|
return Optional.empty();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
var addrs = GsonPtr.to(vmDef.data()).getAsListOf(JsonPrimitive.class,
|
var addrs = Optional.<List<String>> ofNullable(vmDef
|
||||||
"nodeAddresses").stream().map(JsonPrimitive::getAsString)
|
.extra("nodeAddresses")).orElse(Collections.emptyList()).stream()
|
||||||
.map(a -> {
|
.map(a -> {
|
||||||
try {
|
try {
|
||||||
return InetAddress.getByName(a);
|
return InetAddress.getByName(a);
|
||||||
|
|
@ -623,7 +614,7 @@ public class VmViewer extends FreeMarkerConlet<VmViewer.ViewerModel> {
|
||||||
}
|
}
|
||||||
}).filter(a -> a != null).toList();
|
}).filter(a -> a != null).toList();
|
||||||
logger.fine(() -> "Known IP addresses for "
|
logger.fine(() -> "Known IP addresses for "
|
||||||
+ vmDef.getMetadata().getName() + ": " + addrs);
|
+ vmDef.name() + ": " + addrs);
|
||||||
return addrs.stream()
|
return addrs.stream()
|
||||||
.filter(a -> preferredIpVersion.isAssignableFrom(a.getClass()))
|
.filter(a -> preferredIpVersion.isAssignableFrom(a.getClass()))
|
||||||
.findFirst().or(() -> addrs.stream().findFirst());
|
.findFirst().or(() -> addrs.stream().findFirst());
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue