Skip to content

Commit 60537e6

Browse files
authored
feat: status updates (#147)
Signed-off-by: Attila Mészáros <[email protected]>
1 parent c794ea9 commit 60537e6

File tree

18 files changed

+249
-24
lines changed

18 files changed

+249
-24
lines changed

README.md

+5-1
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,9 @@ In other words, it also allows you to write **workflows** over resources in a **
1717

1818
## Contact Us
1919

20-
Either in the discussion section here on GitHub or at [Kubernetes Slack Operator Channel](https://kubernetes.slack.com/archives/CAW0GV7A5).
20+
Either in the discussion section here on GitHub or at [Kubernetes Slack Operator Channel](https://kubernetes.slack.com/archives/CAW0GV7A5). While
21+
in "object" form only placeholder substitutions are possible, in string template you can use all the
22+
features of qute.
2123

2224
## Quick Introduction
2325

@@ -61,6 +63,8 @@ spec:
6163
parent:
6264
apiVersion: glueoperator.sample/v1 # watches all the custom resource of type WebPage
6365
kind: WebPage
66+
statusTemplate: | # update the status of the custom resource at the end of reconciliation
67+
observedGeneration: {parent.metadata.generation}
6468
childResources:
6569
- name: htmlconfigmap
6670
resource:

docs/reference.md

+10
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@ of [Java Operator SDK](https://github.com/operator-framework/java-operator-sdk)
66
Although it is limited only to Kubernetes resources it makes it very easy to use in language-independent
77
(DependentResources in JOSDK are also covering external resources) way.
88

9+
## Generic Notes
10+
11+
- All templates (both object and string based) uses [Qute templating engine](https://quarkus.io/guides/qute-reference).
12+
913
## [Glue resource](https://github.com/java-operator-sdk/kubernetes-glue-operator/releases/latest/download/glues.glue-v1.yml)
1014

1115
`Glue` is the heart of the operator. Note that `GlueOperator` controller just creates a new `Glue` with a related resource,
@@ -57,6 +61,9 @@ The following attributes can be defined for a related resource:
5761
- **`apiVersion`** - Kubernetes resource API Version of the resource
5862
- **`kind`** - Kubernetes kind property of the resource
5963
- **`resourceNames`** - list of string of the resource names within the same namespace as `Glue`.
64+
- **`statusPatch`** - template object used to update status of the related resource at the end of the reconciliation. See [sample](https://github.com/java-operator-sdk/kubernetes-glue-operator/blob/main/src/test/resources/glue/PatchRelatedStatus.yaml#L20-L21).
65+
All the available resources (child, related) are provided.
66+
- **`statusPatchTemplate`** - same as `statusPatch` just as a string template. See [sample](https://github.com/java-operator-sdk/kubernetes-glue-operator/blob/main/src/test/resources/glue/PatchRelatedStatusWithTemplate.yaml#L20-L21).
6067

6168
### Referencing other resources
6269

@@ -91,6 +98,9 @@ The specs of `GlueOperator` are almost identical to `Glue`, it just adds some ad
9198
- **`apiVersion`** and **`kind`** - of the target custom resources.
9299
- **`labelSelector`** - optional label selector for the target resources.
93100
- **`clusterScoped`** - optional boolean value, if the parent resource is cluster scoped. Default is `false`.
101+
- **`status`** - template object to update status of the related resource at the end of the reconciliation.
102+
All the available resources (parent, child, related) are available.
103+
- **`statusTemplate`** - same as `status` just as a string template.
94104
- **`glueMetadata`** - optionally, you can customize the `Glue` resource created for each parent resource.
95105
This is especially important when the parent is a cluster scoped resource - in that case it is mandatory to set.
96106
Using this you can specify the **`name`** and **`namespace`** of the created `Glue`.

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

+26-4
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import java.util.List;
44
import java.util.Objects;
55

6+
import io.fabric8.crd.generator.annotation.PreserveUnknownFields;
67
import io.fabric8.generator.annotation.Required;
78

89
public class RelatedResourceSpec {
@@ -19,6 +20,9 @@ public class RelatedResourceSpec {
1920
private boolean clusterScoped = Boolean.FALSE;
2021
private List<String> resourceNames;
2122

23+
@PreserveUnknownFields
24+
private Object statusPatch;
25+
private String statusPatchTemplate;
2226

2327
public String getApiVersion() {
2428
return apiVersion;
@@ -73,14 +77,16 @@ public boolean equals(Object o) {
7377
return false;
7478
RelatedResourceSpec that = (RelatedResourceSpec) o;
7579
return clusterScoped == that.clusterScoped && Objects.equals(name, that.name)
76-
&& Objects.equals(apiVersion, that.apiVersion) && Objects.equals(kind, that.kind)
77-
&& Objects.equals(namespace, that.namespace)
78-
&& Objects.equals(resourceNames, that.resourceNames);
80+
&& Objects.equals(namespace, that.namespace) && Objects.equals(apiVersion, that.apiVersion)
81+
&& Objects.equals(kind, that.kind) && Objects.equals(resourceNames, that.resourceNames)
82+
&& Objects.equals(statusPatch, that.statusPatch)
83+
&& Objects.equals(statusPatchTemplate, that.statusPatchTemplate);
7984
}
8085

8186
@Override
8287
public int hashCode() {
83-
return Objects.hash(name, apiVersion, kind, clusterScoped, namespace, resourceNames);
88+
return Objects.hash(name, namespace, apiVersion, kind, clusterScoped, resourceNames,
89+
statusPatch, statusPatchTemplate);
8490
}
8591

8692
public boolean isClusterScoped() {
@@ -90,4 +96,20 @@ public boolean isClusterScoped() {
9096
public void setClusterScoped(boolean clusterScoped) {
9197
this.clusterScoped = clusterScoped;
9298
}
99+
100+
public Object getStatusPatch() {
101+
return statusPatch;
102+
}
103+
104+
public void setStatusPatch(Object statusPatch) {
105+
this.statusPatch = statusPatch;
106+
}
107+
108+
public String getStatusPatchTemplate() {
109+
return statusPatchTemplate;
110+
}
111+
112+
public void setStatusPatchTemplate(String statusPatchTemplate) {
113+
this.statusPatchTemplate = statusPatchTemplate;
114+
}
93115
}

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

+25-2
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,18 @@
22

33
import java.util.Objects;
44

5+
import io.fabric8.crd.generator.annotation.PreserveUnknownFields;
6+
57
public class Parent {
68

79
private String apiVersion;
810
private String kind;
911
private boolean clusterScoped = false;
1012
private String labelSelector;
1113

14+
@PreserveUnknownFields
15+
private Object status;
16+
private String statusTemplate;
1217

1318
public Parent() {}
1419

@@ -51,6 +56,22 @@ public void setClusterScoped(boolean clusterScoped) {
5156
this.clusterScoped = clusterScoped;
5257
}
5358

59+
public Object getStatus() {
60+
return status;
61+
}
62+
63+
public void setStatus(Object status) {
64+
this.status = status;
65+
}
66+
67+
public String getStatusTemplate() {
68+
return statusTemplate;
69+
}
70+
71+
public void setStatusTemplate(String statusTemplate) {
72+
this.statusTemplate = statusTemplate;
73+
}
74+
5475
@Override
5576
public boolean equals(Object o) {
5677
if (this == o)
@@ -59,11 +80,13 @@ public boolean equals(Object o) {
5980
return false;
6081
Parent parent = (Parent) o;
6182
return clusterScoped == parent.clusterScoped && Objects.equals(apiVersion, parent.apiVersion)
62-
&& Objects.equals(kind, parent.kind) && Objects.equals(labelSelector, parent.labelSelector);
83+
&& Objects.equals(kind, parent.kind) && Objects.equals(labelSelector, parent.labelSelector)
84+
&& Objects.equals(status, parent.status)
85+
&& Objects.equals(statusTemplate, parent.statusTemplate);
6386
}
6487

6588
@Override
6689
public int hashCode() {
67-
return Objects.hash(apiVersion, kind, clusterScoped, labelSelector);
90+
return Objects.hash(apiVersion, kind, clusterScoped, labelSelector, status, statusTemplate);
6891
}
6992
}

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

+31
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import io.fabric8.kubernetes.client.KubernetesClientException;
1111
import io.fabric8.kubernetes.client.dsl.base.PatchContext;
1212
import io.fabric8.kubernetes.client.dsl.base.PatchType;
13+
import io.fabric8.kubernetes.client.utils.Serialization;
1314
import io.javaoperatorsdk.operator.api.reconciler.*;
1415
import io.javaoperatorsdk.operator.glue.Utils;
1516
import io.javaoperatorsdk.operator.glue.conditions.JavaScripCondition;
@@ -89,6 +90,7 @@ public UpdateControl<Glue> reconcile(Glue primary,
8990
cleanupRemovedResourcesFromWorkflow(context, primary);
9091
informerRegister.deRegisterInformerOnResourceFlowChange(context, primary);
9192
result.throwAggregateExceptionIfErrorsPresent();
93+
patchRelatedResourcesStatus(context, primary);
9294
return UpdateControl.noUpdate();
9395
}
9496

@@ -222,6 +224,35 @@ private GenericDependentResource createDependentResource(DependentResourceSpec s
222224
}
223225
}
224226

227+
// todo add workflow result?
228+
private void patchRelatedResourcesStatus(Context<Glue> context,
229+
Glue primary) {
230+
231+
var targetRelatedResources = primary.getSpec().getRelatedResources().stream()
232+
.filter(r -> r.getStatusPatch() != null || r.getStatusPatchTemplate() != null)
233+
.toList();
234+
235+
if (targetRelatedResources.isEmpty()) {
236+
return;
237+
}
238+
var actualData = genericTemplateHandler.createDataWithResources(primary, context);
239+
240+
targetRelatedResources.forEach(r -> {
241+
var relatedResources = Utils.getRelatedResources(primary, r, context);
242+
243+
var template = r.getStatusPatchTemplate() != null ? r.getStatusPatchTemplate()
244+
: Serialization.asYaml(r.getStatusPatch());
245+
var resultTemplate =
246+
genericTemplateHandler.processTemplate(actualData, template);
247+
var statusObjectMap = GenericTemplateHandler.parseTemplateToMapObject(resultTemplate);
248+
relatedResources.forEach((n, kr) -> {
249+
kr.setAdditionalProperty("status", statusObjectMap);
250+
context.getClient().resource(kr).patchStatus();
251+
});
252+
});
253+
254+
}
255+
225256
@SuppressWarnings({"rawtypes"})
226257
private Condition toCondition(ConditionSpec condition) {
227258
if (condition instanceof ReadyConditionSpec readyConditionSpec) {

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

+15-5
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import io.javaoperatorsdk.operator.glue.customresource.glue.RelatedResourceSpec;
2020
import io.javaoperatorsdk.operator.glue.customresource.operator.GlueOperator;
2121
import io.javaoperatorsdk.operator.glue.customresource.operator.GlueOperatorSpec;
22+
import io.javaoperatorsdk.operator.glue.customresource.operator.Parent;
2223
import io.javaoperatorsdk.operator.glue.customresource.operator.ResourceFlowOperatorStatus;
2324
import io.javaoperatorsdk.operator.glue.reconciler.ValidationAndErrorHandler;
2425
import io.javaoperatorsdk.operator.glue.reconciler.glue.GlueReconciler;
@@ -103,7 +104,6 @@ private Glue createGlue(GenericKubernetesResource targetParentResource,
103104

104105
ObjectMeta glueMetadata = glueMetadata(glueOperator, targetParentResource);
105106

106-
107107
glue.setMetadata(glueMetadata);
108108
glue.setSpec(toWorkflowSpec(glueOperator.getSpec()));
109109

@@ -112,17 +112,27 @@ private Glue createGlue(GenericKubernetesResource targetParentResource,
112112
}
113113

114114
var parent = glueOperator.getSpec().getParent();
115+
RelatedResourceSpec parentRelatedSpec =
116+
parentRelatedResourceSpec(targetParentResource, glueOperator, parent);
117+
118+
glue.getSpec().getRelatedResources().add(parentRelatedSpec);
119+
glue.addOwnerReference(targetParentResource);
120+
return glue;
121+
}
122+
123+
private static RelatedResourceSpec parentRelatedResourceSpec(
124+
GenericKubernetesResource targetParentResource, GlueOperator glueOperator, Parent parent) {
115125
RelatedResourceSpec parentRelatedSpec = new RelatedResourceSpec();
116126
parentRelatedSpec.setName(PARENT_RELATED_RESOURCE_NAME);
117127
parentRelatedSpec.setApiVersion(parent.getApiVersion());
118128
parentRelatedSpec.setKind(parent.getKind());
119129
parentRelatedSpec.setResourceNames(List.of(targetParentResource.getMetadata().getName()));
120130
parentRelatedSpec.setNamespace(targetParentResource.getMetadata().getNamespace());
121131
parentRelatedSpec.setClusterScoped(glueOperator.getSpec().getParent().isClusterScoped());
122-
123-
glue.getSpec().getRelatedResources().add(parentRelatedSpec);
124-
glue.addOwnerReference(targetParentResource);
125-
return glue;
132+
parentRelatedSpec
133+
.setStatusPatchTemplate(glueOperator.getSpec().getParent().getStatusTemplate());
134+
parentRelatedSpec.setStatusPatch(glueOperator.getSpec().getParent().getStatus());
135+
return parentRelatedSpec;
126136
}
127137

128138
private ObjectMeta glueMetadata(GlueOperator glueOperator,

src/main/java/io/javaoperatorsdk/operator/glue/templating/GenericTemplateHandler.java

+18-5
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import java.util.Map;
55

66
import io.fabric8.kubernetes.api.model.GenericKubernetesResource;
7+
import io.fabric8.kubernetes.client.utils.Serialization;
78
import io.javaoperatorsdk.operator.api.reconciler.Context;
89
import io.javaoperatorsdk.operator.glue.Utils;
910
import io.javaoperatorsdk.operator.glue.customresource.glue.Glue;
@@ -29,20 +30,26 @@ public String processTemplate(Map<String, Map<?, ?>> data, String template) {
2930

3031
public String processInputAndTemplate(Map<String, GenericKubernetesResource> data,
3132
String template) {
32-
Map<String, Map<?, ?>> res = new HashMap<>();
33-
data.forEach((key, value) -> res.put(key,
34-
value == null ? null : objectMapper.convertValue(value, Map.class)));
33+
Map<String, Map<?, ?>> res =
34+
genericKubernetesResourceDataToGenericData(data);
3535
return processTemplate(res, template);
3636
}
3737

3838
public String processTemplate(String template, Glue primary,
3939
Context<Glue> context) {
40-
4140
var data = createDataWithResources(primary, context);
4241
return processTemplate(data, template);
4342
}
4443

45-
private static Map<String, Map<?, ?>> createDataWithResources(Glue primary,
44+
private static Map<String, Map<?, ?>> genericKubernetesResourceDataToGenericData(
45+
Map<String, GenericKubernetesResource> data) {
46+
Map<String, Map<?, ?>> res = new HashMap<>();
47+
data.forEach((key, value) -> res.put(key,
48+
value == null ? null : objectMapper.convertValue(value, Map.class)));
49+
return res;
50+
}
51+
52+
public static Map<String, Map<?, ?>> createDataWithResources(Glue primary,
4653
Context<Glue> context) {
4754
Map<String, Map<?, ?>> res = new HashMap<>();
4855
var actualResourcesByName = Utils.getActualResourcesByNameInWorkflow(context, primary);
@@ -55,4 +62,10 @@ public String processTemplate(String template, Glue primary,
5562

5663
return res;
5764
}
65+
66+
@SuppressWarnings("unchecked")
67+
public static Map<String, ?> parseTemplateToMapObject(String template) {
68+
return Serialization.unmarshal(template, Map.class);
69+
}
70+
5871
}

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

+30
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,15 @@
88

99
import org.assertj.core.api.Assertions;
1010
import org.junit.jupiter.api.Test;
11+
import org.junit.jupiter.params.ParameterizedTest;
12+
import org.junit.jupiter.params.provider.ValueSource;
1113

1214
import io.fabric8.kubernetes.api.model.ConfigMap;
1315
import io.fabric8.kubernetes.api.model.ConfigMapBuilder;
1416
import io.fabric8.kubernetes.api.model.ObjectMetaBuilder;
1517
import io.fabric8.kubernetes.api.model.Secret;
1618
import io.fabric8.kubernetes.client.dsl.NonDeletingOperation;
19+
import io.javaoperatorsdk.operator.glue.customresource.TestCustomResource;
1720
import io.javaoperatorsdk.operator.glue.customresource.glue.DependentResourceSpec;
1821
import io.javaoperatorsdk.operator.glue.customresource.glue.Glue;
1922
import io.javaoperatorsdk.operator.glue.reconciler.ValidationAndErrorHandler;
@@ -324,6 +327,33 @@ void clusterScopedRelatedResource() {
324327
});
325328
}
326329

330+
@ParameterizedTest
331+
@ValueSource(strings = {"PatchRelatedStatus.yaml", "PatchRelatedStatusWithTemplate.yaml"})
332+
void pathRelatedResourceStatus(String glueFileName) {
333+
TestUtils.applyTestCrd(client, TestCustomResource.class);
334+
335+
var customResource = create(TestData.testCustomResource());
336+
var glue = createGlue("/glue/" + glueFileName);
337+
338+
await().untilAsserted(() -> {
339+
var cm = get(ConfigMap.class, "configmap1");
340+
assertThat(cm).isNotNull();
341+
var cr = get(TestCustomResource.class, "testcr1");
342+
assertThat(cr.getStatus()).isNotNull();
343+
assertThat(cr.getStatus().getValue()).isEqualTo(cm.getMetadata().getResourceVersion());
344+
});
345+
delete(glue);
346+
await().timeout(TestUtils.GC_WAIT_TIMEOUT).untilAsserted(() -> {
347+
var cm = get(ConfigMap.class, "configmap1");
348+
assertThat(cm).isNull();
349+
});
350+
delete(customResource);
351+
await().untilAsserted(() -> {
352+
var cr = get(TestCustomResource.class, "testcr1");
353+
assertThat(cr).isNull();
354+
});
355+
}
356+
327357
private List<Glue> testWorkflowList(int num) {
328358
List<Glue> res = new ArrayList<>();
329359
IntStream.range(0, num).forEach(index -> {

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

+6
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,11 @@
1313
import io.fabric8.kubernetes.client.KubernetesClient;
1414
import io.fabric8.kubernetes.client.dsl.NonDeletingOperation;
1515
import io.fabric8.kubernetes.client.utils.KubernetesResourceUtil;
16+
import io.javaoperatorsdk.operator.glue.customresource.glue.Glue;
1617

1718
import jakarta.inject.Inject;
1819

20+
import static io.javaoperatorsdk.operator.glue.TestUtils.loadGlue;
1921
import static org.assertj.core.api.Assertions.assertThat;
2022
import static org.awaitility.Awaitility.await;
2123

@@ -51,6 +53,10 @@ protected Namespace testNamespace(String name) {
5153
.build()).build();
5254
}
5355

56+
protected Glue createGlue(String path) {
57+
return create(loadGlue(path));
58+
}
59+
5460
protected <T extends HasMetadata> T create(T resource) {
5561
return client.resource(resource).inNamespace(testNamespace).create();
5662
}

0 commit comments

Comments
 (0)