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:
|
||||
- name: vm-operator
|
||||
image: >-
|
||||
ghcr.io/mnlipp/org.jdrupes.vmoperator.manager:latest
|
||||
ghcr.io/mnlipp/org.jdrupes.vmoperator.manager:3.4.1
|
||||
volumeMounts:
|
||||
- name: config
|
||||
mountPath: /etc/opt/vmoperator
|
||||
|
|
|
|||
|
|
@ -9,6 +9,14 @@
|
|||
"/Reconciler":
|
||||
runnerData:
|
||||
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":
|
||||
port: 8888
|
||||
"/GuiHttpServer":
|
||||
|
|
@ -17,12 +25,12 @@
|
|||
"/WebConsole":
|
||||
"/LoginConlet":
|
||||
users:
|
||||
- name: admin
|
||||
fullName: Administrator
|
||||
password: "$2b$05$NiBd74ZGdplLC63ePZf1f.UtjMKkbQ23cQoO2OKOFalDBHWAOy21."
|
||||
- name: test
|
||||
fullName: Test Account
|
||||
password: "$2b$05$hZaI/jToXf/d3BctZdT38Or7H7h6Pn2W3WiB49p5AyhDHFkkYCvo2"
|
||||
- name: admin
|
||||
fullName: Administrator
|
||||
password: "$2b$05$NiBd74ZGdplLC63ePZf1f.UtjMKkbQ23cQoO2OKOFalDBHWAOy21."
|
||||
- name: test
|
||||
fullName: Test Account
|
||||
password: "$2b$05$hZaI/jToXf/d3BctZdT38Or7H7h6Pn2W3WiB49p5AyhDHFkkYCvo2"
|
||||
"/RoleConfigurator":
|
||||
rolesByUser:
|
||||
# User admin has role admin
|
||||
|
|
|
|||
|
|
@ -35,6 +35,14 @@ patches:
|
|||
"/Reconciler":
|
||||
runnerData:
|
||||
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":
|
||||
port: 8888
|
||||
"/GuiHttpServer":
|
||||
|
|
@ -43,12 +51,12 @@ patches:
|
|||
"/WebConsole":
|
||||
"/LoginConlet":
|
||||
users:
|
||||
admin:
|
||||
fullName: Administrator
|
||||
password: "$2b$05$NiBd74ZGdplLC63ePZf1f.UtjMKkbQ23cQoO2OKOFalDBHWAOy21."
|
||||
test:
|
||||
fullName: Test Account
|
||||
password: "$2b$05$hZaI/jToXf/d3BctZdT38Or7H7h6Pn2W3WiB49p5AyhDHFkkYCvo2"
|
||||
- name: admin
|
||||
fullName: Administrator
|
||||
password: "$2b$05$NiBd74ZGdplLC63ePZf1f.UtjMKkbQ23cQoO2OKOFalDBHWAOy21."
|
||||
- name: test
|
||||
fullName: Test Account
|
||||
password: "$2b$05$hZaI/jToXf/d3BctZdT38Or7H7h6Pn2W3WiB49p5AyhDHFkkYCvo2"
|
||||
"/RoleConfigurator":
|
||||
rolesByUser:
|
||||
# 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;
|
||||
|
||||
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.util.generic.options.ListOptions;
|
||||
import io.kubernetes.client.util.generic.options.PatchOptions;
|
||||
import java.io.FileReader;
|
||||
import java.io.IOException;
|
||||
import java.util.Collection;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
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_KIND_VM;
|
||||
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.K8sV1ConfigMapStub;
|
||||
import org.jdrupes.vmoperator.common.K8sV1DeploymentStub;
|
||||
import org.jdrupes.vmoperator.common.K8sV1PodStub;
|
||||
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 static org.junit.jupiter.api.Assertions.*;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
|
|
@ -29,6 +40,9 @@ class BasicTests {
|
|||
private static K8sClient client;
|
||||
private static APIResource vmsContext;
|
||||
private static K8sV1DeploymentStub mgrDeployment;
|
||||
private static K8sDynamicStub vmStub;
|
||||
private static final String VM_NAME = "unittest-vm";
|
||||
private static final Object EXISTS = new Object();
|
||||
|
||||
@BeforeAll
|
||||
static void setUpBeforeClass() throws Exception {
|
||||
|
|
@ -38,23 +52,40 @@ class BasicTests {
|
|||
// Get client
|
||||
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
|
||||
var apiRes = K8s.context(client, VM_OP_GROUP, null, VM_OP_KIND_VM);
|
||||
assertTrue(apiRes.isPresent());
|
||||
vmsContext = apiRes.get();
|
||||
|
||||
// Cleanup existing VM
|
||||
K8sDynamicStub.get(client, vmsContext, "vmop-dev", "unittest-vm")
|
||||
K8sDynamicStub.get(client, vmsContext, "vmop-dev", VM_NAME)
|
||||
.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
|
||||
mgrDeployment
|
||||
= K8sV1DeploymentStub.get(client, "vmop-dev", "vm-operator");
|
||||
mgrDeployment.scale(0);
|
||||
mgrDeployment.scale(1);
|
||||
// Load from Yaml
|
||||
var rdr = new FileReader("test-resources/basic-vm.yaml");
|
||||
vmStub = K8sDynamicStub.createFromYaml(client, vmsContext, rdr);
|
||||
assertTrue(vmStub.model().isPresent());
|
||||
}
|
||||
|
||||
private static void waitForManager()
|
||||
throws ApiException, InterruptedException {
|
||||
// Wait until available
|
||||
|
||||
for (int i = 0; i < 10; i++) {
|
||||
if (mgrDeployment.model().get().getStatus().getConditions()
|
||||
.stream().filter(c -> "Available".equals(c.getType())).findAny()
|
||||
|
|
@ -66,70 +97,250 @@ class BasicTests {
|
|||
fail("vm-operator not deployed.");
|
||||
}
|
||||
|
||||
@AfterAll
|
||||
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();
|
||||
private static void deletePvcs() throws ApiException {
|
||||
ListOptions listOpts = new ListOptions();
|
||||
listOpts.setLabelSelector(
|
||||
"app.kubernetes.io/managed-by=" + VM_OP_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);
|
||||
for (var pvc : knownPvcs) {
|
||||
pvc.delete();
|
||||
}
|
||||
}
|
||||
|
||||
private boolean waitForConfigMap(K8sClient client)
|
||||
throws InterruptedException, ApiException {
|
||||
var stub = K8sV1ConfigMapStub.get(client, "vmop-dev", "unittest-vm");
|
||||
for (int i = 0; i < 10; i++) {
|
||||
if (stub.model().isPresent()) {
|
||||
return true;
|
||||
}
|
||||
Thread.sleep(1000);
|
||||
}
|
||||
return false;
|
||||
@AfterAll
|
||||
static void tearDownAfterClass() throws Exception {
|
||||
// Cleanup
|
||||
K8sDynamicStub.get(client, vmsContext, "vmop-dev", VM_NAME)
|
||||
.delete();
|
||||
deletePvcs();
|
||||
|
||||
// Bring down manager
|
||||
mgrDeployment.scale(0);
|
||||
}
|
||||
|
||||
private boolean waitForPvc(K8sClient client)
|
||||
throws InterruptedException, ApiException {
|
||||
var stub
|
||||
= K8sV1PvcStub.get(client, "vmop-dev", "unittest-vm-runner-data");
|
||||
@Test
|
||||
void testConfigMap()
|
||||
throws IOException, InterruptedException, ApiException {
|
||||
K8sV1ConfigMapStub stub
|
||||
= K8sV1ConfigMapStub.get(client, "vmop-dev", VM_NAME);
|
||||
for (int i = 0; i < 10; i++) {
|
||||
if (stub.model().isPresent()) {
|
||||
return true;
|
||||
break;
|
||||
}
|
||||
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