Skip to content

Commit

Permalink
feat: bulk resource management (#157)
Browse files Browse the repository at this point in the history

Signed-off-by: Attila Mészáros <[email protected]>
  • Loading branch information
csviri authored Jan 3, 2025
1 parent e955b3c commit c172f54
Show file tree
Hide file tree
Showing 15 changed files with 290 additions and 22 deletions.
18 changes: 14 additions & 4 deletions src/main/java/io/javaoperatorsdk/operator/glue/Utils.java
Original file line number Diff line number Diff line change
Expand Up @@ -138,17 +138,21 @@ public static String getKindFromTemplate(String resourceTemplate) {
public static Set<String> leafResourceNames(Glue glue) {
Set<String> result = new HashSet<>();
glue.getSpec().getChildResources().forEach(r -> result.add(r.getName()));
glue.getSpec().getChildResources().forEach(r -> {
r.getDependsOn().forEach(result::remove);
});
glue.getSpec().getChildResources().forEach(r -> r.getDependsOn().forEach(result::remove));
return result;
}

private static Optional<String> getOptionalPropertyValueFromTemplate(String resourceTemplate,
String property) {
var finalProp = property + ":";
var targetLine = resourceTemplate.lines().filter(l -> l.contains(finalProp)).findFirst();
return targetLine.map(l -> l.replace(finalProp, "").trim());
return targetLine.map(l -> {
int index = l.indexOf(finalProp);
if (index > 0) {
l = l.substring(index);
}
return l.replace(finalProp, "").trim();
});
}

private static String getPropertyValueFromTemplate(String resourceTemplate, String property) {
Expand All @@ -157,4 +161,10 @@ private static String getPropertyValueFromTemplate(String resourceTemplate, Stri
"Template does not contain property. " + resourceTemplate));
}

public static GroupVersionKind getGVKFromTemplate(String resourceTemplate) {
String apiVersion = getApiVersionFromTemplate(resourceTemplate);
String kind = getKindFromTemplate(resourceTemplate);
return new GroupVersionKind(apiVersion, kind);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ public class DependentResourceSpec {

private Matcher matcher = Matcher.SSA;

private Boolean bulk = false;

private List<String> dependsOn = new ArrayList<>();

@PreserveUnknownFields
Expand Down Expand Up @@ -102,6 +104,14 @@ public void setMatcher(Matcher matcher) {
this.matcher = matcher;
}

public Boolean getBulk() {
return bulk;
}

public void setBulk(Boolean bulk) {
this.bulk = bulk;
}

@Override
public boolean equals(Object o) {
if (this == o)
Expand All @@ -112,14 +122,14 @@ public boolean equals(Object o) {
return clusterScoped == that.clusterScoped && Objects.equals(name, that.name)
&& Objects.equals(resource, that.resource)
&& Objects.equals(resourceTemplate, that.resourceTemplate) && matcher == that.matcher
&& Objects.equals(dependsOn, that.dependsOn)
&& Objects.equals(bulk, that.bulk) && Objects.equals(dependsOn, that.dependsOn)
&& Objects.equals(readyPostCondition, that.readyPostCondition)
&& Objects.equals(condition, that.condition);
}

@Override
public int hashCode() {
return Objects.hash(name, clusterScoped, resource, resourceTemplate, matcher, dependsOn,
return Objects.hash(name, clusterScoped, resource, resourceTemplate, matcher, bulk, dependsOn,
readyPostCondition, condition);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package io.javaoperatorsdk.operator.glue.dependent;

import io.javaoperatorsdk.operator.api.reconciler.dependent.GarbageCollected;
import io.javaoperatorsdk.operator.glue.customresource.glue.Glue;
import io.javaoperatorsdk.operator.glue.customresource.glue.Matcher;
import io.javaoperatorsdk.operator.glue.templating.GenericTemplateHandler;

public class GCGenericBulkDependentResource extends GenericBulkDependentResource
implements GarbageCollected<Glue> {

public GCGenericBulkDependentResource(GenericTemplateHandler genericTemplateHandler,
String desiredTemplate, String name,
boolean clusterScoped, Matcher matcher) {
super(genericTemplateHandler, desiredTemplate, name, clusterScoped, matcher);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package io.javaoperatorsdk.operator.glue.dependent;

import java.util.Map;
import java.util.stream.Collectors;

import io.fabric8.kubernetes.api.model.GenericKubernetesResource;
import io.fabric8.kubernetes.api.model.GenericKubernetesResourceList;
import io.fabric8.kubernetes.client.utils.Serialization;
import io.javaoperatorsdk.operator.api.reconciler.Context;
import io.javaoperatorsdk.operator.glue.customresource.glue.Glue;
import io.javaoperatorsdk.operator.glue.customresource.glue.Matcher;
import io.javaoperatorsdk.operator.glue.templating.GenericTemplateHandler;
import io.javaoperatorsdk.operator.processing.dependent.BulkDependentResource;

import static io.javaoperatorsdk.operator.glue.reconciler.glue.GlueReconciler.DEPENDENT_NAME_ANNOTATION_KEY;

public class GenericBulkDependentResource extends
GenericDependentResource implements
BulkDependentResource<GenericKubernetesResource, Glue> {

public GenericBulkDependentResource(GenericTemplateHandler genericTemplateHandler,
String desiredTemplate, String name,
boolean clusterScoped,
Matcher matcher) {
super(genericTemplateHandler, desiredTemplate, name, clusterScoped, matcher);
}

@Override
public Map<String, GenericKubernetesResource> desiredResources(Glue primary,
Context<Glue> context) {

var res = genericTemplateHandler.processTemplate(desiredTemplate, primary, context);
var desiredList = Serialization.unmarshal(res, GenericKubernetesResourceList.class).getItems();
desiredList.forEach(r -> {
r.getMetadata().getAnnotations()
.put(DEPENDENT_NAME_ANNOTATION_KEY, name);
if (r.getMetadata().getNamespace() == null && !clusterScoped) {
r.getMetadata().setNamespace(primary.getMetadata().getNamespace());
}
});
return desiredList.stream().collect(Collectors.toMap(r -> r.getMetadata().getName(), r -> r));
}

@Override
public Map<String, GenericKubernetesResource> getSecondaryResources(Glue glue,
Context<Glue> context) {
return context.getSecondaryResources(GenericKubernetesResource.class).stream()
.filter(
r -> name.equals(r.getMetadata().getAnnotations().get(DEPENDENT_NAME_ANNOTATION_KEY)))
.collect(Collectors.toMap(r -> r.getMetadata().getName(), r -> r));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,13 @@ public class GenericDependentResource
Updater<GenericKubernetesResource, Glue>,
Creator<GenericKubernetesResource, Glue> {

private final GenericKubernetesResource desired;
private final String desiredTemplate;
private final String name;
private final boolean clusterScoped;
private final Matcher matcher;
protected final GenericKubernetesResource desired;
protected final String desiredTemplate;
protected final String name;
protected final boolean clusterScoped;
protected final Matcher matcher;

// optimize share between instances
private final GenericTemplateHandler genericTemplateHandler;
protected final GenericTemplateHandler genericTemplateHandler;

public GenericDependentResource(GenericTemplateHandler genericTemplateHandler,
GenericKubernetesResource desired, String name,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,20 @@ public class ValidationAndErrorHandler {
}
}

public void checkIfNamesAreUnique(GlueSpec glueSpec) {
public void checkIfValidGlueSpec(GlueSpec glueSpec) {
checkIfBulkProvidesResourceTemplate(glueSpec);
checkIfNamesAreUnique(glueSpec);
}

private void checkIfBulkProvidesResourceTemplate(GlueSpec glueSpec) {
glueSpec.getChildResources().forEach(r -> {
if (Boolean.TRUE.equals(r.getBulk()) && r.getResourceTemplate() == null) {
throw new GlueException("Bulk resource requires a template to be set");
}
});
}

void checkIfNamesAreUnique(GlueSpec glueSpec) {
Set<String> seen = new HashSet<>();
List<String> duplicates = new ArrayList<>();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,14 @@
import io.javaoperatorsdk.operator.glue.customresource.glue.condition.ConditionSpec;
import io.javaoperatorsdk.operator.glue.customresource.glue.condition.JavaScriptConditionSpec;
import io.javaoperatorsdk.operator.glue.customresource.glue.condition.ReadyConditionSpec;
import io.javaoperatorsdk.operator.glue.dependent.GCGenericBulkDependentResource;
import io.javaoperatorsdk.operator.glue.dependent.GCGenericDependentResource;
import io.javaoperatorsdk.operator.glue.dependent.GenericDependentResource;
import io.javaoperatorsdk.operator.glue.dependent.GenericResourceDiscriminator;
import io.javaoperatorsdk.operator.glue.reconciler.ValidationAndErrorHandler;
import io.javaoperatorsdk.operator.glue.reconciler.operator.GlueOperatorReconciler;
import io.javaoperatorsdk.operator.glue.templating.GenericTemplateHandler;
import io.javaoperatorsdk.operator.processing.dependent.BulkDependentResource;
import io.javaoperatorsdk.operator.processing.dependent.workflow.Condition;
import io.javaoperatorsdk.operator.processing.dependent.workflow.KubernetesResourceDeletedCondition;
import io.javaoperatorsdk.operator.processing.dependent.workflow.WorkflowBuilder;
Expand Down Expand Up @@ -78,7 +80,7 @@ public UpdateControl<Glue> reconcile(Glue primary,
log.debug("Reconciling glue. name: {} namespace: {}",
primary.getMetadata().getName(), primary.getMetadata().getNamespace());

validationAndErrorHandler.checkIfNamesAreUnique(primary.getSpec());
validationAndErrorHandler.checkIfValidGlueSpec(primary.getSpec());

registerRelatedResourceInformers(context, primary);
if (deletedGlueIfParentMarkedForDeletion(context, primary)) {
Expand Down Expand Up @@ -183,9 +185,11 @@ private void createAndAddDependentToWorkflow(Glue primary, Context<Glue> context
var dr = createDependentResource(spec, leafDependent, resourceInSameNamespaceAsPrimary);
var gvk = dr.getGroupVersionKind();

dr.setResourceDiscriminator(new GenericResourceDiscriminator(dr.getGroupVersionKind(),
genericTemplateHandler.processTemplate(Utils.getName(spec), primary, context),
targetNamespace.orElse(null)));
if (!(dr instanceof BulkDependentResource<?, ?>)) {
dr.setResourceDiscriminator(new GenericResourceDiscriminator(dr.getGroupVersionKind(),
genericTemplateHandler.processTemplate(Utils.getName(spec), primary, context),
targetNamespace.orElse(null)));
}

var es = informerRegister.registerInformer(context, gvk, primary);
dr.configureWith(es);
Expand All @@ -209,9 +213,14 @@ private GenericDependentResource createDependentResource(DependentResourceSpec s

if (leafDependent && resourceInSameNamespaceAsPrimary && !spec.isClusterScoped()) {
return spec.getResourceTemplate() != null
? new GCGenericDependentResource(genericTemplateHandler, spec.getResourceTemplate(),
spec.getName(),
spec.isClusterScoped(), spec.getMatcher())
? spec.getBulk()
? new GCGenericBulkDependentResource(genericTemplateHandler,
spec.getResourceTemplate(),
spec.getName(),
spec.isClusterScoped(), spec.getMatcher())
: new GCGenericDependentResource(genericTemplateHandler, spec.getResourceTemplate(),
spec.getName(),
spec.isClusterScoped(), spec.getMatcher())
: new GCGenericDependentResource(genericTemplateHandler, spec.getResource(),
spec.getName(),
spec.isClusterScoped(), spec.getMatcher());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ public UpdateControl<GlueOperator> reconcile(GlueOperator glueOperator,
log.info("Reconciling GlueOperator {} in namespace: {}", glueOperator.getMetadata().getName(),
glueOperator.getMetadata().getNamespace());

validationAndErrorHandler.checkIfNamesAreUnique(glueOperator.getSpec());
validationAndErrorHandler.checkIfValidGlueSpec(glueOperator.getSpec());

var targetCREventSource = getOrRegisterCustomResourceEventSource(glueOperator, context);
targetCREventSource.list().forEach(cr -> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import io.javaoperatorsdk.operator.glue.customresource.operator.GlueOperatorSpec;
import io.javaoperatorsdk.operator.glue.customresource.operator.Parent;
import io.javaoperatorsdk.operator.glue.reconciler.ValidationAndErrorHandler;
import io.javaoperatorsdk.operator.glue.reconciler.operator.GlueOperatorReconciler;
import io.quarkus.test.junit.QuarkusTest;

import static io.javaoperatorsdk.operator.glue.TestData.*;
Expand Down Expand Up @@ -229,6 +230,41 @@ void secretCopySample() {
});
}

@Test
void operatorWithBulkResource() {
var go = create(TestUtils
.loadGlueOperator("/glueoperator/BulkOperator.yaml"));

var cr = testCustomResource();
cr.getSpec().setReplicas(2);
var createdCR = create(cr);
assertConfigMapsCreated(cr, 2);

createdCR.getSpec().setReplicas(3);
createdCR = update(createdCR);
assertConfigMapsCreated(cr, 3);

createdCR.getSpec().setReplicas(1);
createdCR = update(createdCR);
assertConfigMapsCreated(cr, 1);

delete(createdCR);
assertConfigMapsCreated(cr, 0);
await().untilAsserted(() -> {
var actualCR = get(TestCustomResource.class, cr.getMetadata().getName());
assertThat(actualCR).isNull();
});

delete(go);
}

private void assertConfigMapsCreated(TestCustomResource cr, int expected) {
await().untilAsserted(() -> {
var configMaps = getRelatedList(ConfigMap.class,
GlueOperatorReconciler.glueName(cr.getMetadata().getName(), cr.getKind()));
assertThat(configMaps).hasSize(expected);
});
}

GlueOperator testWorkflowOperator() {
var wo = new GlueOperator();
Expand Down
19 changes: 19 additions & 0 deletions src/test/java/io/javaoperatorsdk/operator/glue/GlueTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -368,6 +368,25 @@ void customizeMatcher() {
});
}

@Test
void simpleBulk() {
var glue = createGlue("/glue/" + "SimpleBulk.yaml");

await().untilAsserted(() -> {
var configMaps = getRelatedList(ConfigMap.class, glue.getMetadata().getName());
assertThat(configMaps).hasSize(3);
assertThat(configMaps)
.allMatch(cm -> cm.getMetadata().getName().startsWith("simple-glue-configmap-"));
});

delete(glue);

await().untilAsserted(
() -> assertThat(getRelatedList(ConfigMap.class, glue.getMetadata().getName()).isEmpty()));
}



private List<Glue> testWorkflowList(int num) {
List<Glue> res = new ArrayList<>();
IntStream.range(0, num).forEach(index -> {
Expand Down
13 changes: 13 additions & 0 deletions src/test/java/io/javaoperatorsdk/operator/glue/TestBase.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package io.javaoperatorsdk.operator.glue;

import java.time.Duration;
import java.util.List;

import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
Expand Down Expand Up @@ -70,6 +71,18 @@ protected <T extends HasMetadata> T get(Class<T> clazz, String name) {
return client.resources(clazz).inNamespace(testNamespace).withName(name).get();
}

protected <T extends HasMetadata> List<T> list(Class<T> clazz) {
return client.resources(clazz).inNamespace(testNamespace).list().getItems();
}

protected <T extends HasMetadata> List<T> getRelatedList(Class<T> clazz, String ownerName) {
return list(clazz).stream()
.filter(cm -> !cm.getMetadata().getOwnerReferences().isEmpty()
&& cm.getMetadata().getOwnerReferences()
.get(0).getName().equals(ownerName))
.toList();
}

protected <T extends HasMetadata> T update(T resource) {
resource.getMetadata().setResourceVersion(null);
return client.resource(resource).inNamespace(testNamespace).update();
Expand Down
Loading

0 comments on commit c172f54

Please sign in to comment.