Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

use qute as template engine #41

Merged
merged 13 commits into from
Mar 31, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 0 additions & 6 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@
<fabric8-client.version>6.11.0</fabric8-client.version>
<graalvm.version>24.0.0</graalvm.version>
<mokito.version>5.11.0</mokito.version>
<mustache.version>0.9.11</mustache.version>
<qosdk.version>6.6.7</qosdk.version>
<assertj.version>3.25.3</assertj.version>
<spotless.version>2.43.0</spotless.version>
Expand Down Expand Up @@ -103,11 +102,6 @@
<type>pom</type>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>com.github.spullara.mustache.java</groupId>
<artifactId>compiler</artifactId>
<version>${mustache.version}</version>
</dependency>
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcpkix-jdk18on</artifactId>
Expand Down
71 changes: 55 additions & 16 deletions src/main/java/io/csviri/operator/resourceglue/Utils.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,25 @@

import java.util.*;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import io.csviri.operator.resourceglue.customresource.glue.DependentResourceSpec;
import io.csviri.operator.resourceglue.customresource.glue.Glue;
import io.csviri.operator.resourceglue.customresource.glue.RelatedResourceSpec;
import io.fabric8.kubernetes.api.model.GenericKubernetesResource;
import io.fabric8.kubernetes.api.model.ObjectMetaBuilder;
import io.javaoperatorsdk.operator.api.reconciler.Context;
import io.javaoperatorsdk.operator.processing.GroupVersionKind;
import io.javaoperatorsdk.operator.processing.event.ResourceID;
import io.javaoperatorsdk.operator.processing.event.source.informer.InformerEventSource;

import static io.csviri.operator.resourceglue.reconciler.glue.GlueReconciler.DEPENDENT_NAME_ANNOTATION_KEY;

public class Utils {

private static final Logger log = LoggerFactory.getLogger(Utils.class);

public static final String RESOURCE_NAME_DELIMITER = "#";

private Utils() {}
Expand All @@ -24,36 +33,66 @@ public static Map<String, GenericKubernetesResource> getActualResourcesByNameInW
var dependentSpec = glue.getSpec().getResources().stream()
.filter(r -> Utils.getApiVersion(r).equals(sr.getApiVersion())
&& Utils.getKind(r).equals(sr.getKind())
&& Utils.getName(r).equals(sr.getMetadata().getName())
// comparing the name from annotation since the resource name might be templated in spec
// therefore "Utils.getName(relatedResourceSpec).equals(sr.getMetadata().getName())" would not
// work
&& r.getName()
.equals(sr.getMetadata().getAnnotations().get(DEPENDENT_NAME_ANNOTATION_KEY))
// namespace not compared here, it should be done it is just not trivial, now it is limited to
// have one kind of resource in the workflow with the same resource name
).findFirst();
dependentSpec.ifPresent(spec -> res.put(spec.getName(), sr));
});

glue.getSpec().getRelatedResources().forEach(r -> {
var gvk = new GroupVersionKind(r.getApiVersion(), r.getKind());
var es =
(InformerEventSource<GenericKubernetesResource, Glue>) context
.eventSourceRetriever()
.getResourceEventSourceFor(GenericKubernetesResource.class, gvk.toString());
var namespace =
r.getNamespace() == null ? glue.getMetadata().getNamespace() : r.getNamespace();
if (r.getResourceNames().size() == 1) {
es.get(new ResourceID(r.getResourceNames().get(0), namespace)).ifPresentOrElse(resource -> {
res.put(r.getName(), resource);
}, () -> res.put(r.getName(), null));
var relatedResources = getRelatedResources(glue, r, context);
if (relatedResources.size() == 1) {
var resourceEntry = relatedResources.entrySet().iterator().next();
res.put(r.getName(), resourceEntry.getValue());

} else {
r.getResourceNames().forEach(resourceName -> es.get(new ResourceID(resourceName, namespace))
.ifPresentOrElse(
resource -> res.put(r.getName() + RESOURCE_NAME_DELIMITER + resourceName, resource),
() -> res.put(r.getName() + "#" + resourceName, null)));
relatedResources.forEach((resourceName, resource) -> res
.put(r.getName() + RESOURCE_NAME_DELIMITER + resourceName, resource));
}
});

return res;
}

@SuppressWarnings("unchecked")
public static Map<String, GenericKubernetesResource> getRelatedResources(Glue glue,
RelatedResourceSpec relatedResourceSpec,
Context<?> context) {
var gvk =
new GroupVersionKind(relatedResourceSpec.getApiVersion(), relatedResourceSpec.getKind());
log.debug("Getting event source for gvk: {}", gvk);
var es =
(InformerEventSource<GenericKubernetesResource, Glue>) context
.eventSourceRetriever()
.getResourceEventSourceFor(GenericKubernetesResource.class, gvk.toString());
var namespace =
relatedResourceSpec.getNamespace() == null ? glue.getMetadata().getNamespace()
: relatedResourceSpec.getNamespace();

var res = new HashMap<String, GenericKubernetesResource>();

relatedResourceSpec.getResourceNames()
.forEach(r -> res.put(r, es.get(new ResourceID(r, namespace)).orElse(null)));
return res;
}

public static GenericKubernetesResource getResourceForSSAFrom(
GenericKubernetesResource resourceFromServer) {
var res = new GenericKubernetesResource();
res.setKind(resourceFromServer.getKind());
res.setApiVersion(resourceFromServer.getApiVersion());
res.setMetadata(new ObjectMetaBuilder()
.withName(resourceFromServer.getMetadata().getName())
.withNamespace(resourceFromServer.getMetadata().getNamespace())
.build());
return res;
}

public static GroupVersionKind getGVK(DependentResourceSpec spec) {
return new GroupVersionKind(getApiVersion(spec), getKind(spec));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,10 @@
import io.csviri.operator.resourceglue.dependent.GenericResourceDiscriminator;
import io.csviri.operator.resourceglue.templating.GenericTemplateHandler;
import io.fabric8.kubernetes.api.model.GenericKubernetesResource;
import io.fabric8.kubernetes.api.model.HasMetadata;
import io.fabric8.kubernetes.client.KubernetesClientException;
import io.fabric8.kubernetes.client.dsl.base.PatchContext;
import io.fabric8.kubernetes.client.dsl.base.PatchType;
import io.javaoperatorsdk.operator.api.reconciler.*;
import io.javaoperatorsdk.operator.processing.GroupVersionKind;
import io.javaoperatorsdk.operator.processing.dependent.workflow.Condition;
Expand All @@ -27,24 +30,41 @@
import io.javaoperatorsdk.operator.processing.event.ResourceID;
import io.javaoperatorsdk.operator.processing.event.source.informer.InformerEventSource;

import static io.csviri.operator.resourceglue.Utils.getResourceForSSAFrom;
import static io.csviri.operator.resourceglue.reconciler.operator.GlueOperatorReconciler.PARENT_RELATED_RESOURCE_NAME;

@ControllerConfiguration
public class GlueReconciler implements Reconciler<Glue>, Cleaner<Glue> {

private static final Logger log = LoggerFactory.getLogger(GlueReconciler.class);
public static final String DEPENDENT_NAME_ANNOTATION_KEY = "io.csviri.operator.resourceflow/name";
public static final String PARENT_GLUE_FINALIZER_PREFIX = "io.csviri.operator.resourceflow.glue/";

private final KubernetesResourceDeletedCondition deletePostCondition =
new KubernetesResourceDeletedCondition();
private final InformerRegister informerRegister = new InformerRegister();
private final GenericTemplateHandler genericTemplateHandler = new GenericTemplateHandler();

/**
* Handling finalizers for GlueOperator: Glue ids a finalizer to parent, that is necessary since
* on clean up the resource name might be calculated based on the parents name, and it this way
* makes sure that parent is not cleaned up until the Glue is cleaned up. The finalizer is removed
* during cleanup. On Glue side however it is important to make sure that if the parent is deleted
* glue gets deleted too, this is made sure in the reconcile method for glue explicitly deleting
* itself.
*/

@Override
public UpdateControl<Glue> reconcile(Glue primary,
Context<Glue> context) {

log.debug("Reconciling glue. name: {} namespace: {}",
primary.getMetadata().getName(), primary.getMetadata().getNamespace());
registerRelatedResourceInformers(context, primary);
if (deletedGlueIfParentMarkedForDeletion(context, primary)) {
return UpdateControl.noUpdate();
}
addFinalizersToParentResource(primary, context);
if (ownersBeingDeleted(primary, context)) {
return UpdateControl.noUpdate();
}
Expand All @@ -56,6 +76,17 @@ public UpdateControl<Glue> reconcile(Glue primary,
return UpdateControl.noUpdate();
}

private boolean deletedGlueIfParentMarkedForDeletion(Context<Glue> context, Glue primary) {
var parent = getParentRelatedResource(primary, context);
if (parent.map(HasMetadata::isMarkedForDeletion).orElse(false)) {
context.getClient().resource(primary).delete();
return true;
} else {
return false;
}
}


/**
* If a parent gets deleted, the glue is reconciled still, but we don't want that in that case.
* Glue us deleted / marked for deleted eventually by the garbage collector but want to make the
Expand Down Expand Up @@ -84,9 +115,6 @@ private boolean ownersBeingDeleted(Glue primary, Context<Glue> context) {

@Override
public DeleteControl cleanup(Glue primary, Context<Glue> context) {
// todo if related resource referenced to name the resource (name / namespace) the related
// resource might be deleted
// at this point - shall we add finalizer?

var actualWorkflow = buildWorkflowAndRegisterInformers(primary, context);
var result = actualWorkflow.cleanup(primary, context);
Expand All @@ -95,6 +123,7 @@ public DeleteControl cleanup(Glue primary, Context<Glue> context) {
if (!result.allPostConditionsMet()) {
return DeleteControl.noFinalizerRemoval();
} else {
removeFinalizerForParent(primary, context);
actualWorkflow.getDependentResourcesByNameWithoutActivationCondition().forEach((n, dr) -> {
var genericDependentResource = (GenericDependentResource) dr;
informerRegister.deRegisterInformer(genericDependentResource.getGroupVersionKind(),
Expand Down Expand Up @@ -209,4 +238,72 @@ private Condition toCondition(ConditionSpec condition) {
throw new IllegalStateException("Unknown condition: " + condition);
}

// todo docs
private void addFinalizersToParentResource(Glue primary, Context<Glue> context) {
var parent = getParentRelatedResource(primary, context);

parent.ifPresent(p -> {
log.warn("Adding finalizer to parent. Glue name: {} namespace: {}",
primary.getMetadata().getName(), primary.getMetadata().getNamespace());
String finalizer = parentFinalizer(primary.getMetadata().getName());
if (!p.getMetadata().getFinalizers().contains(finalizer)) {
var res = getResourceForSSAFrom(p);
res.getMetadata().getFinalizers().add(finalizer);
context.getClient().resource(res)
.patch(new PatchContext.Builder()
.withFieldManager(context.getControllerConfiguration().fieldManager())
.withForce(true)
.withPatchType(PatchType.SERVER_SIDE_APPLY)
.build());
}
});
}

private void removeFinalizerForParent(Glue primary, Context<Glue> context) {
var parent = getParentRelatedResource(primary, context);
parent.ifPresentOrElse(p -> {
log.warn("Removing finalizer from parent. Glue name: {} namespace: {}",
primary.getMetadata().getName(), primary.getMetadata().getNamespace());
String finalizer = parentFinalizer(primary.getMetadata().getName());
if (p.getMetadata().getFinalizers().contains(finalizer)) {
var res = getResourceForSSAFrom(p);
context.getClient().resource(res)
.patch(new PatchContext.Builder()
.withFieldManager(context.getControllerConfiguration().fieldManager())
.withForce(true)
.withPatchType(PatchType.SERVER_SIDE_APPLY)
.build());
}
}, () -> log.warn(
"Parent resource expected to be present on cleanup. Glue name: {} namespace: {}",
primary.getMetadata().getName(), primary.getMetadata().getNamespace()));
}

private Optional<GenericKubernetesResource> getParentRelatedResource(Glue primary,
Context<Glue> context) {
var parentRelated = primary.getSpec().getRelatedResources().stream()
.filter(r -> r.getName().equals(PARENT_RELATED_RESOURCE_NAME))
.findAny();

return parentRelated.flatMap(r -> {
var relatedResources = Utils.getRelatedResources(primary, r, context);
if (relatedResources.size() > 1) {
throw new IllegalStateException(
"parent related resource contains more resourceNames for glue name: "
+ primary.getMetadata().getName()
+ " namespace: " + primary.getMetadata().getNamespace());
}
if (relatedResources.isEmpty()) {
return Optional.empty();
} else {
return Optional.of(relatedResources.entrySet().iterator().next().getValue());
}
});
}

private String parentFinalizer(String glueName) {
return PARENT_GLUE_FINALIZER_PREFIX + glueName;
}


}
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ class InformerRegister {

private final Map<GroupVersionKind, Set<String>> gvkOfInformerToGlue = new HashMap<>();
private final Map<String, Set<GroupVersionKind>> glueToInformerGVK = new HashMap<>();
private final Map<GroupVersionKind, RelatedResourceSecondaryToPrimaryMapper> relatedResourceMappers =
private final Map<GroupVersionKind, RelatedAndOwnedResourceSecondaryToPrimaryMapper> relatedResourceMappers =
new ConcurrentHashMap<>();

// todo test related resources deleting
Expand Down Expand Up @@ -71,9 +71,10 @@ public void registerInformerForRelatedResource(Context<Glue> context,
public InformerEventSource<GenericKubernetesResource, Glue> registerInformer(
Context<Glue> context, GroupVersionKind gvk, Glue glue) {

RelatedResourceSecondaryToPrimaryMapper mapper;
RelatedAndOwnedResourceSecondaryToPrimaryMapper mapper;
synchronized (this) {
relatedResourceMappers.putIfAbsent(gvk, new RelatedResourceSecondaryToPrimaryMapper());
relatedResourceMappers.putIfAbsent(gvk,
new RelatedAndOwnedResourceSecondaryToPrimaryMapper());
mapper = relatedResourceMappers.get(gvk);
markEventSource(gvk, glue);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
import io.javaoperatorsdk.operator.processing.event.source.SecondaryToPrimaryMapper;
import io.javaoperatorsdk.operator.processing.event.source.informer.Mappers;

public class RelatedResourceSecondaryToPrimaryMapper
public class RelatedAndOwnedResourceSecondaryToPrimaryMapper
implements SecondaryToPrimaryMapper<GenericKubernetesResource> {

private final Map<ResourceID, Set<ResourceID>> secondaryToPrimaryMap = new ConcurrentHashMap<>();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,46 +1,43 @@
package io.csviri.operator.resourceglue.templating;

import java.io.StringReader;
import java.io.StringWriter;
import java.util.HashMap;
import java.util.Map;

import io.csviri.operator.resourceglue.Utils;
import io.csviri.operator.resourceglue.customresource.glue.Glue;
import io.javaoperatorsdk.operator.api.reconciler.Context;
import io.quarkus.qute.Engine;
import io.quarkus.qute.Template;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.github.mustachejava.DefaultMustacheFactory;
import com.github.mustachejava.MustacheFactory;

public class GenericTemplateHandler {
private static final ObjectMapper objectMapper = new ObjectMapper();
private static final MustacheFactory mustacheFactory = new DefaultMustacheFactory();

public static final String WORKFLOW_METADATA_KEY = "glueMetadata";

private static final ObjectMapper objectMapper = new ObjectMapper();
private static final Engine engine = Engine.builder().addDefaults().build();

public String processTemplate(String template, Glue primary,
Context<Glue> context) {

// to precompile?
var mustache = mustacheFactory.compile(new StringReader(template), "desired");

var mustacheContext = createMustacheContextWithResources(primary, context);
return mustache.execute(new StringWriter(), mustacheContext).toString();
Template hello = engine.parse(template);
var data = createDataWithResources(primary, context);
return hello.data(data).render();
}

private static Map<String, Map> createMustacheContextWithResources(Glue primary,
@SuppressWarnings("rawtypes")
private static Map<String, Map> createDataWithResources(Glue primary,
Context<Glue> context) {
Map<String, Map> res = new HashMap<>();
var actualResourcesByName = Utils.getActualResourcesByNameInWorkflow(context, primary);

Map<String, Map> mustacheContext = new HashMap<>();

actualResourcesByName.entrySet().stream().forEach(e -> mustacheContext.put(e.getKey(),
e.getValue() == null ? null : objectMapper.convertValue(e.getValue(), Map.class)));
actualResourcesByName.forEach((key, value) -> res.put(key,
value == null ? null : objectMapper.convertValue(value, Map.class)));

mustacheContext.put(WORKFLOW_METADATA_KEY,
res.put(WORKFLOW_METADATA_KEY,
objectMapper.convertValue(primary.getMetadata(), Map.class));

return mustacheContext;
return res;
}
}
1 change: 1 addition & 0 deletions src/main/resources/application.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
quarkus.application.name=resource-glue-operator
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ void templating() {

delete(cr);

await().untilAsserted(() -> {
await().timeout(Duration.ofMinutes(5)).untilAsserted(() -> {
var cm1 = get(ConfigMap.class, name);
var actualCR = get(TestCustomResource.class, name);
assertThat(cm1).isNull();
Expand Down
Loading