Skip to content

Commit c172f54

Browse files
authored
feat: bulk resource management (#157)
Signed-off-by: Attila Mészáros <[email protected]>
1 parent e955b3c commit c172f54

File tree

15 files changed

+290
-22
lines changed

15 files changed

+290
-22
lines changed

src/main/java/io/javaoperatorsdk/operator/glue/Utils.java

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -138,17 +138,21 @@ public static String getKindFromTemplate(String resourceTemplate) {
138138
public static Set<String> leafResourceNames(Glue glue) {
139139
Set<String> result = new HashSet<>();
140140
glue.getSpec().getChildResources().forEach(r -> result.add(r.getName()));
141-
glue.getSpec().getChildResources().forEach(r -> {
142-
r.getDependsOn().forEach(result::remove);
143-
});
141+
glue.getSpec().getChildResources().forEach(r -> r.getDependsOn().forEach(result::remove));
144142
return result;
145143
}
146144

147145
private static Optional<String> getOptionalPropertyValueFromTemplate(String resourceTemplate,
148146
String property) {
149147
var finalProp = property + ":";
150148
var targetLine = resourceTemplate.lines().filter(l -> l.contains(finalProp)).findFirst();
151-
return targetLine.map(l -> l.replace(finalProp, "").trim());
149+
return targetLine.map(l -> {
150+
int index = l.indexOf(finalProp);
151+
if (index > 0) {
152+
l = l.substring(index);
153+
}
154+
return l.replace(finalProp, "").trim();
155+
});
152156
}
153157

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

164+
public static GroupVersionKind getGVKFromTemplate(String resourceTemplate) {
165+
String apiVersion = getApiVersionFromTemplate(resourceTemplate);
166+
String kind = getKindFromTemplate(resourceTemplate);
167+
return new GroupVersionKind(apiVersion, kind);
168+
}
169+
160170
}

src/main/java/io/javaoperatorsdk/operator/glue/customresource/glue/DependentResourceSpec.java

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ public class DependentResourceSpec {
2323

2424
private Matcher matcher = Matcher.SSA;
2525

26+
private Boolean bulk = false;
27+
2628
private List<String> dependsOn = new ArrayList<>();
2729

2830
@PreserveUnknownFields
@@ -102,6 +104,14 @@ public void setMatcher(Matcher matcher) {
102104
this.matcher = matcher;
103105
}
104106

107+
public Boolean getBulk() {
108+
return bulk;
109+
}
110+
111+
public void setBulk(Boolean bulk) {
112+
this.bulk = bulk;
113+
}
114+
105115
@Override
106116
public boolean equals(Object o) {
107117
if (this == o)
@@ -112,14 +122,14 @@ public boolean equals(Object o) {
112122
return clusterScoped == that.clusterScoped && Objects.equals(name, that.name)
113123
&& Objects.equals(resource, that.resource)
114124
&& Objects.equals(resourceTemplate, that.resourceTemplate) && matcher == that.matcher
115-
&& Objects.equals(dependsOn, that.dependsOn)
125+
&& Objects.equals(bulk, that.bulk) && Objects.equals(dependsOn, that.dependsOn)
116126
&& Objects.equals(readyPostCondition, that.readyPostCondition)
117127
&& Objects.equals(condition, that.condition);
118128
}
119129

120130
@Override
121131
public int hashCode() {
122-
return Objects.hash(name, clusterScoped, resource, resourceTemplate, matcher, dependsOn,
132+
return Objects.hash(name, clusterScoped, resource, resourceTemplate, matcher, bulk, dependsOn,
123133
readyPostCondition, condition);
124134
}
125135
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package io.javaoperatorsdk.operator.glue.dependent;
2+
3+
import io.javaoperatorsdk.operator.api.reconciler.dependent.GarbageCollected;
4+
import io.javaoperatorsdk.operator.glue.customresource.glue.Glue;
5+
import io.javaoperatorsdk.operator.glue.customresource.glue.Matcher;
6+
import io.javaoperatorsdk.operator.glue.templating.GenericTemplateHandler;
7+
8+
public class GCGenericBulkDependentResource extends GenericBulkDependentResource
9+
implements GarbageCollected<Glue> {
10+
11+
public GCGenericBulkDependentResource(GenericTemplateHandler genericTemplateHandler,
12+
String desiredTemplate, String name,
13+
boolean clusterScoped, Matcher matcher) {
14+
super(genericTemplateHandler, desiredTemplate, name, clusterScoped, matcher);
15+
}
16+
17+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
package io.javaoperatorsdk.operator.glue.dependent;
2+
3+
import java.util.Map;
4+
import java.util.stream.Collectors;
5+
6+
import io.fabric8.kubernetes.api.model.GenericKubernetesResource;
7+
import io.fabric8.kubernetes.api.model.GenericKubernetesResourceList;
8+
import io.fabric8.kubernetes.client.utils.Serialization;
9+
import io.javaoperatorsdk.operator.api.reconciler.Context;
10+
import io.javaoperatorsdk.operator.glue.customresource.glue.Glue;
11+
import io.javaoperatorsdk.operator.glue.customresource.glue.Matcher;
12+
import io.javaoperatorsdk.operator.glue.templating.GenericTemplateHandler;
13+
import io.javaoperatorsdk.operator.processing.dependent.BulkDependentResource;
14+
15+
import static io.javaoperatorsdk.operator.glue.reconciler.glue.GlueReconciler.DEPENDENT_NAME_ANNOTATION_KEY;
16+
17+
public class GenericBulkDependentResource extends
18+
GenericDependentResource implements
19+
BulkDependentResource<GenericKubernetesResource, Glue> {
20+
21+
public GenericBulkDependentResource(GenericTemplateHandler genericTemplateHandler,
22+
String desiredTemplate, String name,
23+
boolean clusterScoped,
24+
Matcher matcher) {
25+
super(genericTemplateHandler, desiredTemplate, name, clusterScoped, matcher);
26+
}
27+
28+
@Override
29+
public Map<String, GenericKubernetesResource> desiredResources(Glue primary,
30+
Context<Glue> context) {
31+
32+
var res = genericTemplateHandler.processTemplate(desiredTemplate, primary, context);
33+
var desiredList = Serialization.unmarshal(res, GenericKubernetesResourceList.class).getItems();
34+
desiredList.forEach(r -> {
35+
r.getMetadata().getAnnotations()
36+
.put(DEPENDENT_NAME_ANNOTATION_KEY, name);
37+
if (r.getMetadata().getNamespace() == null && !clusterScoped) {
38+
r.getMetadata().setNamespace(primary.getMetadata().getNamespace());
39+
}
40+
});
41+
return desiredList.stream().collect(Collectors.toMap(r -> r.getMetadata().getName(), r -> r));
42+
}
43+
44+
@Override
45+
public Map<String, GenericKubernetesResource> getSecondaryResources(Glue glue,
46+
Context<Glue> context) {
47+
return context.getSecondaryResources(GenericKubernetesResource.class).stream()
48+
.filter(
49+
r -> name.equals(r.getMetadata().getAnnotations().get(DEPENDENT_NAME_ANNOTATION_KEY)))
50+
.collect(Collectors.toMap(r -> r.getMetadata().getName(), r -> r));
51+
}
52+
}

src/main/java/io/javaoperatorsdk/operator/glue/dependent/GenericDependentResource.java

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -20,14 +20,13 @@ public class GenericDependentResource
2020
Updater<GenericKubernetesResource, Glue>,
2121
Creator<GenericKubernetesResource, Glue> {
2222

23-
private final GenericKubernetesResource desired;
24-
private final String desiredTemplate;
25-
private final String name;
26-
private final boolean clusterScoped;
27-
private final Matcher matcher;
23+
protected final GenericKubernetesResource desired;
24+
protected final String desiredTemplate;
25+
protected final String name;
26+
protected final boolean clusterScoped;
27+
protected final Matcher matcher;
2828

29-
// optimize share between instances
30-
private final GenericTemplateHandler genericTemplateHandler;
29+
protected final GenericTemplateHandler genericTemplateHandler;
3130

3231
public GenericDependentResource(GenericTemplateHandler genericTemplateHandler,
3332
GenericKubernetesResource desired, String name,

src/main/java/io/javaoperatorsdk/operator/glue/reconciler/ValidationAndErrorHandler.java

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,20 @@ public class ValidationAndErrorHandler {
4242
}
4343
}
4444

45-
public void checkIfNamesAreUnique(GlueSpec glueSpec) {
45+
public void checkIfValidGlueSpec(GlueSpec glueSpec) {
46+
checkIfBulkProvidesResourceTemplate(glueSpec);
47+
checkIfNamesAreUnique(glueSpec);
48+
}
49+
50+
private void checkIfBulkProvidesResourceTemplate(GlueSpec glueSpec) {
51+
glueSpec.getChildResources().forEach(r -> {
52+
if (Boolean.TRUE.equals(r.getBulk()) && r.getResourceTemplate() == null) {
53+
throw new GlueException("Bulk resource requires a template to be set");
54+
}
55+
});
56+
}
57+
58+
void checkIfNamesAreUnique(GlueSpec glueSpec) {
4659
Set<String> seen = new HashSet<>();
4760
List<String> duplicates = new ArrayList<>();
4861

src/main/java/io/javaoperatorsdk/operator/glue/reconciler/glue/GlueReconciler.java

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,14 @@
2121
import io.javaoperatorsdk.operator.glue.customresource.glue.condition.ConditionSpec;
2222
import io.javaoperatorsdk.operator.glue.customresource.glue.condition.JavaScriptConditionSpec;
2323
import io.javaoperatorsdk.operator.glue.customresource.glue.condition.ReadyConditionSpec;
24+
import io.javaoperatorsdk.operator.glue.dependent.GCGenericBulkDependentResource;
2425
import io.javaoperatorsdk.operator.glue.dependent.GCGenericDependentResource;
2526
import io.javaoperatorsdk.operator.glue.dependent.GenericDependentResource;
2627
import io.javaoperatorsdk.operator.glue.dependent.GenericResourceDiscriminator;
2728
import io.javaoperatorsdk.operator.glue.reconciler.ValidationAndErrorHandler;
2829
import io.javaoperatorsdk.operator.glue.reconciler.operator.GlueOperatorReconciler;
2930
import io.javaoperatorsdk.operator.glue.templating.GenericTemplateHandler;
31+
import io.javaoperatorsdk.operator.processing.dependent.BulkDependentResource;
3032
import io.javaoperatorsdk.operator.processing.dependent.workflow.Condition;
3133
import io.javaoperatorsdk.operator.processing.dependent.workflow.KubernetesResourceDeletedCondition;
3234
import io.javaoperatorsdk.operator.processing.dependent.workflow.WorkflowBuilder;
@@ -78,7 +80,7 @@ public UpdateControl<Glue> reconcile(Glue primary,
7880
log.debug("Reconciling glue. name: {} namespace: {}",
7981
primary.getMetadata().getName(), primary.getMetadata().getNamespace());
8082

81-
validationAndErrorHandler.checkIfNamesAreUnique(primary.getSpec());
83+
validationAndErrorHandler.checkIfValidGlueSpec(primary.getSpec());
8284

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

186-
dr.setResourceDiscriminator(new GenericResourceDiscriminator(dr.getGroupVersionKind(),
187-
genericTemplateHandler.processTemplate(Utils.getName(spec), primary, context),
188-
targetNamespace.orElse(null)));
188+
if (!(dr instanceof BulkDependentResource<?, ?>)) {
189+
dr.setResourceDiscriminator(new GenericResourceDiscriminator(dr.getGroupVersionKind(),
190+
genericTemplateHandler.processTemplate(Utils.getName(spec), primary, context),
191+
targetNamespace.orElse(null)));
192+
}
189193

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

210214
if (leafDependent && resourceInSameNamespaceAsPrimary && !spec.isClusterScoped()) {
211215
return spec.getResourceTemplate() != null
212-
? new GCGenericDependentResource(genericTemplateHandler, spec.getResourceTemplate(),
213-
spec.getName(),
214-
spec.isClusterScoped(), spec.getMatcher())
216+
? spec.getBulk()
217+
? new GCGenericBulkDependentResource(genericTemplateHandler,
218+
spec.getResourceTemplate(),
219+
spec.getName(),
220+
spec.isClusterScoped(), spec.getMatcher())
221+
: new GCGenericDependentResource(genericTemplateHandler, spec.getResourceTemplate(),
222+
spec.getName(),
223+
spec.isClusterScoped(), spec.getMatcher())
215224
: new GCGenericDependentResource(genericTemplateHandler, spec.getResource(),
216225
spec.getName(),
217226
spec.isClusterScoped(), spec.getMatcher());

src/main/java/io/javaoperatorsdk/operator/glue/reconciler/operator/GlueOperatorReconciler.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ public UpdateControl<GlueOperator> reconcile(GlueOperator glueOperator,
7575
log.info("Reconciling GlueOperator {} in namespace: {}", glueOperator.getMetadata().getName(),
7676
glueOperator.getMetadata().getNamespace());
7777

78-
validationAndErrorHandler.checkIfNamesAreUnique(glueOperator.getSpec());
78+
validationAndErrorHandler.checkIfValidGlueSpec(glueOperator.getSpec());
7979

8080
var targetCREventSource = getOrRegisterCustomResourceEventSource(glueOperator, context);
8181
targetCREventSource.list().forEach(cr -> {

src/test/java/io/javaoperatorsdk/operator/glue/GlueOperatorTest.java

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import io.javaoperatorsdk.operator.glue.customresource.operator.GlueOperatorSpec;
2020
import io.javaoperatorsdk.operator.glue.customresource.operator.Parent;
2121
import io.javaoperatorsdk.operator.glue.reconciler.ValidationAndErrorHandler;
22+
import io.javaoperatorsdk.operator.glue.reconciler.operator.GlueOperatorReconciler;
2223
import io.quarkus.test.junit.QuarkusTest;
2324

2425
import static io.javaoperatorsdk.operator.glue.TestData.*;
@@ -229,6 +230,41 @@ void secretCopySample() {
229230
});
230231
}
231232

233+
@Test
234+
void operatorWithBulkResource() {
235+
var go = create(TestUtils
236+
.loadGlueOperator("/glueoperator/BulkOperator.yaml"));
237+
238+
var cr = testCustomResource();
239+
cr.getSpec().setReplicas(2);
240+
var createdCR = create(cr);
241+
assertConfigMapsCreated(cr, 2);
242+
243+
createdCR.getSpec().setReplicas(3);
244+
createdCR = update(createdCR);
245+
assertConfigMapsCreated(cr, 3);
246+
247+
createdCR.getSpec().setReplicas(1);
248+
createdCR = update(createdCR);
249+
assertConfigMapsCreated(cr, 1);
250+
251+
delete(createdCR);
252+
assertConfigMapsCreated(cr, 0);
253+
await().untilAsserted(() -> {
254+
var actualCR = get(TestCustomResource.class, cr.getMetadata().getName());
255+
assertThat(actualCR).isNull();
256+
});
257+
258+
delete(go);
259+
}
260+
261+
private void assertConfigMapsCreated(TestCustomResource cr, int expected) {
262+
await().untilAsserted(() -> {
263+
var configMaps = getRelatedList(ConfigMap.class,
264+
GlueOperatorReconciler.glueName(cr.getMetadata().getName(), cr.getKind()));
265+
assertThat(configMaps).hasSize(expected);
266+
});
267+
}
232268

233269
GlueOperator testWorkflowOperator() {
234270
var wo = new GlueOperator();

src/test/java/io/javaoperatorsdk/operator/glue/GlueTest.java

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -368,6 +368,25 @@ void customizeMatcher() {
368368
});
369369
}
370370

371+
@Test
372+
void simpleBulk() {
373+
var glue = createGlue("/glue/" + "SimpleBulk.yaml");
374+
375+
await().untilAsserted(() -> {
376+
var configMaps = getRelatedList(ConfigMap.class, glue.getMetadata().getName());
377+
assertThat(configMaps).hasSize(3);
378+
assertThat(configMaps)
379+
.allMatch(cm -> cm.getMetadata().getName().startsWith("simple-glue-configmap-"));
380+
});
381+
382+
delete(glue);
383+
384+
await().untilAsserted(
385+
() -> assertThat(getRelatedList(ConfigMap.class, glue.getMetadata().getName()).isEmpty()));
386+
}
387+
388+
389+
371390
private List<Glue> testWorkflowList(int num) {
372391
List<Glue> res = new ArrayList<>();
373392
IntStream.range(0, num).forEach(index -> {

0 commit comments

Comments
 (0)