Working PVC for disks reconciliation.

This commit is contained in:
Michael Lipp 2023-07-25 12:26:42 +02:00
parent f1b1b2c059
commit d1410183cb
8 changed files with 625 additions and 30 deletions

View file

@ -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:

View file

@ -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

View file

@ -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)'
}

View file

@ -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";
}

View file

@ -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;
}
}

View file

@ -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();
}
}
}

View file

@ -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<>();

View file

@ -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