diff --git a/.vscode/launch.json b/.vscode/launch.json
index b6d2ca5..070f376 100644
--- a/.vscode/launch.json
+++ b/.vscode/launch.json
@@ -24,7 +24,7 @@
"mainClass": "org.jdrupes.vmoperator.runner.qemu.Runner",
"projectName": "org.jdrupes.vmoperator.runner.qemu",
"cwd": "${workspaceFolder}/org.jdrupes.vmoperator.runner.qemu",
- "vmArgs": "-ea -Djava.util.logging.config.file=jul-debug.properties -Dorg.jdrupes.vmoperator.runner.qemu.config=./config.yaml"
+ "vmArgs": "-ea -Djava.util.logging.manager=org.jdrupes.vmoperator.util.LongLoggingManager"
}
]
}
\ No newline at end of file
diff --git a/deploy/crds/vmoperator-crd.yaml b/deploy/crds/vmoperator-crd.yaml
index 134845f..b5792b9 100644
--- a/deploy/crds/vmoperator-crd.yaml
+++ b/deploy/crds/vmoperator-crd.yaml
@@ -17,6 +17,8 @@ spec:
type: object
properties:
image:
+ description: >-
+ The image to use for the pod. Must run a runner.
type: object
properties:
repository:
@@ -34,25 +36,71 @@ spec:
default: "IfNotPresent"
vm:
type: object
+ description: Defines the VM.
properties:
- name:
- type: string
machineUuid:
+ description: >-
+ 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
+ explicitly or to carefully backup the data
+ directory.
type: string
host:
+ description: The host to run this vm on.
type: string
useTpm:
+ description: Whether to provide a software TPM.
type: boolean
default: false
firmware:
+ description: >-
+ How to boot.
type: string
+ enum: ["bios", "uefi", "uefi-4m", "secure", "secure-4m"]
default: "uefi"
+ bootMenu:
+ description: Whether to show a boot menu.
+ type: boolean
+ default: false
+ powerdownTimeout:
+ description: >-
+ When terminating, a graceful powerdown is attempted.
+ If it doesn't succeed within the given timeout
+ (seconds) SIGTERM is sent to Qemu.
+ type: integer
+ default: 900
cpuModel:
+ description: Any model supported by Qemu.
type: string
default: "host"
maximumCpus:
+ description: >-
+ Either maximumCpus or cpuTopology may be specified.
+ If currentCpus is greater than maximumCpus, the
+ latter is adjusted. Setting maximumCpus to 1 omits
+ the "-smp" options.
type: integer
default: 4
+ cpuTopology:
+ description: >-
+ The defaults (0) cause the corresponding property
+ to be omitted from the "-smp" option.
+ type: object
+ properties:
+ sockets:
+ type: integer
+ default: 0
+ diesPerSocket:
+ type: integer
+ default: 0
+ coresPerSocket:
+ type: integer
+ default: 0
+ threadsPerSocket:
+ type: integer
+ default: 0
currentCpus:
type: integer
default: 2
@@ -62,25 +110,73 @@ spec:
currentRam:
type: string
rtcBase:
+ description: Passed to Qemu unmodified.
type: string
default: "utc"
- spicePort:
- type: integer
networks:
type: array
items:
+ description: >-
+ Supported types are "tap" and "user" (for debugging).
type: object
properties:
- bridge:
+ tap:
type: object
properties:
- name:
+ device:
+ description: The device to use.
+ type: string
+ default: "virtio-net"
+ bridge:
+ description: The bridge to attach to.
type: string
default: "br0"
mac:
type: string
- required:
- - name
+ user:
+ type: object
+ properties:
+ net:
+ type: string
+ oneOf:
+ - properties:
+ tap:
+ user:
+ disks:
+ description: >-
+ Disks make persistent storage available. The
+ storage may be provided by a device on the
+ host (preallocated, e.g. a LV).
+ type: array
+ items:
+ type: object
+ properties:
+ hostDevice:
+ type: string
+ bootindex:
+ type: integer
+ displays:
+ type: array
+ items:
+ type: object
+ properties:
+ spice:
+ type: object
+ properties:
+ port:
+ type: integer
+ default: 5900
+ ticket:
+ type: string
+ streamingVideo:
+ type: string
+ usbRedirects:
+ type: integer
+ default: 2
+ oneOf:
+ - properties:
+ maximumCpus:
+ cpuTopology:
required:
- vm
# either Namespaced or Cluster
@@ -91,4 +187,5 @@ spec:
# singular name to be used as an alias on the CLI and for display
singular: vm
# kind is normally the CamelCased singular type. Your resource manifests use this.
- kind: Vm
+ kind: VirtualMachine
+ listKind: VirtualMachineList
diff --git a/deploy/test-vm.yaml b/deploy/test-vm.yaml
index c4fa347..e964360 100644
--- a/deploy/test-vm.yaml
+++ b/deploy/test-vm.yaml
@@ -1,19 +1,24 @@
-image:
- repository: docker-registry.lan.mnl.de
- path: vmoperator/org.jdrupes.vmoperator.runner.qemu-arch
- pullPolicy: Always
+apiVersion: "vmoperator.jdrupes.org/v1"
+kind: VirtualMachine
+metadata:
+ name: test-vm
+spec:
+ image:
+ repository: docker-registry.lan.mnl.de
+ path: vmoperator/org.jdrupes.vmoperator.runner.qemu-arch
+ pullPolicy: Always
-vm:
- maximumCpus: 4
- currentCpus: 4
- maximumMemory: "8 GiB"
- currentMemory: "4 GiB"
- spicePort: 5910
+ vm:
+ maximumCpus: 4
+ currentCpus: 4
+ maximumRam: "8 GiB"
+ currentRam: "4 GiB"
- # Currently only block devices are supported as VM disks
- disks:
- - device: /dev/vgmain/test-vm
- size: 40Gi
- networks:
- - bridge:
- mac: "00:16:3e:33:59:10"
+ networks:
+ - tap:
+ mac: "00:16:3e:33:59:10"
+ disks:
+ - hostDevice: /dev/vgmain/test-vm
+ displays:
+ - spice:
+ port: 5910
diff --git a/org.jdrupes.vmoperator.manager/build.gradle b/org.jdrupes.vmoperator.manager/build.gradle
index e950486..7a96ae3 100644
--- a/org.jdrupes.vmoperator.manager/build.gradle
+++ b/org.jdrupes.vmoperator.manager/build.gradle
@@ -18,6 +18,8 @@ dependencies {
implementation 'commons-cli:commons-cli:1.5.0'
implementation 'io.kubernetes:client-java:18.0.0'
+
+ runtimeOnly 'org.slf4j:slf4j-jdk14:[2.0.7,3)'
}
application {
diff --git a/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/Manager.java b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/Manager.java
index 7b074fd..2e58469 100644
--- a/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/Manager.java
+++ b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/Manager.java
@@ -18,17 +18,6 @@
package org.jdrupes.vmoperator.manager;
-import io.kubernetes.client.openapi.ApiClient;
-import io.kubernetes.client.openapi.ApiException;
-import io.kubernetes.client.openapi.Configuration;
-import io.kubernetes.client.openapi.apis.CoreV1Api;
-import io.kubernetes.client.openapi.apis.CustomObjectsApi;
-import io.kubernetes.client.openapi.models.V1Pod;
-import io.kubernetes.client.openapi.models.V1PodList;
-import io.kubernetes.client.util.Config;
-import io.kubernetes.client.util.Yaml;
-
-import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
@@ -38,48 +27,38 @@ import org.jgrapes.core.Channel;
import org.jgrapes.core.Component;
import org.jgrapes.core.Components;
import org.jgrapes.core.annotation.Handler;
-import org.jgrapes.core.events.Start;
import org.jgrapes.core.events.Stop;
import org.jgrapes.io.NioDispatcher;
+/**
+ * The application class.
+ */
public class Manager extends Component {
+ /** The Constant APP_NAME. */
public static final String APP_NAME = "vmoperator";
private static Manager app;
+ /**
+ * Instantiates a new manager.
+ *
+ * @throws IOException Signals that an I/O exception has occurred.
+ */
public Manager() throws IOException {
- // Attach a general nio dispatcher
+ // Prepare component tree
attach(new NioDispatcher());
+ attach(new VmDefinitionWatcher(channel()));
+ attach(new Reconciliator(channel()));
}
/**
- * Handle the start event.
+ * On stop.
*
* @param event the event
- * @throws IOException
- * @throws ApiException
*/
- @Handler
- public void onStart(Start event) throws IOException, ApiException {
- ApiClient client = Config.defaultClient();
- Configuration.setDefaultApiClient(client);
-
- CoreV1Api api = new CoreV1Api();
- V1PodList list = api.listPodForAllNamespaces(null, null, null, null,
- null, null, null, null, null, null);
- for (V1Pod item : list.getItems()) {
- System.out.println(item.getMetadata().getName());
- }
-
-// CustomObjectsApi cApi = new CustomObjectsApi();
-// var obj = cApi.getNamespacedCustomObject("vmoperator.jdrupes.org", "v1",
-// "default", "vms", "test");
-// obj = null;
- }
-
- @Handler
+ @Handler(priority = -1000)
public void onStop(Stop event) {
- System.out.println("(Done.)");
+ logger.fine(() -> "Applictaion stopped.");
}
static {
diff --git a/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/Reconciliator.java b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/Reconciliator.java
new file mode 100644
index 0000000..6d9b3e1
--- /dev/null
+++ b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/Reconciliator.java
@@ -0,0 +1,18 @@
+package org.jdrupes.vmoperator.manager;
+
+import org.jgrapes.core.Channel;
+import org.jgrapes.core.Component;
+import org.jgrapes.core.annotation.Handler;
+
+public class Reconciliator extends Component {
+
+ public Reconciliator(Channel componentChannel) {
+ super(componentChannel);
+ }
+
+ @Handler
+ public void onVmChanged(VmChangedEvent event, WatchChannel channel) {
+ event = null;
+ }
+
+}
diff --git a/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/VmChangedEvent.java b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/VmChangedEvent.java
new file mode 100644
index 0000000..507916d
--- /dev/null
+++ b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/VmChangedEvent.java
@@ -0,0 +1,83 @@
+/*
+ * JGrapes Event Driven Framework
+ * Copyright (C) 2018 Michael N. Lipp
+ *
+ * This program is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU 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 General Public License
+ * for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, see .
+ */
+
+package org.jdrupes.vmoperator.manager;
+
+import io.kubernetes.client.openapi.models.V1ObjectMeta;
+
+import org.jgrapes.core.Channel;
+import org.jgrapes.core.Components;
+import org.jgrapes.core.Event;
+
+/**
+ * Indicates a change in a VM definition.
+ */
+public class VmChangedEvent extends Event {
+
+ /**
+ * The type of change.
+ */
+ public enum Type {
+ ADDED, MODIFIED, DELETED
+ }
+
+ private final Type type;
+ private final V1ObjectMeta metadata;
+
+ /**
+ * Instantiates a new VM changed event.
+ *
+ * @param type the type
+ * @param metadata the metadata
+ */
+ public VmChangedEvent(Type type, V1ObjectMeta metadata) {
+ this.type = type;
+ this.metadata = metadata;
+ }
+
+ /**
+ * Returns the type.
+ *
+ * @return the type
+ */
+ public Type type() {
+ return type;
+ }
+
+ /**
+ * Returns the metadata.
+ *
+ * @return the metadata
+ */
+ public V1ObjectMeta metadata() {
+ return metadata;
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder builder = new StringBuilder();
+ builder.append(Components.objectName(this)).append(" [").append(type)
+ .append(' ').append(metadata.getName());
+ if (channels() != null) {
+ builder.append(", channels=");
+ builder.append(Channel.toString(channels()));
+ }
+ builder.append(']');
+ return builder.toString();
+ }
+}
diff --git a/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/VmDefinitionWatcher.java b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/VmDefinitionWatcher.java
new file mode 100644
index 0000000..1260800
--- /dev/null
+++ b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/VmDefinitionWatcher.java
@@ -0,0 +1,131 @@
+/*
+ * JGrapes Event Driven Framework
+ * Copyright (C) 2018 Michael N. Lipp
+ *
+ * This program is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU 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 General Public License
+ * for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, see .
+ */
+
+package org.jdrupes.vmoperator.manager;
+
+import com.google.gson.reflect.TypeToken;
+import io.kubernetes.client.openapi.ApiClient;
+import io.kubernetes.client.openapi.ApiException;
+import io.kubernetes.client.openapi.Configuration;
+import io.kubernetes.client.openapi.apis.CoreV1Api;
+import io.kubernetes.client.openapi.apis.CustomObjectsApi;
+import io.kubernetes.client.openapi.models.V1APIResource;
+import io.kubernetes.client.openapi.models.V1Namespace;
+import io.kubernetes.client.openapi.models.V1ObjectMeta;
+import io.kubernetes.client.util.Config;
+import io.kubernetes.client.util.Watch;
+import java.io.IOException;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.logging.Level;
+import okhttp3.Call;
+import org.jdrupes.vmoperator.manager.VmChangedEvent.Type;
+import org.jgrapes.core.Channel;
+import org.jgrapes.core.Component;
+import org.jgrapes.core.annotation.Handler;
+import org.jgrapes.core.events.Start;
+import org.jgrapes.core.events.Stop;
+
+/**
+ * Watches for changes of VM definitions.
+ */
+@SuppressWarnings("PMD.DataflowAnomalyAnalysis")
+public class VmDefinitionWatcher extends Component {
+
+ private static final String CR_GROUP = "vmoperator.jdrupes.org";
+ private static final String CR_VERSION = "v1";
+ private static final String CR_KIND = "VirtualMachine";
+ private CoreV1Api api;
+ private CustomObjectsApi coa;
+ private V1APIResource vmsCrd;
+ private String managedNamespace = "default";
+ private final Map channels
+ = new ConcurrentHashMap<>();
+
+ /**
+ * Instantiates a new VM definition watcher.
+ *
+ * @param componentChannel the component channel
+ */
+ public VmDefinitionWatcher(Channel componentChannel) {
+ super(componentChannel);
+ }
+
+ /**
+ * Handle the start event.
+ *
+ * @param event the event
+ * @throws IOException
+ * @throws ApiException
+ */
+ @Handler
+ public void onStart(Start event) throws IOException, ApiException {
+ ApiClient client = Config.defaultClient();
+ Configuration.setDefaultApiClient(client);
+
+ // Get access to APIs
+ api = new CoreV1Api();
+ coa = new CustomObjectsApi();
+
+ // Derive all information from the CRD
+ var resources = coa.getAPIResources(CR_GROUP, CR_VERSION);
+ vmsCrd = resources.getResources().stream()
+ .filter(r -> CR_KIND.equals(r.getKind())).findFirst().get();
+
+ // Watch the resources (vm definitions)
+ Call call = coa.listNamespacedCustomObjectCall(
+ CR_GROUP, CR_VERSION, managedNamespace, vmsCrd.getName(), null,
+ false, null, null, null, null, null, null, null, true, null);
+ new Thread(() -> {
+ try (Watch watch = Watch.createWatch(client,
+ call, new TypeToken>() {
+ }.getType())) {
+ for (Watch.Response item : watch) {
+ handleCrEvent(item);
+ }
+ } catch (IOException | ApiException e) {
+ logger.log(Level.FINE, e, () -> "Probem while watching: "
+ + e.getMessage());
+ }
+ fire(new Stop());
+
+ }).start();
+ }
+
+ private void handleCrEvent(Watch.Response item) {
+ V1ObjectMeta metadata = item.object.getMetadata();
+ WatchChannel channel = channels.computeIfAbsent(metadata.getName(),
+ k -> new WatchChannel(channel(), newEventPipeline(), api, coa));
+ channel.pipeline().fire(new VmChangedEvent(
+ VmChangedEvent.Type.valueOf(item.type), metadata), channel);
+ }
+
+ /**
+ * Remove VM channel when VM is deleted.
+ *
+ * @param event the event
+ * @param channel the channel
+ */
+ @Handler(priority = -10_000)
+ public void onVmChanged(VmChangedEvent event, WatchChannel channel) {
+ if (event.type() == Type.DELETED) {
+ channels.remove(event.metadata().getName());
+ }
+ }
+
+}
diff --git a/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/WatchChannel.java b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/WatchChannel.java
new file mode 100644
index 0000000..c5d1924
--- /dev/null
+++ b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/WatchChannel.java
@@ -0,0 +1,78 @@
+/*
+ * JGrapes Event Driven Framework
+ * Copyright (C) 2018 Michael N. Lipp
+ *
+ * This program is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU 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 General Public License
+ * for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, see .
+ */
+
+package org.jdrupes.vmoperator.manager;
+
+import io.kubernetes.client.openapi.apis.CoreV1Api;
+import io.kubernetes.client.openapi.apis.CustomObjectsApi;
+import org.jgrapes.core.Channel;
+import org.jgrapes.core.EventPipeline;
+import org.jgrapes.core.Subchannel.DefaultSubchannel;
+
+/**
+ * A subchannel used to send the events related to a specific
+ * VM.
+ */
+public class WatchChannel extends DefaultSubchannel {
+
+ private final EventPipeline pipeline;
+ private final CoreV1Api api;
+ private final CustomObjectsApi coa;
+
+ /**
+ * Instantiates a new watch channel.
+ *
+ * @param mainChannel the main channel
+ * @param pipeline the pipeline
+ */
+ public WatchChannel(Channel mainChannel, EventPipeline pipeline,
+ CoreV1Api api, CustomObjectsApi coa) {
+ super(mainChannel);
+ this.pipeline = pipeline;
+ this.api = api;
+ this.coa = coa;
+ }
+
+ /**
+ * Returns the pipeline.
+ *
+ * @return the event pipeline
+ */
+ public EventPipeline pipeline() {
+ return pipeline;
+ }
+
+ /**
+ * Returns the API object for invoking kubernetes functions.
+ *
+ * @return the API object
+ */
+ public CoreV1Api api() {
+ return api;
+ }
+
+ /**
+ * Returns the API object for invoking kubernetes custom object
+ * functions.
+ *
+ * @return the API object
+ */
+ public CustomObjectsApi coa() {
+ return coa;
+ }
+}
diff --git a/org.jdrupes.vmoperator.util/.settings/org.eclipse.jdt.core.prefs b/org.jdrupes.vmoperator.util/.settings/org.eclipse.jdt.core.prefs
index 4748889..0dab961 100644
--- a/org.jdrupes.vmoperator.util/.settings/org.eclipse.jdt.core.prefs
+++ b/org.jdrupes.vmoperator.util/.settings/org.eclipse.jdt.core.prefs
@@ -1,5 +1,5 @@
#
-#Tue Jun 13 14:18:19 CEST 2023
+#Fri Jul 21 17:39:36 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.comment.indent_root_tags=false
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.enabling_tag=@formatter\:on
org.eclipse.jdt.core.formatter.comment.count_line_length_from_starting_position=false
-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.insert_new_line_after_annotation_on_parameter=do not insert
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.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.comment.preserve_white_space_between_code_and_line_comments=true
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_before_unary_operator=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.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