Avoid unnecessary config map changes.

This commit is contained in:
Michael Lipp 2025-03-14 11:21:29 +01:00
parent c02b3d99cb
commit 5947bd3684
4 changed files with 62 additions and 46 deletions

View file

@ -11,7 +11,7 @@ metadata:
annotations:
# Triggers update of config map mounted in pod
# See https://ahmet.im/blog/kubernetes-secret-volumes-delay/
vmrunner.jdrupes.org/cmVersion: "${ cm.metadata.resourceVersion }"
vmrunner.jdrupes.org/cmVersion: "${ configMapResourceVersion }"
vmoperator.jdrupes.org/version: ${ managerVersion }
ownerReferences:
- apiVersion: ${ cr.apiVersion() }

View file

@ -19,11 +19,17 @@
package org.jdrupes.vmoperator.manager;
import com.google.gson.JsonObject;
import freemarker.template.AdapterTemplateModel;
import freemarker.template.Configuration;
import freemarker.template.TemplateException;
import freemarker.template.TemplateMethodModelEx;
import freemarker.template.TemplateModel;
import freemarker.template.TemplateModelException;
import freemarker.template.utility.DeepUnwrap;
import io.kubernetes.client.custom.V1Patch;
import io.kubernetes.client.openapi.ApiClient;
import io.kubernetes.client.openapi.ApiException;
import io.kubernetes.client.openapi.models.V1ObjectMeta;
import io.kubernetes.client.util.generic.dynamic.DynamicKubernetesApi;
import io.kubernetes.client.util.generic.dynamic.DynamicKubernetesObject;
import io.kubernetes.client.util.generic.dynamic.Dynamics;
@ -31,7 +37,11 @@ import io.kubernetes.client.util.generic.options.ListOptions;
import io.kubernetes.client.util.generic.options.PatchOptions;
import java.io.IOException;
import java.io.StringWriter;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.logging.Logger;
import org.jdrupes.vmoperator.common.K8s;
import static org.jdrupes.vmoperator.manager.Constants.APP_NAME;
@ -66,48 +76,59 @@ import org.yaml.snakeyaml.constructor.SafeConstructor;
*
* @param model the model
* @param channel the channel
* @return the dynamic kubernetes object
* @throws IOException Signals that an I/O exception has occurred.
* @throws TemplateException the template exception
* @throws ApiException the api exception
*/
public Map<String, Object> reconcile(Map<String, Object> model,
VmChannel channel)
@SuppressWarnings("PMD.AvoidDuplicateLiterals")
public void reconcile(Map<String, Object> model, VmChannel channel)
throws IOException, TemplateException, ApiException {
// Combine template and data and parse result
model.put("adjustCloudInitMeta", adjustCloudInitMetaModel);
var fmTemplate = fmConfig.getTemplate("runnerConfig.ftl.yaml");
StringWriter out = new StringWriter();
fmTemplate.process(model, out);
// Avoid Yaml.load due to
// https://github.com/kubernetes-client/java/issues/2741
var mapDef = Dynamics.newFromYaml(
var newCm = Dynamics.newFromYaml(
new Yaml(new SafeConstructor(new LoaderOptions())), out.toString());
// Maybe override logging.properties from reconciler configuration.
DataPath.<String> get(model, "reconciler", "loggingProperties")
.ifPresent(props -> {
GsonPtr.to(mapDef.getRaw()).getAs(JsonObject.class, "data")
GsonPtr.to(newCm.getRaw()).getAs(JsonObject.class, "data")
.get().addProperty("logging.properties", props);
});
// Maybe override logging.properties from VM definition.
DataPath.<String> get(model, "cr", "spec", "loggingProperties")
.ifPresent(props -> {
GsonPtr.to(mapDef.getRaw()).getAs(JsonObject.class, "data")
GsonPtr.to(newCm.getRaw()).getAs(JsonObject.class, "data")
.get().addProperty("logging.properties", props);
});
// Get API
// Look for changes
var oldCm = channel
.associated(getClass(), DynamicKubernetesObject.class).orElse(null);
channel.setAssociated(getClass(), newCm);
if (oldCm != null && Objects.equals(oldCm.getRaw().get("data"),
newCm.getRaw().get("data"))) {
logger.finer(() -> "No changes in config map for "
+ DataPath.<String> get(model, "cr", "name").get());
model.put("configMapResourceVersion",
oldCm.getMetadata().getResourceVersion());
return;
}
// Get API and update
DynamicKubernetesApi cmApi = new DynamicKubernetesApi("", "v1",
"configmaps", channel.client());
// Apply and maybe force pod update
var newState = K8s.apply(cmApi, mapDef, mapDef.getRaw().toString());
maybeForceUpdate(channel.client(), newState);
@SuppressWarnings("unchecked")
var res = (Map<String, Object>) channel.client().getJSON().getGson()
.fromJson(newState.getRaw(), Map.class);
return res;
var updatedCm = K8s.apply(cmApi, newCm, newCm.getRaw().toString());
maybeForceUpdate(channel.client(), updatedCm);
model.put("configMapResourceVersion",
updatedCm.getMetadata().getResourceVersion());
}
/**
@ -153,4 +174,28 @@ import org.yaml.snakeyaml.constructor.SafeConstructor;
}
}
private final TemplateMethodModelEx adjustCloudInitMetaModel
= new TemplateMethodModelEx() {
@Override
@SuppressWarnings("PMD.PreserveStackTrace")
public Object exec(@SuppressWarnings("rawtypes") List arguments)
throws TemplateModelException {
@SuppressWarnings("unchecked")
var res = new HashMap<>((Map<String, Object>) DeepUnwrap
.unwrap((TemplateModel) arguments.get(0)));
var metadata
= (V1ObjectMeta) ((AdapterTemplateModel) arguments.get(1))
.getAdaptedObject(Object.class);
if (!res.containsKey("instance-id")) {
res.put("instance-id",
Optional.ofNullable(metadata.getGeneration())
.map(s -> "v" + s).orElse("v1"));
}
if (!res.containsKey("local-hostname")) {
res.put("local-hostname", metadata.getName());
}
return res;
}
};
}

View file

@ -27,12 +27,9 @@ import freemarker.template.SimpleScalar;
import freemarker.template.TemplateException;
import freemarker.template.TemplateExceptionHandler;
import freemarker.template.TemplateMethodModelEx;
import freemarker.template.TemplateModel;
import freemarker.template.TemplateModelException;
import freemarker.template.utility.DeepUnwrap;
import io.kubernetes.client.custom.Quantity;
import io.kubernetes.client.openapi.ApiException;
import io.kubernetes.client.openapi.models.V1ObjectMeta;
import io.kubernetes.client.util.generic.options.ListOptions;
import java.io.IOException;
import java.lang.reflect.Modifier;
@ -226,13 +223,12 @@ public class Reconciler extends Component {
// Create model for processing templates
Map<String, Object> model
= prepareModel(channel.client(), event.vmDefinition());
var configMap = cmReconciler.reconcile(model, channel);
cmReconciler.reconcile(model, channel);
// The remaining reconcilers depend only on changes of the spec part.
if (!event.specChanged()) {
return;
}
model.put("cm", configMap);
dsReconciler.reconcile(event, model, channel);
// Manage (eventual) removal of stateful set.
stsReconciler.reconcile(event, model, channel);
@ -279,7 +275,6 @@ public class Reconciler extends Component {
model.put("parseQuantity", parseQuantityModel);
model.put("formatMemory", formatMemoryModel);
model.put("imageLocation", imgageLocationModel);
model.put("adjustCloudInitMeta", adjustCloudInitMetaModel);
model.put("toJson", toJsonModel);
return model;
}
@ -422,30 +417,6 @@ public class Reconciler extends Component {
}
};
private final TemplateMethodModelEx adjustCloudInitMetaModel
= new TemplateMethodModelEx() {
@Override
@SuppressWarnings("PMD.PreserveStackTrace")
public Object exec(@SuppressWarnings("rawtypes") List arguments)
throws TemplateModelException {
@SuppressWarnings("unchecked")
var res = new HashMap<>((Map<String, Object>) DeepUnwrap
.unwrap((TemplateModel) arguments.get(0)));
var metadata
= (V1ObjectMeta) ((AdapterTemplateModel) arguments.get(1))
.getAdaptedObject(Object.class);
if (!res.containsKey("instance-id")) {
res.put("instance-id",
Optional.ofNullable(metadata.getResourceVersion())
.map(s -> "v" + s).orElse("v1"));
}
if (!res.containsKey("local-hostname")) {
res.put("local-hostname", metadata.getName());
}
return res;
}
};
private final TemplateMethodModelEx toJsonModel
= new TemplateMethodModelEx() {
@Override