Refactor internal Kubernetes API and upgrade to official v19 (#19)
Some checks failed
Java CI with Gradle / build (push) Has been cancelled

This commit is contained in:
Michael N. Lipp 2024-03-14 20:12:37 +01:00 committed by GitHub
parent ee2de96c56
commit a2641da7f5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
28 changed files with 2343 additions and 395 deletions

View file

@ -16,7 +16,7 @@ dependencies {
implementation project(':org.jdrupes.vmoperator.common')
implementation 'commons-cli:commons-cli:1.5.0'
implementation 'com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:[2.15.1,3]'
implementation 'com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:[2.16.1]'
runtimeOnly 'org.slf4j:slf4j-jdk14:[2.0.7,3)'
}

View file

@ -1,6 +1,6 @@
/*
* VM-Operator
* Copyright (C) 2023 Michael N. Lipp
* Copyright (C) 2023,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
@ -19,27 +19,17 @@
package org.jdrupes.vmoperator.runner.qemu;
import com.google.gson.JsonObject;
import io.kubernetes.client.apimachinery.GroupVersionKind;
import io.kubernetes.client.custom.Quantity;
import io.kubernetes.client.custom.Quantity.Format;
import io.kubernetes.client.custom.V1Patch;
import io.kubernetes.client.openapi.ApiException;
import io.kubernetes.client.openapi.apis.ApisApi;
import io.kubernetes.client.openapi.apis.CustomObjectsApi;
import io.kubernetes.client.openapi.apis.EventsV1Api;
import io.kubernetes.client.openapi.models.EventsV1Event;
import io.kubernetes.client.openapi.models.V1APIGroup;
import io.kubernetes.client.openapi.models.V1GroupVersionForDiscovery;
import io.kubernetes.client.openapi.models.V1ObjectMeta;
import io.kubernetes.client.util.Config;
import io.kubernetes.client.util.generic.dynamic.DynamicKubernetesApi;
import io.kubernetes.client.util.generic.dynamic.DynamicKubernetesObject;
import io.kubernetes.client.util.generic.options.PatchOptions;
import java.io.IOException;
import java.math.BigDecimal;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.Instant;
import java.time.OffsetDateTime;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
@ -48,6 +38,9 @@ import static org.jdrupes.vmoperator.common.Constants.APP_NAME;
import static org.jdrupes.vmoperator.common.Constants.VM_OP_GROUP;
import static org.jdrupes.vmoperator.common.Constants.VM_OP_KIND_VM;
import org.jdrupes.vmoperator.common.K8s;
import org.jdrupes.vmoperator.common.K8sClient;
import org.jdrupes.vmoperator.common.K8sDynamicModel;
import org.jdrupes.vmoperator.common.K8sDynamicStub;
import org.jdrupes.vmoperator.runner.qemu.events.BalloonChangeEvent;
import org.jdrupes.vmoperator.runner.qemu.events.Exit;
import org.jdrupes.vmoperator.runner.qemu.events.HotpluggableCpuStatus;
@ -75,11 +68,11 @@ public class StatusUpdater extends Component {
private String namespace;
private String vmName;
private DynamicKubernetesApi vmCrApi;
private EventsV1Api evtsApi;
private K8sClient apiClient;
private long observedGeneration;
private boolean guestShutdownStops;
private boolean shutdownByGuest;
private K8sDynamicStub vmStub;
/**
* Instantiates a new status updater.
@ -88,6 +81,16 @@ public class StatusUpdater extends Component {
*/
public StatusUpdater(Channel componentChannel) {
super(componentChannel);
try {
apiClient = new K8sClient();
io.kubernetes.client.openapi.Configuration
.setDefaultApiClient(apiClient);
} catch (IOException e) {
logger.log(Level.SEVERE, e,
() -> "Cannot access events API, terminating.");
fire(new Exit(1));
}
}
/**
@ -154,59 +157,18 @@ public class StatusUpdater extends Component {
return;
}
try {
initVmCrApi(event);
} catch (IOException | ApiException e) {
vmStub = K8sDynamicStub.get(apiClient,
new GroupVersionKind(VM_OP_GROUP, "", VM_OP_KIND_VM),
namespace, vmName);
vmStub.model().ifPresent(model -> {
observedGeneration = model.getMetadata().getGeneration();
});
} catch (ApiException e) {
logger.log(Level.SEVERE, e,
() -> "Cannot access VM's CR, terminating.");
() -> "Cannot access VM object, terminating.");
event.cancel(true);
fire(new Exit(1));
}
try {
evtsApi = new EventsV1Api(Config.defaultClient());
} catch (IOException e) {
logger.log(Level.SEVERE, e,
() -> "Cannot access events API, terminating.");
event.cancel(true);
fire(new Exit(1));
}
}
private void initVmCrApi(Start event) throws IOException, ApiException {
var client = Config.defaultClient();
var apis = new ApisApi(client).getAPIVersions();
var crdVersions = apis.getGroups().stream()
.filter(g -> g.getName().equals(VM_OP_GROUP)).findFirst()
.map(V1APIGroup::getVersions).stream().flatMap(l -> l.stream())
.map(V1GroupVersionForDiscovery::getVersion).toList();
var coa = new CustomObjectsApi(client);
for (var crdVersion : crdVersions) {
var crdApiRes = coa.getAPIResources(VM_OP_GROUP,
crdVersion).getResources().stream()
.filter(r -> VM_OP_KIND_VM.equals(r.getKind())).findFirst();
if (crdApiRes.isEmpty()) {
continue;
}
@SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops")
var crApi = new DynamicKubernetesApi(VM_OP_GROUP,
crdVersion, crdApiRes.get().getName(), client);
var vmCr = crApi.get(namespace, vmName);
if (vmCr.isSuccess()) {
vmCrApi = crApi;
observedGeneration
= vmCr.getObject().getMetadata().getGeneration();
break;
}
}
if (vmCrApi == null) {
logger.severe(() -> "Cannot find VM's CR, terminating.");
event.cancel(true);
fire(new Exit(1));
}
}
@SuppressWarnings("PMD.AvoidDuplicateLiterals")
private JsonObject currentStatus(DynamicKubernetesObject vmCr) {
return vmCr.getRaw().getAsJsonObject("status").deepCopy();
}
/**
@ -221,18 +183,19 @@ public class StatusUpdater extends Component {
guestShutdownStops = event.configuration().guestShutdownStops;
// Remainder applies only if we have a connection to k8s.
if (vmCrApi == null) {
if (vmStub == null) {
return;
}
// A change of the runner configuration is typically caused
// by a new version of the CR. So we observe the new CR.
var vmCr = vmCrApi.get(namespace, vmName).throwsApiException()
.getObject();
if (vmCr.getMetadata().getGeneration() == observedGeneration) {
var vmDef = vmStub.model();
if (vmDef.isPresent()
&& vmDef.get().metadata().getGeneration() == observedGeneration) {
return;
}
vmCrApi.updateStatus(vmCr, from -> {
JsonObject status = currentStatus(from);
vmStub.updateStatus(vmDef.get(), from -> {
JsonObject status = from.status();
status.getAsJsonArray("conditions").asList().stream()
.map(cond -> (JsonObject) cond).filter(cond -> "Running"
.equals(cond.get("type").getAsString()))
@ -249,15 +212,15 @@ public class StatusUpdater extends Component {
* @throws ApiException
*/
@Handler
@SuppressWarnings("PMD.AssignmentInOperand")
public void onRunnerStateChanged(RunnerStateChange event)
throws ApiException {
if (vmCrApi == null) {
K8sDynamicModel vmDef;
if (vmStub == null || (vmDef = vmStub.model().orElse(null)) == null) {
return;
}
var vmCr = vmCrApi.get(namespace, vmName).throwsApiException()
.getObject();
vmCrApi.updateStatus(vmCr, from -> {
JsonObject status = currentStatus(from);
vmStub.updateStatus(vmDef, from -> {
JsonObject status = from.status();
status.getAsJsonArray("conditions").asList().stream()
.map(cond -> (JsonObject) cond)
.forEach(cond -> {
@ -266,7 +229,7 @@ public class StatusUpdater extends Component {
}
});
if (event.state() == State.STARTING) {
status.addProperty("ram", GsonPtr.to(from.getRaw())
status.addProperty("ram", GsonPtr.to(from.data())
.getAsString("spec", "vm", "maximumRam").orElse("0"));
status.addProperty("cpus", 1);
} else if (event.state() == State.STOPPED) {
@ -274,40 +237,32 @@ public class StatusUpdater extends Component {
status.addProperty("cpus", 0);
}
return status;
}).throwsApiException();
});
// Maybe stop VM
if (event.state() == State.TERMINATING && !event.failed()
&& guestShutdownStops && shutdownByGuest) {
logger.info(() -> "Stopping VM because of shutdown by guest.");
PatchOptions patchOpts = new PatchOptions();
patchOpts.setFieldManager("kubernetes-java-kubectl-apply");
var res = vmCrApi.patch(namespace, vmName,
V1Patch.PATCH_FORMAT_JSON_PATCH,
var res = vmStub.patch(V1Patch.PATCH_FORMAT_JSON_PATCH,
new V1Patch("[{\"op\": \"replace\", \"path\": \"/spec/vm/state"
+ "\", \"value\": \"Stopped\"}]"),
patchOpts);
if (!res.isSuccess()) {
apiClient.defaultPatchOptions());
if (!res.isPresent()) {
logger.warning(
() -> "Cannot patch pod annotations: " + res.getStatus());
() -> "Cannot patch pod annotations for: " + vmStub.name());
}
}
// Log event
var evt = new EventsV1Event().kind("Event")
.metadata(new V1ObjectMeta().namespace(namespace)
.generateName("vmrunner-"))
var evt = new EventsV1Event()
.reportingController(VM_OP_GROUP + "/" + APP_NAME)
.reportingInstance(vmCr.getMetadata().getName())
.eventTime(OffsetDateTime.now()).type("Normal")
.regarding(K8s.objectReference(vmCr))
.action("StatusUpdate").reason(event.reason())
.note(event.message());
evtsApi.createNamespacedEvent(namespace, evt, null, null, null, null);
K8s.createEvent(apiClient, vmDef, evt);
}
private void updateRunningCondition(RunnerStateChange event,
DynamicKubernetesObject from, JsonObject cond) {
K8sDynamicModel from, JsonObject cond) {
boolean reportedRunning
= "True".equals(cond.get("status").getAsString());
if (RUNNING_STATES.contains(event.state())
@ -336,18 +291,16 @@ public class StatusUpdater extends Component {
*/
@Handler
public void onBallonChange(BalloonChangeEvent event) throws ApiException {
if (vmCrApi == null) {
if (vmStub == null) {
return;
}
var vmCr = vmCrApi.get(namespace, vmName).throwsApiException()
.getObject();
vmCrApi.updateStatus(vmCr, from -> {
JsonObject status = currentStatus(from);
vmStub.updateStatus(from -> {
JsonObject status = from.status();
status.addProperty("ram",
new Quantity(new BigDecimal(event.size()), Format.BINARY_SI)
.toSuffixedString());
return status;
}).throwsApiException();
});
}
/**
@ -358,16 +311,14 @@ public class StatusUpdater extends Component {
*/
@Handler
public void onCpuChange(HotpluggableCpuStatus event) throws ApiException {
if (vmCrApi == null) {
if (vmStub == null) {
return;
}
var vmCr = vmCrApi.get(namespace, vmName).throwsApiException()
.getObject();
vmCrApi.updateStatus(vmCr, from -> {
JsonObject status = currentStatus(from);
vmStub.updateStatus(from -> {
JsonObject status = from.status();
status.addProperty("cpus", event.usedCpus().size());
return status;
}).throwsApiException();
});
}
/**