Refactor internal Kubernetes API and upgrade to official v19 (#19)
Some checks failed
Java CI with Gradle / build (push) Has been cancelled
Some checks failed
Java CI with Gradle / build (push) Has been cancelled
This commit is contained in:
parent
ee2de96c56
commit
a2641da7f5
28 changed files with 2343 additions and 395 deletions
|
|
@ -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)'
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue