Skip to content

Commit 20e91a6

Browse files
authored
feat: support cluster scoped and different namespace resources (#92)
Signed-off-by: Attila Mészáros <[email protected]>
1 parent 77c64c4 commit 20e91a6

14 files changed

+213
-73
lines changed

Diff for: README.md

+1
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,7 @@ spec:
169169
protocol: TCP
170170
171171
- name: mutation_hook_config
172+
clusterScoped: true
172173
# dependsOn relation means, that the resource will be reconciled only if all
173174
# the listed resources are already reconciled and ready (if ready post-condition is present).
174175
# This resource will be applied after the service and deployment are applied,

Diff for: docs/reference.md

+3-11
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ It has several attributes:
2121
- **`name`** - is a mandatory unique (unique also regarding related resources) attribute.
2222
The resource is referenced by this name from other places, typically other resource templates and `JSCondition`.
2323
If it is used in a `JSCondition` the `name` must be a valid JavaScript variable name.
24+
- **`clusterScoped`** - a flag to indicate if the resource is cluster scoped. Default value is `false`.
25+
It is mandatory to set this for cluster scoped resources.
2426
- **`resource`** - is the desired state of the resource applied by default using Server Side Apply. The resource is templated using
2527
[qute templating engine](https://quarkus.io/guides/qute-reference), other resources can be referenced from the templates, see below.
2628
There is a restriction, that the child resource is namespaced, and the namespace is always the same as the namespace of the `Glue`
@@ -51,6 +53,7 @@ See sample usage within `Glue` [here](https://github.com/csviri/kubernetes-glue-
5153
The following attributes can be defined for a related resource:
5254

5355
- **`name`** - same as for child resource, unique identifier, used to reference the resource.
56+
- **`clusterScoped`** - if the related resource is cluster scoped. Default is `false`.
5457
- **`apiVersion`** - Kubernetes resource API Version of the resource
5558
- **`kind`** - Kubernetes kind property of the resource
5659
- **`resourceNames`** - list of string of the resource names within the same namespace as `Glue`.
@@ -175,17 +178,6 @@ resources containing the same resource type.
175178
The templating and some of the Javascript condition is probably the most time-consuming and resource-intensive part which will
176179
be continuously improved in the follow-up releases.
177180

178-
## Current limitations
179-
180-
Note that none of the limitations are unsolvable, and will be continuously removed in the coming releases.
181-
182-
1. Child resources and related resources are always namespace scoped resources, and in the same namespace as the
183-
primary resource (`Glue` or the parent in the case of `GlueOperator`)
184-
185-
2. ~~Related resource changes are not triggering the reconciliation.
186-
Due to a bug in fabric8 client, after that is fixed, this is trivial to fix too:
187-
https://github.com/fabric8io/kubernetes-client/issues/5729~~
188-
189181
## Samples
190182

191183
1. [WebPage](https://github.com/csviri/kubernetes-glue-operator/tree/main/src/test/resources/sample/webpage) `GlueOperator`, serves a static website from the cluster.

Diff for: src/main/java/io/csviri/operator/glue/Utils.java

+7-9
Original file line numberDiff line numberDiff line change
@@ -31,16 +31,13 @@ public static Map<String, GenericKubernetesResource> getActualResourcesByNameInW
3131
Map<String, GenericKubernetesResource> res = new HashMap<>();
3232
secondaryResources.forEach(sr -> {
3333
var dependentSpec = glue.getSpec().getChildResources().stream()
34-
.filter(r -> Utils.getApiVersion(r).equals(sr.getApiVersion())
35-
&& Utils.getKind(r).equals(sr.getKind())
34+
.filter(r ->
3635
// comparing the name from annotation since the resource name might be templated in spec
3736
// therefore "Utils.getName(relatedResourceSpec).equals(sr.getMetadata().getName())" would not
3837
// work
39-
&& r.getName()
40-
.equals(sr.getMetadata().getAnnotations().get(DEPENDENT_NAME_ANNOTATION_KEY))
41-
// namespace not compared here, it should be done it is just not trivial, now it is limited to
42-
// have one kind of resource in the workflow with the same resource name
43-
).findFirst();
38+
r.getName()
39+
.equals(sr.getMetadata().getAnnotations().get(DEPENDENT_NAME_ANNOTATION_KEY)))
40+
.findFirst();
4441
dependentSpec.ifPresent(spec -> res.put(spec.getName(), sr));
4542
});
4643

@@ -70,8 +67,9 @@ public static Map<String, GenericKubernetesResource> getRelatedResources(Glue gl
7067
(InformerEventSource<GenericKubernetesResource, Glue>) context
7168
.eventSourceRetriever()
7269
.getResourceEventSourceFor(GenericKubernetesResource.class, gvk.toString());
73-
var namespace =
74-
relatedResourceSpec.getNamespace() == null ? glue.getMetadata().getNamespace()
70+
71+
var namespace = relatedResourceSpec.isClusterScoped() ? null
72+
: relatedResourceSpec.getNamespace() == null ? glue.getMetadata().getNamespace()
7573
: relatedResourceSpec.getNamespace();
7674

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

Diff for: src/main/java/io/csviri/operator/glue/customresource/glue/DependentResourceSpec.java

+16-4
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,13 @@ public class DependentResourceSpec {
1414
@Required
1515
private String name;
1616

17-
private String resourceTemplate;
17+
private boolean clusterScoped = Boolean.FALSE;
1818

1919
@PreserveUnknownFields
2020
private GenericKubernetesResource resource;
2121

22+
private String resourceTemplate;
23+
2224
private List<String> dependsOn = new ArrayList<>();
2325

2426
@PreserveUnknownFields
@@ -82,22 +84,32 @@ public DependentResourceSpec setResourceTemplate(String resourceTemplate) {
8284
return this;
8385
}
8486

87+
public boolean isClusterScoped() {
88+
return clusterScoped;
89+
}
90+
91+
public void setClusterScoped(boolean clusterScoped) {
92+
this.clusterScoped = clusterScoped;
93+
}
94+
8595
@Override
8696
public boolean equals(Object o) {
8797
if (this == o)
8898
return true;
8999
if (o == null || getClass() != o.getClass())
90100
return false;
91101
DependentResourceSpec that = (DependentResourceSpec) o;
92-
return Objects.equals(name, that.name)
102+
return clusterScoped == that.clusterScoped && Objects.equals(name, that.name)
103+
&& Objects.equals(resource, that.resource)
93104
&& Objects.equals(resourceTemplate, that.resourceTemplate)
94-
&& Objects.equals(resource, that.resource) && Objects.equals(dependsOn, that.dependsOn)
105+
&& Objects.equals(dependsOn, that.dependsOn)
95106
&& Objects.equals(readyPostCondition, that.readyPostCondition)
96107
&& Objects.equals(condition, that.condition);
97108
}
98109

99110
@Override
100111
public int hashCode() {
101-
return Objects.hash(name, resourceTemplate, resource, dependsOn, readyPostCondition, condition);
112+
return Objects.hash(name, clusterScoped, resource, resourceTemplate, dependsOn,
113+
readyPostCondition, condition);
102114
}
103115
}

Diff for: src/main/java/io/csviri/operator/glue/customresource/glue/RelatedResourceSpec.java

+14-4
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,13 @@ public class RelatedResourceSpec {
1010
// name for referencing the resource from templates and conditions (not name from object metadata)
1111
@Required
1212
private String name;
13+
private String namespace;
1314

1415
@Required
1516
private String apiVersion;
1617
@Required
1718
private String kind;
18-
private String namespace;
19+
private boolean clusterScoped = Boolean.FALSE;
1920
private List<String> resourceNames;
2021

2122

@@ -71,13 +72,22 @@ public boolean equals(Object o) {
7172
if (o == null || getClass() != o.getClass())
7273
return false;
7374
RelatedResourceSpec that = (RelatedResourceSpec) o;
74-
return Objects.equals(name, that.name) && Objects.equals(apiVersion, that.apiVersion)
75-
&& Objects.equals(kind, that.kind) && Objects.equals(namespace, that.namespace)
75+
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)
7678
&& Objects.equals(resourceNames, that.resourceNames);
7779
}
7880

7981
@Override
8082
public int hashCode() {
81-
return Objects.hash(name, apiVersion, kind, namespace, resourceNames);
83+
return Objects.hash(name, apiVersion, kind, clusterScoped, namespace, resourceNames);
84+
}
85+
86+
public boolean isClusterScoped() {
87+
return clusterScoped;
88+
}
89+
90+
public void setClusterScoped(boolean clusterScoped) {
91+
this.clusterScoped = clusterScoped;
8292
}
8393
}

Diff for: src/main/java/io/csviri/operator/glue/dependent/GCGenericDependentResource.java

+5-4
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,12 @@
77
public class GCGenericDependentResource extends GenericDependentResource
88
implements GarbageCollected<Glue> {
99

10-
public GCGenericDependentResource(GenericKubernetesResource desired, String name) {
11-
super(desired, name);
10+
public GCGenericDependentResource(GenericKubernetesResource desired, String name,
11+
boolean clusterScoped) {
12+
super(desired, name, clusterScoped);
1213
}
1314

14-
public GCGenericDependentResource(String desiredTemplate, String name) {
15-
super(desiredTemplate, name);
15+
public GCGenericDependentResource(String desiredTemplate, String name, boolean clusterScoped) {
16+
super(desiredTemplate, name, clusterScoped);
1617
}
1718
}

Diff for: src/main/java/io/csviri/operator/glue/dependent/GenericDependentResource.java

+7-4
Original file line numberDiff line numberDiff line change
@@ -22,23 +22,27 @@ public class GenericDependentResource
2222
private final GenericKubernetesResource desired;
2323
private final String desiredTemplate;
2424
private final String name;
25+
private final boolean clusterScoped;
2526

2627
// optimize share between instances
2728
private final GenericTemplateHandler genericTemplateHandler = new GenericTemplateHandler();
2829

29-
public GenericDependentResource(GenericKubernetesResource desired, String name) {
30+
public GenericDependentResource(GenericKubernetesResource desired, String name,
31+
boolean clusterScoped) {
3032
super(new GroupVersionKind(desired.getApiVersion(), desired.getKind()));
3133
this.desired = desired;
3234
this.desiredTemplate = null;
3335
this.name = name;
36+
this.clusterScoped = clusterScoped;
3437
}
3538

36-
public GenericDependentResource(String desiredTemplate, String name) {
39+
public GenericDependentResource(String desiredTemplate, String name, boolean clusterScoped) {
3740
super(new GroupVersionKind(Utils.getApiVersionFromTemplate(desiredTemplate),
3841
Utils.getKindFromTemplate(desiredTemplate)));
3942
this.name = name;
4043
this.desiredTemplate = desiredTemplate;
4144
this.desired = null;
45+
this.clusterScoped = clusterScoped;
4246
}
4347

4448
@Override
@@ -53,8 +57,7 @@ protected GenericKubernetesResource desired(Glue primary,
5357
resultDesired.getMetadata().getAnnotations()
5458
.put(GlueReconciler.DEPENDENT_NAME_ANNOTATION_KEY, name);
5559

56-
// set only for cluster scoped when detection is ready
57-
if (resultDesired.getMetadata().getNamespace() == null) {
60+
if (resultDesired.getMetadata().getNamespace() == null && !clusterScoped) {
5861
resultDesired.getMetadata().setNamespace(primary.getMetadata().getNamespace());
5962
}
6063
return resultDesired;

Diff for: src/main/java/io/csviri/operator/glue/reconciler/glue/GlueReconciler.java

+10-5
Original file line numberDiff line numberDiff line change
@@ -202,14 +202,19 @@ private void createAndAddDependentToWorkflow(Glue primary, Context<Glue> context
202202

203203
private static GenericDependentResource createDependentResource(DependentResourceSpec spec,
204204
boolean leafDependent, Boolean resourceInSameNamespaceAsPrimary) {
205-
if (leafDependent && resourceInSameNamespaceAsPrimary) {
205+
206+
if (leafDependent && resourceInSameNamespaceAsPrimary && !spec.isClusterScoped()) {
206207
return spec.getResourceTemplate() != null
207-
? new GCGenericDependentResource(spec.getResourceTemplate(), spec.getName())
208-
: new GCGenericDependentResource(spec.getResource(), spec.getName());
208+
? new GCGenericDependentResource(spec.getResourceTemplate(), spec.getName(),
209+
spec.isClusterScoped())
210+
: new GCGenericDependentResource(spec.getResource(), spec.getName(),
211+
spec.isClusterScoped());
209212
} else {
210213
return spec.getResourceTemplate() != null
211-
? new GenericDependentResource(spec.getResourceTemplate(), spec.getName())
212-
: new GenericDependentResource(spec.getResource(), spec.getName());
214+
? new GenericDependentResource(spec.getResourceTemplate(), spec.getName(),
215+
spec.isClusterScoped())
216+
: new GenericDependentResource(spec.getResource(), spec.getName(),
217+
spec.isClusterScoped());
213218
}
214219
}
215220

Diff for: src/test/java/io/csviri/operator/glue/GlueTest.java

+86-28
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,10 @@
1212
import io.csviri.operator.glue.customresource.glue.Glue;
1313
import io.csviri.operator.glue.reconciler.ValidationAndErrorHandler;
1414
import io.fabric8.kubernetes.api.model.ConfigMap;
15+
import io.fabric8.kubernetes.api.model.ConfigMapBuilder;
16+
import io.fabric8.kubernetes.api.model.ObjectMetaBuilder;
1517
import io.fabric8.kubernetes.api.model.Secret;
18+
import io.fabric8.kubernetes.client.dsl.NonDeletingOperation;
1619
import io.quarkus.test.junit.QuarkusTest;
1720

1821
import static org.assertj.core.api.Assertions.assertThat;
@@ -237,35 +240,88 @@ void nonUniqueNameResultsInErrorMessageOnStatus() {
237240
});
238241
}
239242

243+
@Test
244+
void childInDifferentNamespace() {
245+
Glue glue = create(TestUtils.loadGlue("/glue/ResourceInDifferentNamespace.yaml"));
246+
247+
await().untilAsserted(() -> {
248+
var cmDifferentNS = client.configMaps().inNamespace("default")
249+
.withName("configmap1");
250+
var cm2 = get(ConfigMap.class, "configmap2");
251+
252+
assertThat(cmDifferentNS).isNotNull();
253+
assertThat(cm2).isNotNull();
254+
});
255+
256+
delete(glue);
257+
await().timeout(TestUtils.GC_WAIT_TIMEOUT).untilAsserted(() -> {
258+
var cmDifferentNS = client.configMaps().inNamespace("default")
259+
.withName("configmap1").get();
260+
var cm2 = get(ConfigMap.class, "configmap2");
261+
262+
assertThat(cmDifferentNS).isNull();
263+
assertThat(cm2).isNull();
264+
});
265+
}
266+
267+
@Test
268+
void clusterScopedChild() {
269+
var glue = create(TestUtils.loadGlue("/glue/ClusterScopedChild.yaml"));
270+
await().untilAsserted(() -> {
271+
var ns = client.namespaces()
272+
.withName("testnamespace");
273+
assertThat(ns).isNotNull();
274+
});
275+
276+
delete(glue);
277+
await().timeout(TestUtils.GC_WAIT_TIMEOUT).untilAsserted(() -> {
278+
var ns = client.namespaces()
279+
.withName("testnamespace").get();
280+
assertThat(ns).isNull();
281+
});
282+
}
283+
284+
@Test
285+
void relatedResourceFromDifferentNamespace() {
286+
client.resource(new ConfigMapBuilder()
287+
.withMetadata(new ObjectMetaBuilder()
288+
.withName("related-configmap")
289+
.withNamespace("default")
290+
.build())
291+
.withData(Map.of("key1", "value1"))
292+
.build()).createOr(NonDeletingOperation::update);
240293

241-
//
242-
// @Disabled("Not supported in current version")
243-
// @Test
244-
// void childInDifferentNamespaceAsPrimary() {
245-
// Glue w = extension
246-
// .create(TestUtils.loadResoureFlow("/glue/ResourceInDifferentNamespace.yaml"));
247-
//
248-
// await().untilAsserted(() -> {
249-
// var cmDifferentNS = extension.getKubernetesClient().configMaps().inNamespace("default")
250-
// .withName("configmap1");
251-
// var cm2 = extension.get(ConfigMap.class, "configmap2");
252-
//
253-
// assertThat(cmDifferentNS).isNotNull();
254-
// assertThat(cm2).isNotNull();
255-
// });
256-
//
257-
// extension.delete(w);
258-
//
259-
// await().untilAsserted(() -> {
260-
// var cmDifferentNS = extension.getKubernetesClient().configMaps().inNamespace("default")
261-
// .withName("configmap1");
262-
// var cm2 = extension.get(ConfigMap.class, "configmap2");
263-
//
264-
// assertThat(cmDifferentNS).isNull();
265-
// assertThat(cm2).isNull();
266-
// });
267-
//
268-
// }
294+
var glue = create(TestUtils.loadGlue("/glue/RelatedResourceFromDifferentNamespace.yaml"));
295+
296+
await().untilAsserted(() -> {
297+
var cm = get(ConfigMap.class, "configmap1");
298+
assertThat(cm).isNotNull();
299+
assertThat(cm.getData()).containsEntry("copy-key", "value1");
300+
});
301+
302+
delete(glue);
303+
await().timeout(TestUtils.GC_WAIT_TIMEOUT).untilAsserted(() -> {
304+
var cm = get(ConfigMap.class, "configmap1");
305+
assertThat(cm).isNull();
306+
});
307+
}
308+
309+
@Test
310+
void clusterScopedRelatedResource() {
311+
var glue = create(TestUtils.loadGlue("/glue/ClusterScopedRelatedResource.yaml"));
312+
313+
await().untilAsserted(() -> {
314+
var cm = get(ConfigMap.class, "configmap1");
315+
assertThat(cm).isNotNull();
316+
assertThat(cm.getData()).containsEntry("phase", "Active");
317+
});
318+
319+
delete(glue);
320+
await().timeout(TestUtils.GC_WAIT_TIMEOUT).untilAsserted(() -> {
321+
var cm = get(ConfigMap.class, "configmap1");
322+
assertThat(cm).isNull();
323+
});
324+
}
269325

270326
private List<Glue> testWorkflowList(int num) {
271327
List<Glue> res = new ArrayList<>();
@@ -278,4 +334,6 @@ private List<Glue> testWorkflowList(int num) {
278334
return res;
279335
}
280336

337+
338+
281339
}

0 commit comments

Comments
 (0)