Compare commits

...
Sign in to create a new pull request.

4 commits

Author SHA1 Message Date
c205971f6f Back port tests.
Some checks failed
Java CI with Gradle / build (push) Has been cancelled
2024-11-02 14:27:59 +01:00
86d3c75779 Update.
Some checks failed
Java CI with Gradle / build (push) Has been cancelled
2024-10-26 22:56:11 +02:00
c69a658b72 Merge branch 'main' into release/v3.4.x 2024-10-26 22:34:31 +02:00
452e0604ca Update version. 2024-10-06 14:07:09 +02:00
7 changed files with 534 additions and 102 deletions

View file

@ -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

View file

@ -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

View file

@ -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

View 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

View file

@ -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

View file

@ -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());
}
}
}

View file

@ -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;
}
}