Compare commits
4 commits
main
...
wip/test-b
| Author | SHA1 | Date | |
|---|---|---|---|
| c205971f6f | |||
| 86d3c75779 | |||
| c69a658b72 | |||
| 452e0604ca |
7 changed files with 534 additions and 102 deletions
|
|
@ -20,7 +20,7 @@ spec:
|
||||||
containers:
|
containers:
|
||||||
- name: vm-operator
|
- name: vm-operator
|
||||||
image: >-
|
image: >-
|
||||||
ghcr.io/mnlipp/org.jdrupes.vmoperator.manager:latest
|
ghcr.io/mnlipp/org.jdrupes.vmoperator.manager:3.4.1
|
||||||
volumeMounts:
|
volumeMounts:
|
||||||
- name: config
|
- name: config
|
||||||
mountPath: /etc/opt/vmoperator
|
mountPath: /etc/opt/vmoperator
|
||||||
|
|
|
||||||
|
|
@ -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":
|
||||||
|
|
@ -17,12 +25,12 @@
|
||||||
"/WebConsole":
|
"/WebConsole":
|
||||||
"/LoginConlet":
|
"/LoginConlet":
|
||||||
users:
|
users:
|
||||||
- name: admin
|
- name: admin
|
||||||
fullName: Administrator
|
fullName: Administrator
|
||||||
password: "$2b$05$NiBd74ZGdplLC63ePZf1f.UtjMKkbQ23cQoO2OKOFalDBHWAOy21."
|
password: "$2b$05$NiBd74ZGdplLC63ePZf1f.UtjMKkbQ23cQoO2OKOFalDBHWAOy21."
|
||||||
- name: test
|
- name: test
|
||||||
fullName: Test Account
|
fullName: Test Account
|
||||||
password: "$2b$05$hZaI/jToXf/d3BctZdT38Or7H7h6Pn2W3WiB49p5AyhDHFkkYCvo2"
|
password: "$2b$05$hZaI/jToXf/d3BctZdT38Or7H7h6Pn2W3WiB49p5AyhDHFkkYCvo2"
|
||||||
"/RoleConfigurator":
|
"/RoleConfigurator":
|
||||||
rolesByUser:
|
rolesByUser:
|
||||||
# User admin has role admin
|
# User admin has role admin
|
||||||
|
|
|
||||||
|
|
@ -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,12 +51,12 @@ 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":
|
||||||
rolesByUser:
|
rolesByUser:
|
||||||
# User admin has role admin
|
# User admin has role admin
|
||||||
|
|
|
||||||
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue