Skip to content

Commit 6c3e636

Browse files
authored
feat: support label selectors (#86)
Signed-off-by: Attila Mészáros <[email protected]>
1 parent 6297111 commit 6c3e636

File tree

13 files changed

+380
-48
lines changed

13 files changed

+380
-48
lines changed

docs/reference.md

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -104,8 +104,41 @@ and `["list", "watch"]` for related resources.
104104

105105
The project is mainly tested with cluster-scoped deployment, however, QOSDK namespace-scoped deployments are also supported.
106106

107-
See also the upcoming deployment modes/options: [sharding with label selectors](https://github.com/csviri/kubernetes-glue-operator/issues/50),
108-
[watching only one custom resources type](https://github.com/csviri/kubernetes-glue-operator/issues/54)
107+
### Sharding with Label Selectors
108+
109+
The operator can be deployed to only target certain `Glue` or `GlueOperator` resources based on [label selectors](https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/).
110+
You can use simply the [configuration](https://docs.quarkiverse.io/quarkus-operator-sdk/dev/includes/quarkus-operator-sdk.html#quarkus-operator-sdk_quarkus-operator-sdk-controllers-controllers-selector)
111+
from Quarkus Operator SDK to set the label selector for the reconciler.
112+
113+
The configuration for `Glue` looks like:
114+
115+
`quarkus.operator-sdk.controllers.glue.selector=mylabel=myvalue`
116+
117+
for `GlueOperator`:
118+
119+
`quarkus.operator-sdk.controllers.glue-operator.selector=mylabel=myvalue`
120+
121+
This will work with any label selector for `GlueOperator` and with simple label selectors for `Glue`,
122+
thus in `key=value` or just `key` form.
123+
124+
125+
With `Glue` there is a caveat. `GlueOperator` works in a way that it creates a `Glue` resource for every
126+
custom resource tracked, so if there is a label selector defined for `Glue` it needs to add this label
127+
to the `Glue` resource when it is created. Since it is not trivial to parse label selectors, in more
128+
complex forms of label selectors (other the ones mentioned above), the labels to add to the `Glue` resources
129+
by a `GlueOperator` needs to be specified explicitly using
130+
[`glue.operator.glue-operator-managed-glue-labels`](https://github.com/csviri/kubernetes-glue-operator/blob/main/src/main/java/io/csviri/operator/glue/ControllerConfig.java#L10-L10)
131+
config key (which is a type of map). Therefore, for a label selector that specified two values for a glue:
132+
133+
`quarkus.operator-sdk.controllers.glue.selector=mylabel1=value1,mylabel2=value2`
134+
135+
the following two configuration params needs to be added:
136+
137+
`glue.operator.glue-operator-managed-glue-labels.mylabel1=value1`
138+
`glue.operator.glue-operator-managed-glue-labels.mylabel2=value2`
139+
140+
This will ensure that the labels are added correctly to the `Glue`. See the related
141+
[integration test](https://github.com/csviri/kubernetes-glue-operator/blob/main/src/test/java/io/csviri/operator/glue/GlueOperatorComplexLabelSelectorTest.java#L23-L23).
109142

110143
## Implementation details and performance
111144

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package io.csviri.operator.glue;
2+
3+
import java.util.Map;
4+
5+
import io.smallrye.config.ConfigMapping;
6+
7+
@ConfigMapping(prefix = "glue.operator")
8+
public interface ControllerConfig {
9+
10+
Map<String, String> glueOperatorManagedGlueLabels();
11+
12+
}

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,12 +34,13 @@
3434
import static io.csviri.operator.glue.Utils.getResourceForSSAFrom;
3535
import static io.csviri.operator.glue.reconciler.operator.GlueOperatorReconciler.PARENT_RELATED_RESOURCE_NAME;
3636

37-
@ControllerConfiguration
37+
@ControllerConfiguration(name = GlueReconciler.GLUE_RECONCILER_NAME)
3838
public class GlueReconciler implements Reconciler<Glue>, Cleaner<Glue>, ErrorStatusHandler<Glue> {
3939

4040
private static final Logger log = LoggerFactory.getLogger(GlueReconciler.class);
4141
public static final String DEPENDENT_NAME_ANNOTATION_KEY = "io.csviri.operator.resourceflow/name";
4242
public static final String PARENT_GLUE_FINALIZER_PREFIX = "io.csviri.operator.resourceflow.glue/";
43+
public static final String GLUE_RECONCILER_NAME = "glue";
4344

4445
@Inject
4546
ValidationAndErrorHandler validationAndErrorHandler;

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

Lines changed: 63 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
package io.csviri.operator.glue.reconciler.operator;
22

3-
import java.util.ArrayList;
4-
import java.util.List;
5-
import java.util.Map;
6-
import java.util.Set;
3+
import java.util.*;
74

5+
import org.eclipse.microprofile.config.inject.ConfigProperty;
86
import org.slf4j.Logger;
97
import org.slf4j.LoggerFactory;
108

9+
import io.csviri.operator.glue.ControllerConfig;
10+
import io.csviri.operator.glue.GlueException;
1111
import io.csviri.operator.glue.customresource.glue.Glue;
1212
import io.csviri.operator.glue.customresource.glue.GlueSpec;
1313
import io.csviri.operator.glue.customresource.glue.RelatedResourceSpec;
@@ -25,9 +25,12 @@
2525
import io.javaoperatorsdk.operator.processing.event.source.EventSource;
2626
import io.javaoperatorsdk.operator.processing.event.source.informer.InformerEventSource;
2727

28+
import jakarta.annotation.PostConstruct;
2829
import jakarta.inject.Inject;
2930

30-
@ControllerConfiguration
31+
import static io.csviri.operator.glue.reconciler.glue.GlueReconciler.GLUE_RECONCILER_NAME;
32+
33+
@ControllerConfiguration(name = GlueOperatorReconciler.GLUE_OPERATOR_RECONCILER_NAME)
3134
public class GlueOperatorReconciler
3235
implements Reconciler<GlueOperator>, EventSourceInitializer<GlueOperator>,
3336
Cleaner<GlueOperator>, ErrorStatusHandler<GlueOperator> {
@@ -37,11 +40,25 @@ public class GlueOperatorReconciler
3740
public static final String GLUE_LABEL_KEY = "foroperator";
3841
public static final String GLUE_LABEL_VALUE = "true";
3942
public static final String PARENT_RELATED_RESOURCE_NAME = "parent";
43+
public static final String GLUE_OPERATOR_RECONCILER_NAME = "glue-operator";
4044

4145
@Inject
4246
ValidationAndErrorHandler validationAndErrorHandler;
4347

44-
private InformerEventSource<Glue, GlueOperator> resourceFlowEventSource;
48+
@ConfigProperty(name = "quarkus.operator-sdk.controllers." + GLUE_RECONCILER_NAME + ".selector")
49+
Optional<String> glueLabelSelector;
50+
51+
@Inject
52+
ControllerConfig controllerConfig;
53+
54+
private Map<String, String> defaultGlueLabels;
55+
56+
private InformerEventSource<Glue, GlueOperator> glueEventSource;
57+
58+
@PostConstruct
59+
void init() {
60+
defaultGlueLabels = initDefaultLabelsToAddToGlue();
61+
}
4562

4663
@Override
4764
public UpdateControl<GlueOperator> reconcile(GlueOperator glueOperator,
@@ -54,9 +71,10 @@ public UpdateControl<GlueOperator> reconcile(GlueOperator glueOperator,
5471

5572
var targetCREventSource = getOrRegisterCustomResourceEventSource(glueOperator, context);
5673
targetCREventSource.list().forEach(cr -> {
57-
var actualResourceFlow = resourceFlowEventSource
58-
.get(new ResourceID(glueName(cr), cr.getMetadata().getNamespace()));
59-
var desiredResourceFlow = createResourceFlow(cr, glueOperator);
74+
var actualResourceFlow = glueEventSource
75+
.get(new ResourceID(glueName(cr.getMetadata().getName(), cr.getKind()),
76+
cr.getMetadata().getNamespace()));
77+
var desiredResourceFlow = createGlue(cr, glueOperator);
6078
if (actualResourceFlow.isEmpty()) {
6179
context.getClient().resource(desiredResourceFlow).serverSideApply();
6280
} else if (!actualResourceFlow.orElseThrow().getSpec()
@@ -72,17 +90,22 @@ public UpdateControl<GlueOperator> reconcile(GlueOperator glueOperator,
7290
return UpdateControl.noUpdate();
7391
}
7492

75-
private Glue createResourceFlow(GenericKubernetesResource targetParentResource,
93+
private Glue createGlue(GenericKubernetesResource targetParentResource,
7694
GlueOperator glueOperator) {
7795
var glue = new Glue();
7896

7997
glue.setMetadata(new ObjectMetaBuilder()
80-
.withName(glueName(targetParentResource))
98+
.withName(
99+
glueName(targetParentResource.getMetadata().getName(), targetParentResource.getKind()))
81100
.withNamespace(targetParentResource.getMetadata().getNamespace())
82101
.withLabels(Map.of(GLUE_LABEL_KEY, GLUE_LABEL_VALUE))
83102
.build());
84103
glue.setSpec(toWorkflowSpec(glueOperator.getSpec()));
85104

105+
if (!defaultGlueLabels.isEmpty()) {
106+
glue.getMetadata().getLabels().putAll(defaultGlueLabels);
107+
}
108+
86109
var parent = glueOperator.getSpec().getParent();
87110
RelatedResourceSpec parentRelatedSpec = new RelatedResourceSpec();
88111
parentRelatedSpec.setName(PARENT_RELATED_RESOURCE_NAME);
@@ -129,12 +152,12 @@ private InformerEventSource<GenericKubernetesResource, GlueOperator> getOrRegist
129152
@Override
130153
public Map<String, EventSource> prepareEventSources(
131154
EventSourceContext<GlueOperator> eventSourceContext) {
132-
resourceFlowEventSource = new InformerEventSource<>(
155+
glueEventSource = new InformerEventSource<>(
133156
InformerConfiguration.from(Glue.class, eventSourceContext)
134157
.withLabelSelector(GLUE_LABEL_KEY + "=" + GLUE_LABEL_VALUE)
135158
.build(),
136159
eventSourceContext);
137-
return EventSourceInitializer.nameEventSources(resourceFlowEventSource);
160+
return EventSourceInitializer.nameEventSources(glueEventSource);
138161
}
139162

140163
@Override
@@ -155,9 +178,34 @@ public DeleteControl cleanup(GlueOperator glueOperator,
155178
return DeleteControl.defaultDelete();
156179
}
157180

158-
private static String glueName(GenericKubernetesResource cr) {
159-
return KubernetesResourceUtil.sanitizeName(cr.getMetadata().getName() + "-" + cr.getKind());
181+
public static String glueName(String name, String kind) {
182+
return KubernetesResourceUtil.sanitizeName(name + "-" + kind);
160183
}
161184

185+
private Map<String, String> initDefaultLabelsToAddToGlue() {
186+
Map<String, String> res = new HashMap<>();
187+
if (!controllerConfig.glueOperatorManagedGlueLabels().isEmpty()) {
188+
res.putAll(controllerConfig.glueOperatorManagedGlueLabels());
189+
} else {
190+
glueLabelSelector.ifPresent(ls -> {
191+
if (ls.contains(",") || ls.contains("(")) {
192+
throw new GlueException(
193+
"Glue reconciler label selector contains non-simple label selector: " + ls +
194+
". Specify Glue label selector in simple form ('key=value' or 'key') " +
195+
"or configure 'glue.operator.glue-operator-managed-glue-labels'");
196+
}
197+
String[] labelSelectorParts = ls.split("=");
198+
if (labelSelectorParts.length > 2) {
199+
throw new GlueException("Invalid label selector: " + ls);
200+
}
201+
if (labelSelectorParts.length == 1) {
202+
res.put(labelSelectorParts[0], "");
203+
} else {
204+
res.put(labelSelectorParts[0], labelSelectorParts[1]);
205+
}
206+
});
207+
}
208+
return res;
209+
}
162210

163211
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
package io.csviri.operator.glue;
2+
3+
4+
import java.util.Map;
5+
6+
import org.junit.jupiter.api.Test;
7+
8+
import io.csviri.operator.glue.customresource.glue.Glue;
9+
import io.fabric8.kubernetes.api.model.ConfigMap;
10+
import io.quarkus.test.junit.QuarkusTest;
11+
import io.quarkus.test.junit.QuarkusTestProfile;
12+
import io.quarkus.test.junit.TestProfile;
13+
14+
import static io.csviri.operator.glue.TestUtils.INITIAL_RECONCILE_WAIT_TIMEOUT;
15+
import static io.csviri.operator.glue.reconciler.glue.GlueReconciler.GLUE_RECONCILER_NAME;
16+
import static org.assertj.core.api.Assertions.assertThat;
17+
import static org.awaitility.Awaitility.await;
18+
19+
@QuarkusTest
20+
@TestProfile(GlueLabelSelectorTest.LabelSelectorTestProfile.class)
21+
public class GlueLabelSelectorTest extends TestBase {
22+
23+
24+
public static final String LABEL_KEY = "test-glue";
25+
public static final String LABEL_VALUE = "true";
26+
27+
@Test
28+
void testLabelSelectorHandling() {
29+
Glue glue =
30+
TestUtils.loadResoureFlow("/glue/SimpleGlue.yaml");
31+
glue = create(glue);
32+
33+
await().pollDelay(INITIAL_RECONCILE_WAIT_TIMEOUT).untilAsserted(() -> {
34+
assertThat(get(ConfigMap.class, "simple-glue-configmap")).isNull();
35+
});
36+
37+
glue.getMetadata().getLabels().put(LABEL_KEY, LABEL_VALUE);
38+
update(glue);
39+
40+
await().untilAsserted(() -> {
41+
assertThat(get(ConfigMap.class, "simple-glue-configmap")).isNotNull();
42+
});
43+
}
44+
45+
public static class LabelSelectorTestProfile implements QuarkusTestProfile {
46+
47+
@Override
48+
public Map<String, String> getConfigOverrides() {
49+
return Map.of("quarkus.operator-sdk.controllers." + GLUE_RECONCILER_NAME + ".selector",
50+
LABEL_KEY + "=" + LABEL_VALUE);
51+
}
52+
}
53+
54+
}
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
package io.csviri.operator.glue;
2+
3+
import java.util.Map;
4+
5+
import org.junit.jupiter.api.BeforeEach;
6+
import org.junit.jupiter.api.Test;
7+
8+
import io.csviri.operator.glue.customresource.TestCustomResource;
9+
import io.csviri.operator.glue.customresource.TestCustomResource2;
10+
import io.csviri.operator.glue.customresource.glue.Glue;
11+
import io.csviri.operator.glue.reconciler.operator.GlueOperatorReconciler;
12+
import io.fabric8.kubernetes.api.model.ConfigMap;
13+
import io.quarkus.test.junit.QuarkusTest;
14+
import io.quarkus.test.junit.QuarkusTestProfile;
15+
import io.quarkus.test.junit.TestProfile;
16+
17+
import static io.csviri.operator.glue.reconciler.glue.GlueReconciler.GLUE_RECONCILER_NAME;
18+
import static org.assertj.core.api.Assertions.assertThat;
19+
import static org.awaitility.Awaitility.await;
20+
21+
@QuarkusTest
22+
@TestProfile(GlueOperatorComplexLabelSelectorTest.GlueOperatorComplexLabelSelectorTestProfile.class)
23+
public class GlueOperatorComplexLabelSelectorTest extends TestBase {
24+
25+
public static final String GLUE_LABEL_KEY1 = "test-glue1";
26+
public static final String GLUE_LABEL_KEY2 = "test-glue2";
27+
public static final String LABEL_VALUE = "true";
28+
29+
@BeforeEach
30+
void applyCRD() {
31+
TestUtils.applyTestCrd(client, TestCustomResource.class, TestCustomResource2.class);
32+
}
33+
34+
@Test
35+
void testGlueOperatorLabelSelector() {
36+
var go = create(TestUtils
37+
.loadResourceFlowOperator("/glueoperator/SimpleGlueOperator.yaml"));
38+
39+
var testCR = create(TestData.testCustomResource());
40+
41+
await().untilAsserted(() -> {
42+
assertThat(get(ConfigMap.class, testCR.getMetadata().getName())).isNotNull();
43+
var glue = get(Glue.class, GlueOperatorReconciler.glueName(testCR.getMetadata().getName(),
44+
testCR.getKind()));
45+
assertThat(glue).isNotNull();
46+
assertThat(glue.getMetadata().getLabels())
47+
.containsEntry(GLUE_LABEL_KEY1, LABEL_VALUE)
48+
.containsEntry(GLUE_LABEL_KEY2, LABEL_VALUE);
49+
});
50+
51+
delete(testCR);
52+
await().untilAsserted(() -> {
53+
var glue = get(Glue.class, GlueOperatorReconciler.glueName(testCR.getMetadata().getName(),
54+
testCR.getKind()));
55+
assertThat(glue).isNull();
56+
});
57+
delete(go);
58+
}
59+
60+
public static class GlueOperatorComplexLabelSelectorTestProfile implements QuarkusTestProfile {
61+
62+
@Override
63+
public Map<String, String> getConfigOverrides() {
64+
return Map.of("quarkus.operator-sdk.controllers." + GLUE_RECONCILER_NAME + ".selector",
65+
// complex label selector with 2 values checked
66+
GLUE_LABEL_KEY1 + "=" + LABEL_VALUE + "," + GLUE_LABEL_KEY2 + "=" + LABEL_VALUE,
67+
// explicit labels added to glue
68+
"glue.operator.glue-operator-managed-glue-labels." + GLUE_LABEL_KEY1, LABEL_VALUE,
69+
"glue.operator.glue-operator-managed-glue-labels." + GLUE_LABEL_KEY2, LABEL_VALUE);
70+
}
71+
}
72+
73+
}

0 commit comments

Comments
 (0)