Skip to content

Commit cba2337

Browse files
committed
feat: shard selector support (#3429)
Signed-off-by: Attila Mészáros <a_meszaros@apple.com>
1 parent 5325dbf commit cba2337

11 files changed

Lines changed: 101 additions & 10 deletions

File tree

docs/content/en/docs/documentation/operations/configuration.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -324,6 +324,7 @@ All controller-level keys are prefixed with `josdk.controller.<controller-name>.
324324
| `josdk.controller.<name>.finalizer` | `String` | Finalizer string added to managed resources |
325325
| `josdk.controller.<name>.generation-aware` | `Boolean` | Skip reconciliation when the resource generation has not changed |
326326
| `josdk.controller.<name>.label-selector` | `String` | Label selector to filter watched resources |
327+
| `josdk.controller.<name>.shard-selector` | `String` | Shard selector to filter watched resources for sharding across operator replicas |
327328
| `josdk.controller.<name>.max-reconciliation-interval` | `Duration` | Maximum interval between reconciliations even without events |
328329
| `josdk.controller.<name>.field-manager` | `String` | Field manager name used for SSA operations |
329330
| `josdk.controller.<name>.trigger-reconciler-on-all-events` | `Boolean` | Trigger reconciliation on every event, not only meaningful changes |
@@ -333,6 +334,7 @@ All controller-level keys are prefixed with `josdk.controller.<controller-name>.
333334
| Key | Type | Description |
334335
|---|---|---|
335336
| `josdk.controller.<name>.informer.label-selector` | `String` | Label selector for the primary resource informer (alias for `label-selector`) |
337+
| `josdk.controller.<name>.informer.shard-selector` | `String` | Shard selector for the primary resource informer (alias for `shard-selector`) |
336338
| `josdk.controller.<name>.informer.list-limit` | `Long` | Page size for paginated informer list requests; omit for no pagination |
337339

338340
#### Retry

operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ControllerConfigurationOverrider.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,11 @@ public ControllerConfigurationOverrider<R> withLabelSelector(String labelSelecto
136136
return this;
137137
}
138138

139+
public ControllerConfigurationOverrider<R> withShardSelector(String shardSelector) {
140+
config.withShardSelector(shardSelector);
141+
return this;
142+
}
143+
139144
public ControllerConfigurationOverrider<R> withReconciliationMaxInterval(
140145
Duration reconciliationMaxInterval) {
141146
this.reconciliationMaxInterval = reconciliationMaxInterval;

operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/informer/Informer.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,18 @@
7171
*/
7272
String labelSelector() default NO_VALUE_SET;
7373

74+
/**
75+
* Optional shard selector used to restrict the set of resources the associated informer will act
76+
* upon to a single shard, typically when the same workload is split across several operator
77+
* instances. Just like {@link #labelSelector()} it is expressed as a label selector and can be
78+
* made of multiple comma separated requirements that act as a logical AND operator. When both a
79+
* label selector and a shard selector are set, the resulting informer only watches resources
80+
* matching both (the two selectors are combined with a logical AND).
81+
*
82+
* @return the shard selector
83+
*/
84+
String shardSelector() default NO_VALUE_SET;
85+
7486
/**
7587
* Optional {@link OnAddFilter} to filter add events sent to the associated informer
7688
*

operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/informer/InformerConfiguration.java

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ public class InformerConfiguration<R extends HasMetadata> {
4747
private Set<String> namespaces;
4848
private Boolean followControllerNamespaceChanges;
4949
private String labelSelector;
50+
private String shardSelector;
5051
private OnAddFilter<? super R> onAddFilter;
5152
private OnUpdateFilter<? super R> onUpdateFilter;
5253
private OnDeleteFilter<? super R> onDeleteFilter;
@@ -62,6 +63,7 @@ protected InformerConfiguration(
6263
Set<String> namespaces,
6364
boolean followControllerNamespaceChanges,
6465
String labelSelector,
66+
String shardSelector,
6567
OnAddFilter<? super R> onAddFilter,
6668
OnUpdateFilter<? super R> onUpdateFilter,
6769
OnDeleteFilter<? super R> onDeleteFilter,
@@ -77,6 +79,7 @@ protected InformerConfiguration(
7779
this.namespaces = namespaces;
7880
this.followControllerNamespaceChanges = followControllerNamespaceChanges;
7981
this.labelSelector = labelSelector;
82+
this.shardSelector = shardSelector;
8083
this.onAddFilter = onAddFilter;
8184
this.onUpdateFilter = onUpdateFilter;
8285
this.onDeleteFilter = onDeleteFilter;
@@ -113,6 +116,7 @@ public static <R extends HasMetadata> InformerConfiguration<R>.Builder builder(
113116
original.namespaces,
114117
original.followControllerNamespaceChanges,
115118
original.labelSelector,
119+
original.shardSelector,
116120
original.onAddFilter,
117121
original.onUpdateFilter,
118122
original.onDeleteFilter,
@@ -125,11 +129,6 @@ public static <R extends HasMetadata> InformerConfiguration<R>.Builder builder(
125129
.builder;
126130
}
127131

128-
public static String ensureValidLabelSelector(String labelSelector) {
129-
// might want to implement validation here?
130-
return labelSelector;
131-
}
132-
133132
public static boolean allNamespacesWatched(Set<String> namespaces) {
134133
failIfNotValid(namespaces);
135134
return DEFAULT_NAMESPACES_SET.equals(namespaces);
@@ -251,6 +250,20 @@ public String getLabelSelector() {
251250
return labelSelector;
252251
}
253252

253+
/**
254+
* Retrieves the shard selector that is used, in addition to the {@link #getLabelSelector() label
255+
* selector}, to restrict which resources are actually watched by the associated informer.
256+
* Typically used to assign a subset (shard) of the resources to a given operator instance. It is
257+
* expressed using the same syntax as a label selector. See the official documentation on the <a
258+
* href="https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/">topic</a> for
259+
* more details on syntax.
260+
*
261+
* @return the shard selector filtering watched resources
262+
*/
263+
public String getShardSelector() {
264+
return shardSelector;
265+
}
266+
254267
public OnAddFilter<? super R> getOnAddFilter() {
255268
return onAddFilter;
256269
}
@@ -353,6 +366,11 @@ public InformerConfiguration<R>.Builder initFromAnnotation(
353366
var labelSelector = Constants.NO_VALUE_SET.equals(fromAnnotation) ? null : fromAnnotation;
354367
withLabelSelector(labelSelector);
355368

369+
final var shardFromAnnotation = informerConfig.shardSelector();
370+
var shardSelector =
371+
Constants.NO_VALUE_SET.equals(shardFromAnnotation) ? null : shardFromAnnotation;
372+
withShardSelector(shardSelector);
373+
356374
withOnAddFilter(
357375
Utils.instantiate(informerConfig.onAddFilter(), OnAddFilter.class, context));
358376

@@ -442,7 +460,12 @@ public Builder withFollowControllerNamespacesChanges(boolean followChanges) {
442460
}
443461

444462
public Builder withLabelSelector(String labelSelector) {
445-
InformerConfiguration.this.labelSelector = ensureValidLabelSelector(labelSelector);
463+
InformerConfiguration.this.labelSelector = labelSelector;
464+
return this;
465+
}
466+
467+
public Builder withShardSelector(String shardSelector) {
468+
InformerConfiguration.this.shardSelector = shardSelector;
446469
return this;
447470
}
448471

operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/informer/InformerEventSourceConfiguration.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,11 @@ public Builder<R> withLabelSelector(String labelSelector) {
251251
return this;
252252
}
253253

254+
public Builder<R> withShardSelector(String shardSelector) {
255+
config.withShardSelector(shardSelector);
256+
return this;
257+
}
258+
254259
public Builder<R> withOnAddFilter(OnAddFilter<? super R> onAddFilter) {
255260
config.withOnAddFilter(onAddFilter);
256261
return this;
@@ -308,6 +313,7 @@ public void updateFrom(InformerConfiguration<R> informerConfig) {
308313
.withFollowControllerNamespacesChanges(
309314
informerConfig.getFollowControllerNamespaceChanges())
310315
.withLabelSelector(informerConfig.getLabelSelector())
316+
.withShardSelector(informerConfig.getShardSelector())
311317
.withItemStore(informerConfig.getItemStore())
312318
.withOnAddFilter(informerConfig.getOnAddFilter())
313319
.withOnUpdateFilter(informerConfig.getOnUpdateFilter())

operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerManager.java

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -136,13 +136,18 @@ public void changeNamespaces(Set<String> namespaces) {
136136
private InformerWrapper<R> createEventSourceForNamespace(String namespace) {
137137
final InformerWrapper<R> source;
138138
final var labelSelector = configuration.getInformerConfig().getLabelSelector();
139+
final var shardSelector = configuration.getInformerConfig().getShardSelector();
139140
if (namespace.equals(WATCH_ALL_NAMESPACES)) {
140-
final var filteredBySelectorClient = client.inAnyNamespace().withLabelSelector(labelSelector);
141+
final var filteredBySelectorClient =
142+
client.inAnyNamespace().withLabelSelector(labelSelector).withShardSelector(shardSelector);
141143
source = createEventSource(filteredBySelectorClient, eventHandler, WATCH_ALL_NAMESPACES);
142144
} else {
143145
source =
144146
createEventSource(
145-
client.inNamespace(namespace).withLabelSelector(labelSelector),
147+
client
148+
.inNamespace(namespace)
149+
.withLabelSelector(labelSelector)
150+
.withShardSelector(shardSelector),
146151
eventHandler,
147152
namespace);
148153
}
@@ -275,12 +280,14 @@ public List<R> byIndex(String indexName, String indexKey) {
275280
@Override
276281
public String toString() {
277282
final var informerConfig = configuration.getInformerConfig();
278-
final var selector = informerConfig.getLabelSelector();
283+
final var labelSelector = informerConfig.getLabelSelector();
284+
final var shardSelector = informerConfig.getShardSelector();
279285
return "InformerManager ["
280286
+ ReconcilerUtilsInternal.getResourceTypeNameWithVersion(configuration.getResourceClass())
281287
+ "] watching: "
282288
+ informerConfig.getEffectiveNamespaces(controllerConfiguration)
283-
+ (selector != null ? " selector: " + selector : "");
289+
+ (labelSelector != null ? " label selector: " + labelSelector : "")
290+
+ (shardSelector != null ? " shard selector: " + shardSelector : "");
284291
}
285292

286293
public Map<String, InformerHealthIndicator> informerHealthIndicators() {

operator-framework-core/src/test/java/io/javaoperatorsdk/operator/MockKubernetesClient.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ public static <T extends HasMetadata> KubernetesClient client(
8484
when(nonNamespaceOperation.withLabelSelector(nullable(String.class))).thenReturn(filterable);
8585
when(resources.inAnyNamespace()).thenReturn(inAnyNamespace);
8686
when(inAnyNamespace.withLabelSelector(nullable(String.class))).thenReturn(filterable);
87+
when(filterable.withShardSelector(nullable(String.class))).thenReturn(filterable);
8788
SharedIndexInformer<T> informer = mock(SharedIndexInformer.class);
8889
CompletableFuture<Void> informerStartRes = new CompletableFuture<>();
8990
informerStartRes.complete(null);

operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/ControllerConfigurationOverriderTest.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,20 @@ void itemStorePreserved() {
181181
assertNotNull(configuration.getInformerConfig().getItemStore());
182182
}
183183

184+
@Test
185+
void shardSelectorShouldBePropagated() {
186+
var configuration = createConfiguration(new WatchCurrentReconciler());
187+
assertNull(configuration.getInformerConfig().getShardSelector());
188+
189+
final var shardSelector = "shard=1";
190+
configuration =
191+
ControllerConfigurationOverrider.override(configuration)
192+
.withShardSelector(shardSelector)
193+
.build();
194+
195+
assertEquals(shardSelector, configuration.getInformerConfig().getShardSelector());
196+
}
197+
184198
@Test
185199
void configuredDependentShouldNotChangeOnParentOverrideEvenWhenInitialConfigIsSame() {
186200
var configuration = createConfiguration(new OverriddenNSOnDepReconciler());

operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/InformerConfigurationTest.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,19 @@ void nullLabelSelectorByDefault() {
7373
assertNull(informerConfig.getLabelSelector());
7474
}
7575

76+
@Test
77+
void nullShardSelectorByDefault() {
78+
final var informerConfig = InformerConfiguration.builder(ConfigMap.class).build();
79+
assertNull(informerConfig.getShardSelector());
80+
}
81+
82+
@Test
83+
void shardSelectorIsSetOnBuilder() {
84+
final var informerConfig =
85+
InformerConfiguration.builder(ConfigMap.class).withShardSelector("shard=1").build();
86+
assertEquals("shard=1", informerConfig.getShardSelector());
87+
}
88+
7689
@Test
7790
void shouldWatchAllNamespacesByDefaultForControllers() {
7891
final var informerConfig = InformerConfiguration.builder(ConfigMap.class).buildForController();

operator-framework/src/main/java/io/javaoperatorsdk/operator/config/loader/ConfigLoader.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,8 @@ public static ConfigLoader getDefault() {
143143
ControllerConfigurationOverrider::withGenerationAware),
144144
new ConfigBinding<>(
145145
"label-selector", String.class, ControllerConfigurationOverrider::withLabelSelector),
146+
new ConfigBinding<>(
147+
"shard-selector", String.class, ControllerConfigurationOverrider::withShardSelector),
146148
new ConfigBinding<>(
147149
"max-reconciliation-interval",
148150
Duration.class,
@@ -157,6 +159,10 @@ public static ConfigLoader getDefault() {
157159
"informer.label-selector",
158160
String.class,
159161
ControllerConfigurationOverrider::withLabelSelector),
162+
new ConfigBinding<>(
163+
"informer.shard-selector",
164+
String.class,
165+
ControllerConfigurationOverrider::withShardSelector),
160166
new ConfigBinding<>(
161167
"informer.list-limit",
162168
Long.class,

0 commit comments

Comments
 (0)