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

@ -78,20 +78,18 @@ spec:
maximumCpus: maximumCpus:
description: >- description: >-
Either maximumCpus or cpuTopology may be specified. Either maximumCpus or cpuTopology may be specified.
If currentCpus is greater than maximumCpus, the If neither is specified, maximum cpus is set to 4.
latter is adjusted. Setting maximumCpus to 1 omits Setting maximumCpus to 1 omits the "-smp" options.
the "-smp" options.
type: integer type: integer
default: 4
cpuTopology: cpuTopology:
description: >- description: >-
The defaults (0) cause the corresponding property Values of 0 cause the corresponding property
to be omitted from the "-smp" option. to be omitted from the "-smp" option.
type: object type: object
properties: properties:
sockets: sockets:
type: integer type: integer
default: 0 default: 1
diesPerSocket: diesPerSocket:
type: integer type: integer
default: 0 default: 0
@ -102,8 +100,9 @@ spec:
type: integer type: integer
default: 0 default: 0
currentCpus: currentCpus:
description: >-
Defaults to maximumCpus.
type: integer type: integer
default: 2
maximumRam: maximumRam:
type: string type: string
default: "1G" default: "1G"
@ -113,6 +112,10 @@ spec:
description: Passed to Qemu unmodified. description: Passed to Qemu unmodified.
type: string type: string
default: "utc" default: "utc"
rtcClock:
description: Passed to Qemu unmodified.
type: string
default: "rt"
networks: networks:
type: array type: array
items: items:
@ -136,12 +139,17 @@ spec:
user: user:
type: object type: object
properties: properties:
device:
description: The device to use.
type: string
default: "virtio-net"
net: net:
type: string type: string
oneOf: oneOf:
- properties: - properties:
tap: tap:
user: user:
default: []
disks: disks:
description: >- description: >-
Disks make persistent storage available. The Disks make persistent storage available. The
@ -151,8 +159,6 @@ spec:
items: items:
type: object type: object
properties: properties:
hostDevice:
type: string
volumeClaimTemplate: volumeClaimTemplate:
description: >- description: >-
A PVC spec to be used to provide the disk. The easiest A PVC spec to be used to provide the disk. The easiest
@ -180,36 +186,8 @@ spec:
description: >- description: >-
EmbeddedMetadata contains metadata relevant to EmbeddedMetadata contains metadata relevant to
an EmbeddedResource. an EmbeddedResource.
type: object
properties: properties:
annotations:
additionalProperties:
type: string
description: >-
Annotations is an unstructured key value
map stored with a resource that may be set by external
tools to store and retrieve arbitrary metadata. They
are not queryable and should be preserved when modifying
objects. More info: http://kubernetes.io/docs/user-guide/annotations
type: object
labels:
additionalProperties:
type: string
description: >-
Map of string keys and values that can be
used to organize and categorize (scope and select) objects.
May match selectors of replication controllers and services.
More info: http://kubernetes.io/docs/user-guide/labels
type: object
name:
description: >-
Name must be unique within a namespace.
Is required when creating resources, although some resources
may allow a client to request the generation of an appropriate
name automatically. Name is primarily intended for creation
idempotence and configuration definition. Cannot be
updated. More info: http://kubernetes.io/docs/user-guide/identifiers#names.
The name is generated automatically but can be overriden.
type: string
namespace: namespace:
description: >- description: >-
Namespace defines the space within which each Namespace defines the space within which each
@ -221,7 +199,35 @@ spec:
More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces.
The default value is the VM's namespace. The default value is the VM's namespace.
type: string type: string
type: object name:
description: >-
Name must be unique within a namespace.
Is required when creating resources, although some resources
may allow a client to request the generation of an appropriate
name automatically. Name is primarily intended for creation
idempotence and configuration definition. Cannot be
updated. More info: http://kubernetes.io/docs/user-guide/identifiers#names.
The name is generated automatically but can be overriden.
type: string
labels:
description: >-
Map of string keys and values that can be
used to organize and categorize (scope and select) objects.
May match selectors of replication controllers and services.
More info: http://kubernetes.io/docs/user-guide/labels
type: object
additionalProperties:
type: string
annotations:
description: >-
Annotations is an unstructured key value
map stored with a resource that may be set by external
tools to store and retrieve arbitrary metadata. They
are not queryable and should be preserved when modifying
objects. More info: http://kubernetes.io/docs/user-guide/annotations
type: object
additionalProperties:
type: string
spec: spec:
description: >- description: >-
Spec defines the desired characteristics of Spec defines the desired characteristics of
@ -420,24 +426,26 @@ spec:
type: object type: object
bootindex: bootindex:
type: integer type: integer
displays: required:
type: array - volumeClaimTemplate
items: default: []
type: object display:
properties: type: object
spice: properties:
type: object spice:
properties: type: object
port: properties:
type: integer port:
default: 5900 type: integer
ticket: default: 5900
type: string ticket:
streamingVideo: type: string
type: string streamingVideo:
usbRedirects: type: string
type: integer usbRedirects:
default: 2 type: integer
default: 2
default: { spice: { port: 5900, usbRedirects: 2 } }
oneOf: oneOf:
- properties: - properties:
maximumCpus: maximumCpus:

View file

@ -26,6 +26,6 @@ spec:
storage: 40Gi storage: 40Gi
# - hostDevice: /dev/vgmain/test-vm # - hostDevice: /dev/vgmain/test-vm
displays: display:
- spice: spice:
port: 5910 port: 5910

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.JsonArray;
import com.google.gson.JsonObject; import com.google.gson.JsonObject;
import com.google.gson.JsonPrimitive; 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.custom.V1Patch;
import io.kubernetes.client.openapi.ApiException; import io.kubernetes.client.openapi.ApiException;
import io.kubernetes.client.util.generic.dynamic.DynamicKubernetesApi; import io.kubernetes.client.util.generic.dynamic.DynamicKubernetesApi;
import io.kubernetes.client.util.generic.dynamic.DynamicKubernetesObject; import io.kubernetes.client.util.generic.dynamic.DynamicKubernetesObject;
import io.kubernetes.client.util.generic.options.PatchOptions; import io.kubernetes.client.util.generic.options.PatchOptions;
import java.io.IOException;
import java.io.StringWriter;
import java.util.Collections; 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.APP_NAME;
import static org.jdrupes.vmoperator.manager.Constants.VM_OP_GROUP; 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_NAME;
import static org.jdrupes.vmoperator.manager.Constants.VM_OP_VERSION; 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.Channel;
import org.jgrapes.core.Component; import org.jgrapes.core.Component;
import org.jgrapes.core.annotation.Handler; 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). * Adapts Kubenetes resources to changes in VM definitions (CRs).
*/ */
@SuppressWarnings("PMD.DataflowAnomalyAnalysis") @SuppressWarnings({ "PMD.DataflowAnomalyAnalysis",
"PMD.AvoidDuplicateLiterals" })
public class Reconciler extends Component { public class Reconciler extends Component {
private final Configuration fmConfig;
/** /**
* Instantiates a new reconciler. * Instantiates a new reconciler.
* *
@ -48,6 +64,16 @@ public class Reconciler extends Component {
*/ */
public Reconciler(Channel componentChannel) { public Reconciler(Channel componentChannel) {
super(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 event the event
* @param channel the channel * @param channel the channel
* @throws ApiException the api exception * @throws ApiException the api exception
* @throws IOException
* @throws ParseException
* @throws MalformedTemplateNameException
* @throws TemplateNotFoundException
* @throws TemplateException
* @throws KubectlException * @throws KubectlException
*/ */
@Handler @Handler
public void onVmDefChanged(VmDefChanged event, WatchChannel channel) public void onVmDefChanged(VmDefChanged event, WatchChannel channel)
throws ApiException { throws ApiException, TemplateNotFoundException,
MalformedTemplateNameException, ParseException, IOException,
TemplateException {
DynamicKubernetesApi vmDefApi = new DynamicKubernetesApi(VM_OP_GROUP, DynamicKubernetesApi vmDefApi = new DynamicKubernetesApi(VM_OP_GROUP,
VM_OP_VERSION, event.crd().getName(), channel.client()); VM_OP_VERSION, event.crd().getName(), channel.client());
var defMeta = event.metadata(); var defMeta = event.metadata();
var vmDef = vmDefApi.get(defMeta.getNamespace(), defMeta.getName()) var vmDef = vmDefApi.get(defMeta.getNamespace(), defMeta.getName())
.getObject(); .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()) var disks = GsonPtr.to(vmDef.getRaw())
.get(JsonArray.class, "spec", "vm", "disks") .get(JsonArray.class, "spec", "vm", "disks")
.map(JsonArray::asList).orElse(Collections.emptyList()); .map(JsonArray::asList).orElse(Collections.emptyList());
@ -82,13 +120,13 @@ public class Reconciler extends Component {
int index, JsonObject diskDef, WatchChannel channel) int index, JsonObject diskDef, WatchChannel channel)
throws ApiException { throws ApiException {
var pvcObject = new DynamicKubernetesObject(); var pvcObject = new DynamicKubernetesObject();
pvcObject.setApiVersion("v1");
pvcObject.setKind("PersistentVolumeClaim");
var pvcDef = GsonPtr.to(pvcObject.getRaw()); var pvcDef = GsonPtr.to(pvcObject.getRaw());
var vmDef = GsonPtr.to(vmDefinition.getRaw()); var vmDef = GsonPtr.to(vmDefinition.getRaw());
var pvcTpl = GsonPtr.to(diskDef).to("volumeClaimTemplate"); 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"); var vmName = vmDef.getAsString("metadata", "name").orElse("default");
pvcDef.get(JsonObject.class).add("metadata", pvcDef.get(JsonObject.class).add("metadata",
pvcTpl.to("metadata").get(JsonObject.class).deepCopy()); 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 // PVC does not exist yet, copy spec from template
pvcDef.get(JsonObject.class).add("spec", pvcDef.get(JsonObject.class).add("spec",
pvcTpl.to("spec").get(JsonObject.class).deepCopy()); 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); pvcApi.create(pvcObject);
} else { } else {
// spec is immutable, so mix in existing spec // 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();
}
} }