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:
|
properties:
|
||||||
hostDevice:
|
hostDevice:
|
||||||
type: string
|
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:
|
bootindex:
|
||||||
type: integer
|
type: integer
|
||||||
displays:
|
displays:
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
apiVersion: "vmoperator.jdrupes.org/v1"
|
apiVersion: "vmoperator.jdrupes.org/v1"
|
||||||
kind: VirtualMachine
|
kind: VirtualMachine
|
||||||
metadata:
|
metadata:
|
||||||
|
namespace: qemu-vms
|
||||||
name: test-vm
|
name: test-vm
|
||||||
spec:
|
spec:
|
||||||
image:
|
image:
|
||||||
|
|
@ -18,7 +19,13 @@ spec:
|
||||||
- tap:
|
- tap:
|
||||||
mac: "00:16:3e:33:59:10"
|
mac: "00:16:3e:33:59:10"
|
||||||
disks:
|
disks:
|
||||||
- hostDevice: /dev/vgmain/test-vm
|
- volumeClaimTemplate:
|
||||||
|
spec:
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
storage: 40Gi
|
||||||
|
|
||||||
|
# - hostDevice: /dev/vgmain/test-vm
|
||||||
displays:
|
displays:
|
||||||
- spice:
|
- spice:
|
||||||
port: 5910
|
port: 5910
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,8 @@ dependencies {
|
||||||
implementation project(':org.jdrupes.vmoperator.util')
|
implementation project(':org.jdrupes.vmoperator.util')
|
||||||
|
|
||||||
implementation 'commons-cli:commons-cli:1.5.0'
|
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)'
|
runtimeOnly 'org.slf4j:slf4j-jdk14:[2.0.7,3)'
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -23,8 +23,18 @@ package org.jdrupes.vmoperator.manager;
|
||||||
*/
|
*/
|
||||||
public class Constants {
|
public class Constants {
|
||||||
|
|
||||||
static final String VM_OP_GROUP = "vmoperator.jdrupes.org";
|
/** The Constant VM_OP_NAME. */
|
||||||
static final String VM_OP_VERSION = "v1";
|
public static final String VM_OP_NAME = "vm-operator";
|
||||||
static final String VM_OP_KIND_VM = "VirtualMachine";
|
|
||||||
|
|
||||||
|
/** 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;
|
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.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.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_GROUP;
|
||||||
|
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.jgrapes.core.Channel;
|
import org.jgrapes.core.Channel;
|
||||||
import org.jgrapes.core.Component;
|
import org.jgrapes.core.Component;
|
||||||
|
|
@ -29,6 +39,7 @@ 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")
|
||||||
public class Reconciler extends Component {
|
public class Reconciler extends Component {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -46,6 +57,7 @@ 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 KubectlException
|
||||||
*/
|
*/
|
||||||
@Handler
|
@Handler
|
||||||
public void onVmDefChanged(VmDefChanged event, WatchChannel channel)
|
public void onVmDefChanged(VmDefChanged event, WatchChannel channel)
|
||||||
|
|
@ -53,28 +65,75 @@ public class Reconciler extends Component {
|
||||||
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();
|
||||||
|
|
||||||
// DynamicKubernetesApi cmApi = new DynamicKubernetesApi("", "v1",
|
@SuppressWarnings("PMD.AvoidDuplicateLiterals")
|
||||||
// "configmaps", channel.client());
|
var disks = GsonPtr.to(vmDef.getRaw())
|
||||||
// var cm = new DynamicKubernetesObject();
|
.get(JsonArray.class, "spec", "vm", "disks")
|
||||||
// cm.setApiVersion("v1");
|
.map(JsonArray::asList).orElse(Collections.emptyList());
|
||||||
// cm.setKind("ConfigMap");
|
int index = 0;
|
||||||
// V1ObjectMeta metadata = new V1ObjectMeta();
|
for (var disk : disks) {
|
||||||
// metadata.setNamespace("default");
|
reconcileDisk(vmDef, index++, (JsonObject) disk, channel);
|
||||||
// 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();
|
|
||||||
|
|
||||||
// var obj = channel.coa().getNamespacedCustomObject(VM_OP_GROUP, VM_OP_VERSION,
|
@SuppressWarnings({ "PMD.AvoidDuplicateLiterals", "PMD.ConfusingTernary" })
|
||||||
// md.getNamespace(), event.crd().getName(), md.getName());
|
private void reconcileDisk(DynamicKubernetesObject vmDefinition,
|
||||||
event = null;
|
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 ApiClient client;
|
||||||
private V1APIResource vmsCrd;
|
private V1APIResource vmsCrd;
|
||||||
private String managedNamespace = "default";
|
private String managedNamespace = "qemu-vms";
|
||||||
private final Map<String, WatchChannel> channels
|
private final Map<String, WatchChannel> channels
|
||||||
= new ConcurrentHashMap<>();
|
= 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_ellipsis=insert
|
||||||
org.eclipse.jdt.core.formatter.insert_space_after_comma_in_enum_declarations=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
|
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.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.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.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.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.enabling_tag=@formatter\:on
|
||||||
org.eclipse.jdt.core.formatter.comment.count_line_length_from_starting_position=false
|
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.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.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_comma_in_explicitconstructorcall_arguments=insert
|
||||||
org.eclipse.jdt.core.formatter.insert_space_after_prefix_operator=do not 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_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_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_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.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.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.alignment_for_union_type_in_multicatch=16
|
||||||
org.eclipse.jdt.core.formatter.number_of_blank_lines_at_beginning_of_method_body=0
|
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.compiler.problem.assertIdentifier=error
|
||||||
org.eclipse.jdt.core.formatter.comment.clear_blank_lines_in_block_comment=false
|
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_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_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.format_line_comment_starting_on_first_column=false
|
||||||
org.eclipse.jdt.core.formatter.parentheses_positions_in_annotation=common_lines
|
org.eclipse.jdt.core.formatter.parentheses_positions_in_annotation=common_lines
|
||||||
org.eclipse.jdt.core.formatter.insert_space_before_ellipsis=do not insert
|
org.eclipse.jdt.core.formatter.insert_space_before_ellipsis=do not insert
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue