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

feat: bulk resource management #157

Merged
merged 4 commits into from
Jan 3, 2025
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
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
Loading