Working PVC for disks reconciliation.
This commit is contained in:
parent
f1b1b2c059
commit
d1410183cb
8 changed files with 625 additions and 30 deletions
|
|
@ -153,6 +153,271 @@ spec:
|
|||
properties:
|
||||
hostDevice:
|
||||
type: string
|
||||
volumeClaimTemplate:
|
||||
description: >-
|
||||
A PVC spec to be used to provide the disk. The easiest
|
||||
way to use a volume that cannot be automatically provisioned
|
||||
(for whatever reason) is to use a label selector alongside
|
||||
manually created PersistentVolumes.
|
||||
properties:
|
||||
apiVersion:
|
||||
description: >-
|
||||
APIVersion defines the versioned schema of this
|
||||
representation of an object. Servers should convert recognized
|
||||
schemas to the latest internal value, and may reject unrecognized
|
||||
values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
|
||||
type: string
|
||||
default: v1
|
||||
kind:
|
||||
description: >-
|
||||
Kind is a string value representing the REST
|
||||
resource this object represents. Servers may infer this
|
||||
from the endpoint the client submits requests to. Cannot
|
||||
be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
|
||||
type: string
|
||||
default: PersistentVolumeClaim
|
||||
metadata:
|
||||
description: >-
|
||||
EmbeddedMetadata contains metadata relevant to
|
||||
an EmbeddedResource.
|
||||
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:
|
||||
description: >-
|
||||
Namespace defines the space within which each
|
||||
name must be unique. An empty namespace is equivalent to the
|
||||
"default" namespace, but "default" is the canonical
|
||||
representation. Not all objects are required to be scoped
|
||||
to a namespace - the value of this field for those objects
|
||||
will be empty. Must be a DNS_LABEL. Cannot be updated.
|
||||
More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces.
|
||||
The default value is the VM's namespace.
|
||||
type: string
|
||||
type: object
|
||||
spec:
|
||||
description: >-
|
||||
Spec defines the desired characteristics of
|
||||
a volume requested by a pod author. More info:
|
||||
https://kubernetes.io/docs/concepts/storage/persistent-volumes#persistentvolumeclaims
|
||||
properties:
|
||||
accessModes:
|
||||
description: >-
|
||||
accessModes contains the desired access
|
||||
modes the volume should have. More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#access-modes-1
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
default: [ "ReadWriteOnce" ]
|
||||
dataSource:
|
||||
description: >-
|
||||
dataSource field can be used to specify
|
||||
either: * An existing VolumeSnapshot object (snapshot.storage.k8s.io/VolumeSnapshot)
|
||||
* An existing PVC (PersistentVolumeClaim) If the provisioner
|
||||
or an external controller can support the specified
|
||||
data source, it will create a new volume based on the
|
||||
contents of the specified data source. If the AnyVolumeDataSource
|
||||
feature gate is enabled, this field will always have
|
||||
the same contents as the DataSourceRef field.
|
||||
properties:
|
||||
apiGroup:
|
||||
description: >-
|
||||
APIGroup is the group for the resource
|
||||
being referenced. If APIGroup is not specified,
|
||||
the specified Kind must be in the core API group.
|
||||
For any other third-party types, APIGroup is required.
|
||||
type: string
|
||||
kind:
|
||||
description: >-
|
||||
Kind is the type of resource being referenced
|
||||
type: string
|
||||
name:
|
||||
description: >-
|
||||
Name is the name of resource being referenced
|
||||
type: string
|
||||
required:
|
||||
- kind
|
||||
- name
|
||||
type: object
|
||||
x-kubernetes-map-type: atomic
|
||||
dataSourceRef:
|
||||
description: >-
|
||||
dataSourceRef specifies the object from
|
||||
which to populate the volume with data, if a non-empty
|
||||
volume is desired. This may be any local object from
|
||||
a non-empty API group (non core object) or a PersistentVolumeClaim
|
||||
object. When this field is specified, volume binding
|
||||
will only succeed if the type of the specified object
|
||||
matches some installed volume populator or dynamic provisioner.
|
||||
This field will replace the functionality of the DataSource
|
||||
field and as such if both fields are non-empty, they
|
||||
must have the same value. For backwards compatibility,
|
||||
both fields (DataSource and DataSourceRef) will be set
|
||||
to the same value automatically if one of them is empty
|
||||
and the other is non-empty. There are two important
|
||||
differences between DataSource and DataSourceRef: *
|
||||
While DataSource only allows two specific types of objects,
|
||||
DataSourceRef allows any non-core object, as well as
|
||||
PersistentVolumeClaim objects. * While DataSource ignores
|
||||
disallowed values (dropping them), DataSourceRef preserves
|
||||
all values, and generates an error if a disallowed value
|
||||
is specified. (Beta) Using this field requires the AnyVolumeDataSource
|
||||
feature gate to be enabled.
|
||||
properties:
|
||||
apiGroup:
|
||||
description: >-
|
||||
APIGroup is the group for the resource
|
||||
being referenced. If APIGroup is not specified,
|
||||
the specified Kind must be in the core API group.
|
||||
For any other third-party types, APIGroup is required.
|
||||
type: string
|
||||
kind:
|
||||
description: >-
|
||||
Kind is the type of resource being referenced
|
||||
type: string
|
||||
name:
|
||||
description: >-
|
||||
Name is the name of resource being referenced
|
||||
type: string
|
||||
required:
|
||||
- kind
|
||||
- name
|
||||
type: object
|
||||
x-kubernetes-map-type: atomic
|
||||
resources:
|
||||
description: >-
|
||||
resources represents the minimum resources
|
||||
the volume should have. If RecoverVolumeExpansionFailure
|
||||
feature is enabled users are allowed to specify resource
|
||||
requirements that are lower than previous value but
|
||||
must still be higher than capacity recorded in the status
|
||||
field of the claim. More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#resources
|
||||
properties:
|
||||
limits:
|
||||
additionalProperties:
|
||||
anyOf:
|
||||
- type: integer
|
||||
- type: string
|
||||
pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$
|
||||
x-kubernetes-int-or-string: true
|
||||
description: >-
|
||||
Limits describes the maximum amount
|
||||
of compute resources allowed. More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/
|
||||
type: object
|
||||
requests:
|
||||
additionalProperties:
|
||||
anyOf:
|
||||
- type: integer
|
||||
- type: string
|
||||
pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$
|
||||
x-kubernetes-int-or-string: true
|
||||
description: >-
|
||||
Requests describes the minimum amount
|
||||
of compute resources required. If Requests is omitted
|
||||
for a container, it defaults to Limits if that is
|
||||
explicitly specified, otherwise to an implementation-defined
|
||||
value. More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/
|
||||
type: object
|
||||
type: object
|
||||
selector:
|
||||
description: >-
|
||||
selector is a label query over volumes to
|
||||
consider for binding.
|
||||
properties:
|
||||
matchExpressions:
|
||||
description: >-
|
||||
matchExpressions is a list of label selector
|
||||
requirements. The requirements are ANDed.
|
||||
items:
|
||||
description: >-
|
||||
A label selector requirement is a selector
|
||||
that contains values, a key, and an operator that
|
||||
relates the key and values.
|
||||
properties:
|
||||
key:
|
||||
description: >-
|
||||
key is the label key that the selector
|
||||
applies to.
|
||||
type: string
|
||||
operator:
|
||||
description: >-
|
||||
operator represents a key's relationship
|
||||
to a set of values. Valid operators are In,
|
||||
NotIn, Exists and DoesNotExist.
|
||||
type: string
|
||||
values:
|
||||
description: >-
|
||||
values is an array of string values.
|
||||
If the operator is In or NotIn, the values
|
||||
array must be non-empty. If the operator is
|
||||
Exists or DoesNotExist, the values array must
|
||||
be empty. This array is replaced during a
|
||||
strategic merge patch.
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
required:
|
||||
- key
|
||||
- operator
|
||||
type: object
|
||||
type: array
|
||||
matchLabels:
|
||||
additionalProperties:
|
||||
type: string
|
||||
description: >-
|
||||
matchLabels is a map of {key,value} pairs.
|
||||
A single {key,value} in the matchLabels map is equivalent
|
||||
to an element of matchExpressions, whose key field
|
||||
is "key", the operator is "In", and the values array
|
||||
contains only "value". The requirements are ANDed.
|
||||
type: object
|
||||
type: object
|
||||
x-kubernetes-map-type: atomic
|
||||
storageClassName:
|
||||
description: >-
|
||||
storageClassName is the name of the StorageClass
|
||||
required by the claim. More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#class-1
|
||||
type: string
|
||||
volumeMode:
|
||||
description: >-
|
||||
volumeMode defines what type of volume is
|
||||
required by the claim.
|
||||
type: string
|
||||
default: Block
|
||||
volumeName:
|
||||
description: >-
|
||||
volumeName is the binding reference to the
|
||||
PersistentVolume backing this claim.
|
||||
type: string
|
||||
type: object
|
||||
type: object
|
||||
bootindex:
|
||||
type: integer
|
||||
displays:
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
apiVersion: "vmoperator.jdrupes.org/v1"
|
||||
kind: VirtualMachine
|
||||
metadata:
|
||||
namespace: qemu-vms
|
||||
name: test-vm
|
||||
spec:
|
||||
image:
|
||||
|
|
@ -18,7 +19,13 @@ spec:
|
|||
- tap:
|
||||
mac: "00:16:3e:33:59:10"
|
||||
disks:
|
||||
- hostDevice: /dev/vgmain/test-vm
|
||||
- volumeClaimTemplate:
|
||||
spec:
|
||||
resources:
|
||||
requests:
|
||||
storage: 40Gi
|
||||
|
||||
# - hostDevice: /dev/vgmain/test-vm
|
||||
displays:
|
||||
- spice:
|
||||
port: 5910
|
||||
|
|
|
|||
|
|
@ -17,7 +17,8 @@ dependencies {
|
|||
implementation project(':org.jdrupes.vmoperator.util')
|
||||
|
||||
implementation 'commons-cli:commons-cli:1.5.0'
|
||||
implementation 'io.kubernetes:client-java:18.0.0'
|
||||
implementation 'io.kubernetes:client-java:[18.0.0,19)'
|
||||
implementation 'io.kubernetes:client-java-extended:[18.0.0,19)'
|
||||
|
||||
runtimeOnly 'org.slf4j:slf4j-jdk14:[2.0.7,3)'
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,8 +23,18 @@ package org.jdrupes.vmoperator.manager;
|
|||
*/
|
||||
public class Constants {
|
||||
|
||||
static final String VM_OP_GROUP = "vmoperator.jdrupes.org";
|
||||
static final String VM_OP_VERSION = "v1";
|
||||
static final String VM_OP_KIND_VM = "VirtualMachine";
|
||||
/** The Constant VM_OP_NAME. */
|
||||
public static final String VM_OP_NAME = "vm-operator";
|
||||
|
||||
/** The Constant VM_OP_GROUP. */
|
||||
public static final String VM_OP_GROUP = "vmoperator.jdrupes.org";
|
||||
|
||||
/** The Constant VM_OP_VERSION. */
|
||||
public static final String VM_OP_VERSION = "v1";
|
||||
|
||||
/** The Constant VM_OP_KIND_VM. */
|
||||
public static final String VM_OP_KIND_VM = "VirtualMachine";
|
||||
|
||||
/** The Constant APP_NAME. */
|
||||
public static final String APP_NAME = "vm-runner";
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,253 @@
|
|||
/*
|
||||
* 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.manager;
|
||||
|
||||
import com.google.gson.JsonArray;
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonPrimitive;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
/**
|
||||
* Utility class for pointing to elements on a Gson (Json) tree.
|
||||
*/
|
||||
@SuppressWarnings({ "PMD.DataflowAnomalyAnalysis",
|
||||
"PMD.ClassWithOnlyPrivateConstructorsShouldBeFinal" })
|
||||
public class GsonPtr {
|
||||
|
||||
private final JsonElement position;
|
||||
|
||||
private GsonPtr(JsonElement root) {
|
||||
this.position = root;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new instance pointing to the given element.
|
||||
*
|
||||
* @param root the root
|
||||
* @return the Gson pointer
|
||||
*/
|
||||
@SuppressWarnings("PMD.ShortMethodName")
|
||||
public static GsonPtr to(JsonElement root) {
|
||||
return new GsonPtr(root);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new instance pointing to the {@link JsonElement}
|
||||
* selected by the given selectors. If a selector of type
|
||||
* {@link String} denoted a non-existant member of a
|
||||
* {@link JsonObject}, a new member (of type {@link JsonObject}
|
||||
* is added.
|
||||
*
|
||||
* @param selectors the selectors
|
||||
* @return the Gson pointer
|
||||
*/
|
||||
@SuppressWarnings({ "PMD.ShortMethodName", "PMD.PreserveStackTrace" })
|
||||
public GsonPtr to(Object... selectors) {
|
||||
JsonElement element = position;
|
||||
for (Object sel : selectors) {
|
||||
if (element instanceof JsonObject obj
|
||||
&& sel instanceof String member) {
|
||||
element = Optional.ofNullable(obj.get(member)).orElseGet(() -> {
|
||||
@SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops")
|
||||
var child = new JsonObject();
|
||||
obj.add(member, child);
|
||||
return child;
|
||||
});
|
||||
continue;
|
||||
}
|
||||
if (element instanceof JsonArray arr
|
||||
&& sel instanceof Integer index) {
|
||||
try {
|
||||
element = arr.get(index);
|
||||
} catch (IndexOutOfBoundsException e) {
|
||||
throw new IllegalStateException("Selected array index"
|
||||
+ " may not be empty.");
|
||||
}
|
||||
continue;
|
||||
}
|
||||
throw new IllegalStateException("Invalid selection");
|
||||
}
|
||||
return new GsonPtr(element);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns {@link JsonElement} that the pointer points to.
|
||||
*
|
||||
* @return the result
|
||||
*/
|
||||
public JsonElement get() {
|
||||
return position;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns {@link JsonElement} that the pointer points to,
|
||||
* casted to the given type.
|
||||
*
|
||||
* @param <T> the generic type
|
||||
* @param cls the cls
|
||||
* @return the result
|
||||
*/
|
||||
@SuppressWarnings({ "PMD.AvoidBranchingStatementAsLastInLoop" })
|
||||
public <T extends JsonElement> T get(Class<T> cls) {
|
||||
if (cls.isAssignableFrom(position.getClass())) {
|
||||
return cls.cast(position);
|
||||
}
|
||||
throw new IllegalArgumentException("Not positioned at element"
|
||||
+ " of desired type.");
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the selected {@link JsonElement}, cast to the class
|
||||
* specified.
|
||||
*
|
||||
* @param <T> the generic type
|
||||
* @param cls the cls
|
||||
* @param selectors the selectors
|
||||
* @return the optional
|
||||
*/
|
||||
@SuppressWarnings({ "PMD.AvoidBranchingStatementAsLastInLoop" })
|
||||
public <T extends JsonElement> Optional<T>
|
||||
get(Class<T> cls, Object... selectors) {
|
||||
JsonElement element = position;
|
||||
for (Object sel : selectors) {
|
||||
if (element instanceof JsonObject obj
|
||||
&& sel instanceof String member) {
|
||||
element = obj.get(member);
|
||||
if (element == null) {
|
||||
return Optional.empty();
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (element instanceof JsonArray arr
|
||||
&& sel instanceof Integer index) {
|
||||
try {
|
||||
element = arr.get(index);
|
||||
} catch (IndexOutOfBoundsException e) {
|
||||
return Optional.empty();
|
||||
}
|
||||
continue;
|
||||
}
|
||||
return Optional.empty();
|
||||
}
|
||||
if (cls.isAssignableFrom(element.getClass())) {
|
||||
return Optional.of(cls.cast(element));
|
||||
}
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the String value of the selected {@link JsonPrimitive}.
|
||||
*
|
||||
* @param selectors the selectors
|
||||
* @return the as string
|
||||
*/
|
||||
public Optional<String> getAsString(Object... selectors) {
|
||||
return get(JsonPrimitive.class, selectors)
|
||||
.map(JsonPrimitive::getAsString);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the selected value. This pointer must point to a
|
||||
* {@link JsonObject} or {@link JsonArray}. The selector must
|
||||
* be a {@link String} or an integer respectively.
|
||||
*
|
||||
* @param selector the selector
|
||||
* @param value the value
|
||||
* @return the Gson pointer
|
||||
*/
|
||||
public GsonPtr set(Object selector, JsonElement value) {
|
||||
if (position instanceof JsonObject obj
|
||||
&& selector instanceof String member) {
|
||||
obj.add(member, value);
|
||||
return this;
|
||||
}
|
||||
if (position instanceof JsonArray arr
|
||||
&& selector instanceof Integer index) {
|
||||
if (index >= arr.size()) {
|
||||
arr.add(value);
|
||||
} else {
|
||||
arr.set(index, value);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
throw new IllegalStateException("Invalid selection");
|
||||
}
|
||||
|
||||
/**
|
||||
* Short for `set(selector, new JsonPrimitive(value))`.
|
||||
*
|
||||
* @param selector the selector
|
||||
* @param value the value
|
||||
* @return the gson ptr
|
||||
* @see #set(Object, JsonElement)
|
||||
*/
|
||||
public GsonPtr set(Object selector, String value) {
|
||||
return set(selector, new JsonPrimitive(value));
|
||||
}
|
||||
|
||||
/**
|
||||
* Same as {@link #set(Object, JsonElement)}, but sets the value
|
||||
* only if it doesn't exist yet, else returns the existing value.
|
||||
* If this pointer points to a {@link JsonArray} and the selector
|
||||
* if larger than or equal to the size of the array, the supplied
|
||||
* value will be appended.
|
||||
*
|
||||
* @param <T> the generic type
|
||||
* @param selector the selector
|
||||
* @param supplier the supplier of the missing value
|
||||
* @return the existing or supplied value
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public <T extends JsonElement> T
|
||||
computeIfAbsent(Object selector, Supplier<T> supplier) {
|
||||
if (position instanceof JsonObject obj
|
||||
&& selector instanceof String member) {
|
||||
return Optional.ofNullable((T) obj.get(member)).orElseGet(() -> {
|
||||
var res = supplier.get();
|
||||
obj.add(member, res);
|
||||
return res;
|
||||
});
|
||||
}
|
||||
if (position instanceof JsonArray arr
|
||||
&& selector instanceof Integer index) {
|
||||
if (index >= arr.size()) {
|
||||
var res = supplier.get();
|
||||
arr.add(res);
|
||||
return res;
|
||||
}
|
||||
return (T) arr.get(index);
|
||||
}
|
||||
throw new IllegalStateException("Invalid selection");
|
||||
}
|
||||
|
||||
/**
|
||||
* Short for `computeIfAbsent(selector, () -> new JsonPrimitive(value))`.
|
||||
*
|
||||
* @param selector the selector
|
||||
* @param value the value
|
||||
* @return the Gson pointer
|
||||
*/
|
||||
public GsonPtr computeIfAbsent(Object selector, String value) {
|
||||
computeIfAbsent(selector, () -> new JsonPrimitive(value));
|
||||
return this;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -18,9 +18,19 @@
|
|||
|
||||
package org.jdrupes.vmoperator.manager;
|
||||
|
||||
import com.google.gson.JsonArray;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonPrimitive;
|
||||
import io.kubernetes.client.custom.V1Patch;
|
||||
import io.kubernetes.client.extended.kubectl.exception.KubectlException;
|
||||
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.util.Collections;
|
||||
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.jgrapes.core.Channel;
|
||||
import org.jgrapes.core.Component;
|
||||
|
|
@ -29,6 +39,7 @@ import org.jgrapes.core.annotation.Handler;
|
|||
/**
|
||||
* Adapts Kubenetes resources to changes in VM definitions (CRs).
|
||||
*/
|
||||
@SuppressWarnings("PMD.DataflowAnomalyAnalysis")
|
||||
public class Reconciler extends Component {
|
||||
|
||||
/**
|
||||
|
|
@ -46,6 +57,7 @@ public class Reconciler extends Component {
|
|||
* @param event the event
|
||||
* @param channel the channel
|
||||
* @throws ApiException the api exception
|
||||
* @throws KubectlException
|
||||
*/
|
||||
@Handler
|
||||
public void onVmDefChanged(VmDefChanged event, WatchChannel channel)
|
||||
|
|
@ -53,28 +65,75 @@ public class Reconciler extends Component {
|
|||
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());
|
||||
var vmDef = vmDefApi.get(defMeta.getNamespace(), defMeta.getName())
|
||||
.getObject();
|
||||
|
||||
// DynamicKubernetesApi cmApi = new DynamicKubernetesApi("", "v1",
|
||||
// "configmaps", channel.client());
|
||||
// var cm = new DynamicKubernetesObject();
|
||||
// cm.setApiVersion("v1");
|
||||
// cm.setKind("ConfigMap");
|
||||
// V1ObjectMeta metadata = new V1ObjectMeta();
|
||||
// metadata.setNamespace("default");
|
||||
// metadata.setName("test");
|
||||
// cm.setMetadata(metadata);
|
||||
// JsonObject data = new JsonObject();
|
||||
// data.addProperty("test", "value");
|
||||
// cm.getRaw().add("data", data);
|
||||
//
|
||||
// var response = cmApi.create("default", cm, new CreateOptions())
|
||||
// .throwsApiException();
|
||||
@SuppressWarnings("PMD.AvoidDuplicateLiterals")
|
||||
var disks = GsonPtr.to(vmDef.getRaw())
|
||||
.get(JsonArray.class, "spec", "vm", "disks")
|
||||
.map(JsonArray::asList).orElse(Collections.emptyList());
|
||||
int index = 0;
|
||||
for (var disk : disks) {
|
||||
reconcileDisk(vmDef, index++, (JsonObject) disk, channel);
|
||||
}
|
||||
}
|
||||
|
||||
// var obj = channel.coa().getNamespacedCustomObject(VM_OP_GROUP, VM_OP_VERSION,
|
||||
// md.getNamespace(), event.crd().getName(), md.getName());
|
||||
event = null;
|
||||
@SuppressWarnings({ "PMD.AvoidDuplicateLiterals", "PMD.ConfusingTernary" })
|
||||
private void reconcileDisk(DynamicKubernetesObject vmDefinition,
|
||||
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.
|
||||
var vmName = vmDef.getAsString("metadata", "name").orElse("default");
|
||||
pvcDef.get(JsonObject.class).add("metadata",
|
||||
pvcTpl.to("metadata").get(JsonObject.class).deepCopy());
|
||||
var defMeta = pvcDef.to("metadata");
|
||||
defMeta.computeIfAbsent("namespace", () -> new JsonPrimitive(
|
||||
vmDef.getAsString("metadata", "namespace").orElse("default")));
|
||||
defMeta.computeIfAbsent("name", () -> new JsonPrimitive(
|
||||
vmName + "-disk-" + index));
|
||||
var pvcLbls = pvcDef.to("metadata", "labels");
|
||||
pvcLbls.set("app.kubernetes.io/name", APP_NAME);
|
||||
pvcLbls.set("app.kubernetes.io/instance", vmName);
|
||||
pvcLbls.set("app.kubernetes.io/component", "disk");
|
||||
pvcLbls.set("app.kubernetes.io/managed-by", VM_OP_NAME);
|
||||
|
||||
// Get API and check if PVC exists
|
||||
DynamicKubernetesApi pvcApi = new DynamicKubernetesApi("", "v1",
|
||||
"persistentvolumeclaims", channel.client());
|
||||
var existing = pvcApi.get(defMeta.getAsString("namespace").get(),
|
||||
defMeta.getAsString("name").get());
|
||||
|
||||
// If PVC does not exist, create. Else patch (apply)
|
||||
if (!existing.isSuccess()) {
|
||||
// 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").computeIfAbsent("volumeMode", "Block");
|
||||
pvcApi.create(pvcObject);
|
||||
} else {
|
||||
// spec is immutable, so mix in existing spec
|
||||
pvcDef.set("spec", GsonPtr.to(existing.getObject().getRaw())
|
||||
.to("spec").get().deepCopy());
|
||||
PatchOptions opts = new PatchOptions();
|
||||
opts.setForce(false);
|
||||
opts.setFieldManager("kubernetes-java-kubectl-apply");
|
||||
pvcApi.patch(pvcObject.getMetadata().getNamespace(),
|
||||
pvcObject.getMetadata().getName(),
|
||||
V1Patch.PATCH_FORMAT_APPLY_YAML,
|
||||
new V1Patch(channel.client().getJSON().serialize(pvcObject)),
|
||||
opts).throwsApiException();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@ public class VmWatcher extends Component {
|
|||
|
||||
private ApiClient client;
|
||||
private V1APIResource vmsCrd;
|
||||
private String managedNamespace = "default";
|
||||
private String managedNamespace = "qemu-vms";
|
||||
private final Map<String, WatchChannel> channels
|
||||
= new ConcurrentHashMap<>();
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
#
|
||||
#Fri Jul 21 17:39:36 CEST 2023
|
||||
#Mon Jul 24 15:40:37 CEST 2023
|
||||
org.eclipse.jdt.core.formatter.insert_space_after_ellipsis=insert
|
||||
org.eclipse.jdt.core.formatter.insert_space_after_comma_in_enum_declarations=insert
|
||||
org.eclipse.jdt.core.formatter.insert_new_line_in_empty_annotation_declaration=insert
|
||||
|
|
@ -22,12 +22,12 @@ org.eclipse.jdt.core.formatter.blank_lines_after_package=1
|
|||
org.eclipse.jdt.core.formatter.insert_space_after_comma_in_multiple_local_declarations=insert
|
||||
org.eclipse.jdt.core.formatter.alignment_for_arguments_in_enum_constant=16
|
||||
org.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_parameterized_type_reference=do not insert
|
||||
org.eclipse.jdt.core.formatter.wrap_before_or_operator_multicatch=true
|
||||
org.eclipse.jdt.core.formatter.comment.indent_root_tags=false
|
||||
org.eclipse.jdt.core.formatter.wrap_before_or_operator_multicatch=true
|
||||
org.eclipse.jdt.core.formatter.enabling_tag=@formatter\:on
|
||||
org.eclipse.jdt.core.formatter.comment.count_line_length_from_starting_position=false
|
||||
org.eclipse.jdt.core.formatter.alignment_for_throws_clause_in_method_declaration=16
|
||||
org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_parameter=do not insert
|
||||
org.eclipse.jdt.core.formatter.alignment_for_throws_clause_in_method_declaration=16
|
||||
org.eclipse.jdt.core.formatter.keep_then_statement_on_same_line=false
|
||||
org.eclipse.jdt.core.formatter.insert_space_after_comma_in_explicitconstructorcall_arguments=insert
|
||||
org.eclipse.jdt.core.formatter.insert_space_after_prefix_operator=do not insert
|
||||
|
|
@ -43,8 +43,8 @@ org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_method_invoc
|
|||
org.eclipse.jdt.core.formatter.insert_space_after_assignment_operator=insert
|
||||
org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_type_declaration=insert
|
||||
org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_for=do not insert
|
||||
org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_local_variable=insert
|
||||
org.eclipse.jdt.core.formatter.comment.preserve_white_space_between_code_and_line_comments=true
|
||||
org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_local_variable=insert
|
||||
org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_method_invocation=do not insert
|
||||
org.eclipse.jdt.core.formatter.alignment_for_union_type_in_multicatch=16
|
||||
org.eclipse.jdt.core.formatter.number_of_blank_lines_at_beginning_of_method_body=0
|
||||
|
|
@ -63,8 +63,8 @@ org.eclipse.jdt.core.formatter.alignment_for_type_parameters=16
|
|||
org.eclipse.jdt.core.compiler.problem.assertIdentifier=error
|
||||
org.eclipse.jdt.core.formatter.comment.clear_blank_lines_in_block_comment=false
|
||||
org.eclipse.jdt.core.formatter.insert_new_line_at_end_of_file_if_missing=do not insert
|
||||
org.eclipse.jdt.core.formatter.insert_space_after_comma_in_array_initializer=insert
|
||||
org.eclipse.jdt.core.formatter.insert_space_before_unary_operator=do not insert
|
||||
org.eclipse.jdt.core.formatter.insert_space_after_comma_in_array_initializer=insert
|
||||
org.eclipse.jdt.core.formatter.format_line_comment_starting_on_first_column=false
|
||||
org.eclipse.jdt.core.formatter.parentheses_positions_in_annotation=common_lines
|
||||
org.eclipse.jdt.core.formatter.insert_space_before_ellipsis=do not insert
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue