Create ConfigMap.

This commit is contained in:
Michael Lipp 2023-07-27 13:14:04 +02:00
parent ee1a460960
commit 076f86bbe4
4 changed files with 313 additions and 70 deletions

View file

@ -0,0 +1,172 @@
apiVersion: v1
kind: ConfigMap
metadata:
namespace: ${ metadata.namespace.asString }
name: ${ metadata.name.asString }
labels:
app.kubernetes.io/name: ${ constants.APP_NAME }
app.kubernetes.io/instance: ${ metadata.name.asString }
app.kubernetes.io/managed-by: ${ constants.VM_OP_NAME }
data:
config.yaml: |
"/Runner":
# The directory used to store data files. Defaults to (depending on
# values available):
# * $XDG_DATA_HOME/vmrunner/${ metadata.name.asString }
# * $HOME/.local/share/vmrunner/${ metadata.name.asString }
# * ./${ metadata.name.asString }
dataDir: /var/local/vm-data
# The directory used to store runtime files. Defaults to (depending on
# values available):
# * $XDG_RUNTIME_DIR/vmrunner/${ metadata.name.asString }
# * /tmp/$USER/vmrunner/${ metadata.name.asString }
# * /tmp/vmrunner/${ metadata.name.asString }
# runtimeDir: "$XDG_RUNTIME_DIR/vmrunner/${ metadata.name.asString }"
# The template to use. Resolved relative to /usr/share/vmrunner/templates.
# template: "Standard-VM-latest.ftl.yaml"
# The template is copied to the data diretory when the VM starts for
# the first time. Subsequent starts use the copy unless this option is set.
updateTemplate: true
# Define the VM (required)
vm:
# The VM's name (required)
name: ${ metadata.name.asString }
# The machine's uuid. If none is specified, a uuid is generated
# and stored in the data directory. If the uuid is important
# (e.g. because licenses depend on it) it is recommaned to specify
# it here explicitly or to carefully backup the data directory.
# uuid: "generated uuid"
<#if spec.vm.machineUuid??>
uuid: "${ spec.vm.machineUuid.asString }"
</#if>
# Whether to provide a software TPM (defaults to false)
# useTpm: false
useTpm: ${ spec.vm.useTpm.asBoolean?c }
# How to boot (see https://github.com/mnlipp/VM-Operator/blob/main/org.jdrupes.vmoperator.runner.qemu/resources/org/jdrupes/vmoperator/runner/qemu/defaults.yaml):
# * bios
# * uefi[-4m]
# * secure[-4m]
firmware: ${ spec.vm.firmware.asString }
# Whether to show a boot menu.
# bootMenu: false
bootMenu: ${ spec.vm.bootMenu.asBoolean?c }
# When terminating, a graceful powerdown is attempted. If it
# doesn't succeed within the given timeout (seconds) SIGTERM
# is sent to Qemu.
# powerdownTimeout: 900
powerdownTimeout: ${ spec.vm.powerdownTimeout.asLong?c }
# CPU settings
cpuModel: ${ spec.vm.cpuModel.asString }
# Setting maximumCpus to 1 omits the "-smp" options. The defaults (0)
# cause the corresponding property to be omitted from the "-smp" option.
# If currentCpus is greater than maximumCpus, the latter is adjusted.
<#if spec.vm.maximumCpus?? >
maximumCpus: ${ spec.vm.maximumCpus.asInt?c }
</#if>
<#if spec.vm.cpuTopology?? >
cpuSockets: ${ spec.vm.cpuTopology.cpuSockets.asInt?c }
diesPerSocket: ${ spec.vm.cpuTopology.diesPerSocket.asInt?c }
coresPerSocket: ${ spec.vm.cpuTopology.coresPerSocket.asInt?c }
threadsPerCore: ${ spec.vm.cpuTopology.threadsPerCore.asInt?c }
</#if>
<#if spec.vm.currentCpus?? >
currentCpus: ${ spec.vm.currentCpus.asInt?c }
</#if>
# RAM settings
# Maximum defaults to 1G
maximumRam: "${ spec.vm.maximumRam.asString }"
<#if spec.vm.currentRam?? >
currentRam: "${ spec.vm.currentRam.asString }"
</#if>
# RTC settings.
# rtcBase: utc
# rtcClock: rt
rtcBase: ${ spec.vm.rtcBase.asString }
rtcClock: ${ spec.vm.rtcClock.asString }
# Network settings
# Supported types are "tap" and "user" (for debugging). Type "user"
# supports only the property "net".
# network:
# - type: tap
# bridge: br0
# device: virtio-net
# mac: (undefined)
network:
<#assign nwCounter = 0/>
<#list spec.vm.networks.asList() as itf>
<#if itf.tap??>
- type: tap
device: ${ itf.tap.device.asString }
bridge: ${ itf.tap.bridge.asString }
<#if itf.tap.mac??>
mac: "${ itf.tap.mac.asString }"
</#if>
<#elseif itf.user??>
- type: user
device: ${ itf.tap.device.asString }
<#if itf.user.net??>
net: "${ itf.user.net.asString }"
</#if>
</#if>
<#assign nwCounter += 1/>
</#list>
# There are no default drives. The supported types are "ide-cd"
# and "raw". All types support a "bootindex" property.
# Type "raw" can have a property "file" (if backed by a file on
# the host) or a property "device" (if backed by a device).
# drives:
# - type: ide-cd
# bootindex: (undefined)
# file: (undefined)
drives:
<#assign drvCounter = 0/>
<#list spec.vm.disks.asList() as disk>
<#if disk.volumeClaimTemplate.metadata??
&& disk.volumeClaimTemplate.metadata.name??>
<#assign name = disk.volumeClaimTemplate.metadata.name.asString>
<#else>
<#assign name = "" + drvCounter>
</#if>
- type: raw
resource: /dev/disk-${ name }
</#list>
display:
<#if spec.vm.display.spice??>
spice:
port: ${ spec.vm.display.spice.port.asInt?c }
<#if spec.vm.display.spice.ticket??>
ticket: "${ spec.vm.display.spice.ticket.asString }"
</#if>
<#if spec.vm.display.spice.streamingVideo??>
ticket: "${ spec.vm.display.spice.streamingVideo.asString }"
</#if>
usbRedirects: ${ spec.vm.display.spice.usbRedirects.asInt?c }
</#if>
logging.properties: |
handlers=java.util.logging.ConsoleHandler
#org.jgrapes.level=FINE
#org.jgrapes.core.handlerTracking.level=FINER
org.jdrupes.vmoperator.runner.qemu.level=FINE
java.util.logging.ConsoleHandler.level=ALL
java.util.logging.ConsoleHandler.formatter=java.util.logging.SimpleFormatter
java.util.logging.SimpleFormatter.format=%1$tb %1$td %1$tT %4$s %5$s%6$s%n

View file

@ -21,16 +21,29 @@ package org.jdrupes.vmoperator.manager;
import com.google.gson.JsonArray;
import com.google.gson.JsonObject;
import com.google.gson.JsonPrimitive;
import freemarker.core.ParseException;
import freemarker.template.Configuration;
import freemarker.template.DefaultObjectWrapperBuilder;
import freemarker.template.MalformedTemplateNameException;
import freemarker.template.TemplateException;
import freemarker.template.TemplateExceptionHandler;
import freemarker.template.TemplateHashModel;
import freemarker.template.TemplateNotFoundException;
import io.kubernetes.client.custom.V1Patch;
import io.kubernetes.client.openapi.ApiException;
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.io.StringWriter;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import static org.jdrupes.vmoperator.manager.Constants.APP_NAME;
import static org.jdrupes.vmoperator.manager.Constants.VM_OP_GROUP;
import static org.jdrupes.vmoperator.manager.Constants.VM_OP_NAME;
import static org.jdrupes.vmoperator.manager.Constants.VM_OP_VERSION;
import org.jdrupes.vmoperator.util.ExtendedObjectWrapper;
import org.jgrapes.core.Channel;
import org.jgrapes.core.Component;
import org.jgrapes.core.annotation.Handler;
@ -38,9 +51,12 @@ import org.jgrapes.core.annotation.Handler;
/**
* Adapts Kubenetes resources to changes in VM definitions (CRs).
*/
@SuppressWarnings("PMD.DataflowAnomalyAnalysis")
@SuppressWarnings({ "PMD.DataflowAnomalyAnalysis",
"PMD.AvoidDuplicateLiterals" })
public class Reconciler extends Component {
private final Configuration fmConfig;
/**
* Instantiates a new reconciler.
*
@ -48,6 +64,16 @@ public class Reconciler extends Component {
*/
public Reconciler(Channel componentChannel) {
super(componentChannel);
// Configure freemarker library
fmConfig = new Configuration(Configuration.VERSION_2_3_32);
fmConfig.setDefaultEncoding("utf-8");
fmConfig.setObjectWrapper(new ExtendedObjectWrapper(
fmConfig.getIncompatibleImprovements()));
fmConfig.setTemplateExceptionHandler(
TemplateExceptionHandler.RETHROW_HANDLER);
fmConfig.setLogTemplateExceptions(false);
fmConfig.setClassForTemplateLoading(Reconciler.class, "");
}
/**
@ -56,18 +82,30 @@ public class Reconciler extends Component {
* @param event the event
* @param channel the channel
* @throws ApiException the api exception
* @throws IOException
* @throws ParseException
* @throws MalformedTemplateNameException
* @throws TemplateNotFoundException
* @throws TemplateException
* @throws KubectlException
*/
@Handler
public void onVmDefChanged(VmDefChanged event, WatchChannel channel)
throws ApiException {
throws ApiException, TemplateNotFoundException,
MalformedTemplateNameException, ParseException, IOException,
TemplateException {
DynamicKubernetesApi vmDefApi = new DynamicKubernetesApi(VM_OP_GROUP,
VM_OP_VERSION, event.crd().getName(), channel.client());
var defMeta = event.metadata();
var vmDef = vmDefApi.get(defMeta.getNamespace(), defMeta.getName())
.getObject();
@SuppressWarnings("PMD.AvoidDuplicateLiterals")
reconcileDisks(vmDef, channel);
reconcileConfigMap(vmDef, channel);
}
private void reconcileDisks(DynamicKubernetesObject vmDef,
WatchChannel channel) throws ApiException {
var disks = GsonPtr.to(vmDef.getRaw())
.get(JsonArray.class, "spec", "vm", "disks")
.map(JsonArray::asList).orElse(Collections.emptyList());
@ -82,13 +120,13 @@ public class Reconciler extends Component {
int index, JsonObject diskDef, WatchChannel channel)
throws ApiException {
var pvcObject = new DynamicKubernetesObject();
pvcObject.setApiVersion("v1");
pvcObject.setKind("PersistentVolumeClaim");
var pvcDef = GsonPtr.to(pvcObject.getRaw());
var vmDef = GsonPtr.to(vmDefinition.getRaw());
var pvcTpl = GsonPtr.to(diskDef).to("volumeClaimTemplate");
// Copy metadata from template and add missing/additional data.
// Copy base and metadata from template and add missing/additional data.
pvcObject.setApiVersion(pvcTpl.getAsString("apiVersion").get());
pvcObject.setKind(pvcTpl.getAsString("kind").get());
var vmName = vmDef.getAsString("metadata", "name").orElse("default");
pvcDef.get(JsonObject.class).add("metadata",
pvcTpl.to("metadata").get(JsonObject.class).deepCopy());
@ -114,11 +152,6 @@ public class Reconciler extends Component {
// PVC does not exist yet, copy spec from template
pvcDef.get(JsonObject.class).add("spec",
pvcTpl.to("spec").get(JsonObject.class).deepCopy());
// Add missing
pvcDef.to("spec").computeIfAbsent("accessModes",
() -> GsonPtr.to(new JsonArray()).set(0, "ReadWriteOnce")
.get());
pvcDef.to("spec").getOrSet("volumeMode", "Block");
pvcApi.create(pvcObject);
} else {
// spec is immutable, so mix in existing spec
@ -135,4 +168,34 @@ public class Reconciler extends Component {
}
}
private void reconcileConfigMap(DynamicKubernetesObject vmDefinition,
WatchChannel channel) throws TemplateNotFoundException,
MalformedTemplateNameException, ParseException, IOException,
TemplateException, ApiException {
// Combine template and data and parse result
// (tempting, but no need to use a pipe here)
var fmTemplate = fmConfig.getTemplate("etcConfig.ftl.yaml");
StringWriter out = new StringWriter();
@SuppressWarnings("PMD.UseConcurrentHashMap")
Map<String, Object> model = new HashMap<>();
model.putAll(vmDefinition.getRaw().asMap());
model.put("constants",
(TemplateHashModel) new DefaultObjectWrapperBuilder(
Configuration.VERSION_2_3_32)
.build().getStaticModels().get(Constants.class.getName()));
fmTemplate.process(model, out);
// Apply
PatchOptions opts = new PatchOptions();
opts.setForce(false);
opts.setFieldManager("kubernetes-java-kubectl-apply");
DynamicKubernetesApi pvcApi = new DynamicKubernetesApi("", "v1",
"configmaps", channel.client());
var vmDef = GsonPtr.to(vmDefinition.getRaw());
pvcApi.patch(vmDef.getAsString("metadata", "namespace").get(),
vmDef.getAsString("metadata", "name").get(),
V1Patch.PATCH_FORMAT_APPLY_YAML, new V1Patch(out.toString()),
opts).throwsApiException();
}
}