Skip to content

Commit ef2c5f7

Browse files
csvirimetacosm
andauthored
dynamic namespace config (#1187)
Co-authored-by: Chris Laprun <[email protected]>
1 parent 80f631e commit ef2c5f7

File tree

26 files changed

+628
-146
lines changed

26 files changed

+628
-146
lines changed

docs/documentation/features.md

+63-1
Original file line numberDiff line numberDiff line change
@@ -458,9 +458,71 @@ following attributes are available in most parts of reconciliation logic and dur
458458

459459
For more information about MDC see this [link](https://www.baeldung.com/mdc-in-log4j-2-logback).
460460

461+
## Dynamically Changing Target Namespaces
462+
463+
A controller can be configured to watch a specific set of namespaces in addition of the
464+
namespace in which it is currently deployed or the whole cluster. The framework supports
465+
dynamically changing the list of these namespaces while the operator is running.
466+
When a reconciler is registered, an instance of
467+
[`RegisteredController`](https://github.com/java-operator-sdk/java-operator-sdk/blob/ec37025a15046d8f409c77616110024bf32c3416/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/RegisteredController.java#L5)
468+
is returned, providing access to the methods allowing users to change watched namespaces as the
469+
operator is running.
470+
471+
A typical scenario would probably involve extracting the list of target namespaces from a
472+
`ConfigMap` or some other input but this part is out of the scope of the framework since this is
473+
use-case specific. For example, reacting to changes to a `ConfigMap` would probably involve
474+
registering an associated `Informer` and then calling the `changeNamespaces` method on
475+
`RegisteredController`.
476+
477+
```java
478+
479+
public static void main(String[] args) throws IOException {
480+
KubernetesClient client = new DefaultKubernetesClient();
481+
Operator operator = new Operator(client);
482+
RegisteredController registeredController = operator.register(new WebPageReconciler(client));
483+
operator.installShutdownHook();
484+
operator.start();
485+
486+
// call registeredController further while operator is running
487+
}
488+
489+
```
490+
491+
If watched namespaces change for a controller, it might be desirable to propagate these changes to
492+
`InformerEventSources` associated with the controller. In order to express this,
493+
`InformerEventSource` implementations interested in following such changes need to be
494+
configured appropriately so that the `followControllerNamespaceChanges` method returns `true`:
495+
496+
```java
497+
498+
@ControllerConfiguration
499+
public class MyReconciler
500+
implements Reconciler<TestCustomResource>, EventSourceInitializer<TestCustomResource>{
501+
502+
@Override
503+
public Map<String, EventSource> prepareEventSources(
504+
EventSourceContext<ChangeNamespaceTestCustomResource> context) {
505+
506+
InformerEventSource<ConfigMap, TestCustomResource> configMapES =
507+
new InformerEventSource<>(InformerConfiguration.from(ConfigMap.class)
508+
.withNamespacesInheritedFromController(context)
509+
.build(), context);
510+
511+
return EventSourceInitializer.nameEventSources(configMapES);
512+
}
513+
514+
}
515+
```
516+
517+
As seen in the above code snippet, the informer will have the initial namespaces inherited from controller, but
518+
also will adjust the target namespaces if it changes for the controller.
519+
520+
See also the [integration test](https://github.com/java-operator-sdk/java-operator-sdk/blob/ec37025a15046d8f409c77616110024bf32c3416/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/changenamespace/ChangeNamespaceTestReconciler.java)
521+
for this feature.
522+
461523
## Monitoring with Micrometer
462524

463-
## Automatic generation of CRDs
525+
## Automatic Generation of CRDs
464526

465527
Note that this is feature of [Fabric8 Kubernetes Client](https://github.com/fabric8io/kubernetes-client) not the JOSDK.
466528
But it's worth to mention here.

operator-framework-core/src/main/java/io/javaoperatorsdk/operator/Operator.java

+6-5
Original file line numberDiff line numberDiff line change
@@ -123,11 +123,11 @@ public void stop() throws OperatorException {
123123
* @param <R> the {@code CustomResource} type associated with the reconciler
124124
* @throws OperatorException if a problem occurred during the registration process
125125
*/
126-
public <R extends HasMetadata> void register(Reconciler<R> reconciler)
126+
public <R extends HasMetadata> RegisteredController register(Reconciler<R> reconciler)
127127
throws OperatorException {
128128
final var controllerConfiguration =
129129
ConfigurationServiceProvider.instance().getConfigurationFor(reconciler);
130-
register(reconciler, controllerConfiguration);
130+
return register(reconciler, controllerConfiguration);
131131
}
132132

133133
/**
@@ -142,7 +142,7 @@ public <R extends HasMetadata> void register(Reconciler<R> reconciler)
142142
* @param <R> the {@code HasMetadata} type associated with the reconciler
143143
* @throws OperatorException if a problem occurred during the registration process
144144
*/
145-
public <R extends HasMetadata> void register(Reconciler<R> reconciler,
145+
public <R extends HasMetadata> RegisteredController register(Reconciler<R> reconciler,
146146
ControllerConfiguration<R> configuration)
147147
throws OperatorException {
148148

@@ -167,6 +167,7 @@ public <R extends HasMetadata> void register(Reconciler<R> reconciler,
167167
configuration.getName(),
168168
configuration.getResourceClass(),
169169
watchedNS);
170+
return controller;
170171
}
171172

172173
/**
@@ -176,13 +177,13 @@ public <R extends HasMetadata> void register(Reconciler<R> reconciler,
176177
* @param configOverrider consumer to use to change config values
177178
* @param <R> the {@code HasMetadata} type associated with the reconciler
178179
*/
179-
public <R extends HasMetadata> void register(Reconciler<R> reconciler,
180+
public <R extends HasMetadata> RegisteredController register(Reconciler<R> reconciler,
180181
Consumer<ControllerConfigurationOverrider<R>> configOverrider) {
181182
final var controllerConfiguration =
182183
ConfigurationServiceProvider.instance().getConfigurationFor(reconciler);
183184
var configToOverride = ControllerConfigurationOverrider.override(controllerConfiguration);
184185
configOverrider.accept(configToOverride);
185-
register(reconciler, configToOverride.build());
186+
return register(reconciler, configToOverride.build());
186187
}
187188

188189
static class ControllerManager implements LifecycleAware {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
package io.javaoperatorsdk.operator;
2+
3+
import io.javaoperatorsdk.operator.api.config.NamespaceChangeable;
4+
5+
public interface RegisteredController extends NamespaceChangeable {
6+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package io.javaoperatorsdk.operator.api.config;
2+
3+
import java.util.Set;
4+
5+
public interface NamespaceChangeable {
6+
7+
/**
8+
* If the controller and possibly registered
9+
* {@link io.javaoperatorsdk.operator.processing.event.source.informer.InformerEventSource}
10+
* watches a set of namespaces this set can be adjusted dynamically, this when the operator is
11+
* running.
12+
*
13+
* @param namespaces target namespaces to watch
14+
*/
15+
void changeNamespaces(Set<String> namespaces);
16+
17+
default void changeNamespaces(String... namespaces) {
18+
changeNamespaces(
19+
namespaces != null ? Set.of(namespaces) : ResourceConfiguration.DEFAULT_NAMESPACES);
20+
}
21+
22+
default boolean allowsNamespaceChanges() {
23+
return true;
24+
}
25+
26+
}

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

+2-3
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@
1010

1111
public interface ResourceConfiguration<R extends HasMetadata> {
1212

13-
Set<String> DEFAULT_NAMESPACES = Set.of(Constants.WATCH_ALL_NAMESPACES);
14-
Set<String> CURRENT_NAMESPACE_ONLY = Set.of(Constants.WATCH_CURRENT_NAMESPACE);
13+
Set<String> DEFAULT_NAMESPACES = Collections.singleton(Constants.WATCH_ALL_NAMESPACES);
14+
Set<String> CURRENT_NAMESPACE_ONLY = Collections.singleton(Constants.WATCH_CURRENT_NAMESPACE);
1515

1616
default String getResourceTypeName() {
1717
return ReconcilerUtils.getResourceTypeName(getResourceClass());
@@ -64,7 +64,6 @@ static void failIfNotValid(Set<String> namespaces) {
6464
return;
6565
}
6666
}
67-
6867
throw new IllegalArgumentException(
6968
"Must specify namespaces. To watch all namespaces, use only '"
7069
+ Constants.WATCH_ALL_NAMESPACES
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,53 @@
11
package io.javaoperatorsdk.operator.api.config.informer;
22

3-
import java.util.Collections;
43
import java.util.Objects;
54
import java.util.Set;
65

76
import io.fabric8.kubernetes.api.model.HasMetadata;
87
import io.javaoperatorsdk.operator.api.config.DefaultResourceConfiguration;
98
import io.javaoperatorsdk.operator.api.config.ResourceConfiguration;
9+
import io.javaoperatorsdk.operator.api.reconciler.EventSourceContext;
1010
import io.javaoperatorsdk.operator.processing.event.source.SecondaryToPrimaryMapper;
1111
import io.javaoperatorsdk.operator.processing.event.source.informer.Mappers;
1212

13-
@SuppressWarnings("rawtypes")
1413
public interface InformerConfiguration<R extends HasMetadata>
1514
extends ResourceConfiguration<R> {
1615

1716
class DefaultInformerConfiguration<R extends HasMetadata> extends
1817
DefaultResourceConfiguration<R> implements InformerConfiguration<R> {
1918

2019
private final SecondaryToPrimaryMapper<R> secondaryToPrimaryMapper;
20+
private final boolean followControllerNamespaceChanges;
2121

2222
protected DefaultInformerConfiguration(String labelSelector,
2323
Class<R> resourceClass,
2424
SecondaryToPrimaryMapper<R> secondaryToPrimaryMapper,
25-
Set<String> namespaces) {
25+
Set<String> namespaces, boolean followControllerNamespaceChanges) {
2626
super(labelSelector, resourceClass, namespaces);
27+
this.followControllerNamespaceChanges = followControllerNamespaceChanges;
2728
this.secondaryToPrimaryMapper =
2829
Objects.requireNonNullElse(secondaryToPrimaryMapper,
2930
Mappers.fromOwnerReference());
3031
}
3132

33+
public boolean followControllerNamespaceChanges() {
34+
return followControllerNamespaceChanges;
35+
}
3236

3337
public SecondaryToPrimaryMapper<R> getSecondaryToPrimaryMapper() {
3438
return secondaryToPrimaryMapper;
3539
}
3640

3741
}
3842

43+
/**
44+
* Used in case the watched namespaces are changed dynamically, thus when operator is running (See
45+
* {@link io.javaoperatorsdk.operator.RegisteredController}). If true, changing the target
46+
* namespaces of a controller would result to change target namespaces for the
47+
* InformerEventSource.
48+
*/
49+
boolean followControllerNamespaceChanges();
50+
3951
SecondaryToPrimaryMapper<R> getSecondaryToPrimaryMapper();
4052

4153
@SuppressWarnings("unused")
@@ -45,6 +57,7 @@ class InformerConfigurationBuilder<R extends HasMetadata> {
4557
private Set<String> namespaces;
4658
private String labelSelector;
4759
private final Class<R> resourceClass;
60+
private boolean inheritControllerNamespacesOnChange = false;
4861

4962
private InformerConfigurationBuilder(Class<R> resourceClass) {
5063
this.resourceClass = resourceClass;
@@ -57,15 +70,61 @@ public InformerConfigurationBuilder<R> withSecondaryToPrimaryMapper(
5770
}
5871

5972
public InformerConfigurationBuilder<R> withNamespaces(String... namespaces) {
60-
this.namespaces = namespaces != null ? Set.of(namespaces) : Collections.emptySet();
61-
return this;
73+
return withNamespaces(
74+
namespaces != null ? Set.of(namespaces) : ResourceConfiguration.DEFAULT_NAMESPACES);
6275
}
6376

6477
public InformerConfigurationBuilder<R> withNamespaces(Set<String> namespaces) {
65-
this.namespaces = namespaces != null ? namespaces : Collections.emptySet();
78+
return withNamespaces(namespaces, false);
79+
}
80+
81+
/**
82+
* Sets the initial set of namespaces to watch (typically extracted from the parent
83+
* {@link io.javaoperatorsdk.operator.processing.Controller}'s configuration), specifying
84+
* whether changes made to the parent controller configured namespaces should be tracked or not.
85+
*
86+
* @param namespaces the initial set of namespaces to watch
87+
* @param followChanges {@code true} to follow the changes made to the parent controller
88+
* namespaces, {@code false} otherwise
89+
* @return the builder instance so that calls can be chained fluently
90+
*/
91+
public InformerConfigurationBuilder<R> withNamespaces(Set<String> namespaces,
92+
boolean followChanges) {
93+
this.namespaces = namespaces != null ? namespaces : ResourceConfiguration.DEFAULT_NAMESPACES;
94+
this.inheritControllerNamespacesOnChange = true;
95+
return this;
96+
}
97+
98+
/**
99+
* Configures the informer to watch and track the same namespaces as the parent
100+
* {@link io.javaoperatorsdk.operator.processing.Controller}, meaning that the informer will be
101+
* restarted to watch the new namespaces if the parent controller's namespace configuration
102+
* changes.
103+
*
104+
* @param context {@link EventSourceContext} from which the parent
105+
* {@link io.javaoperatorsdk.operator.processing.Controller}'s configuration is retrieved
106+
* @param <P> the primary resource type associated with the parent controller
107+
* @return the builder instance so that calls can be chained fluently
108+
*/
109+
public <P extends HasMetadata> InformerConfigurationBuilder<R> withNamespacesInheritedFromController(
110+
EventSourceContext<P> context) {
111+
namespaces = context.getControllerConfiguration().getEffectiveNamespaces();
112+
this.inheritControllerNamespacesOnChange = true;
66113
return this;
67114
}
68115

116+
/**
117+
* Whether or not the associated informer should track changes made to the parent
118+
* {@link io.javaoperatorsdk.operator.processing.Controller}'s namespaces configuration.
119+
*
120+
* @param followChanges {@code true} to reconfigure the associated informer when the parent
121+
* controller's namespaces are reconfigured, {@code false} otherwise
122+
* @return the builder instance so that calls can be chained fluently
123+
*/
124+
public InformerConfigurationBuilder<R> followNamespaceChanges(boolean followChanges) {
125+
this.inheritControllerNamespacesOnChange = followChanges;
126+
return this;
127+
}
69128

70129
public InformerConfigurationBuilder<R> withLabelSelector(String labelSelector) {
71130
this.labelSelector = labelSelector;
@@ -75,7 +134,7 @@ public InformerConfigurationBuilder<R> withLabelSelector(String labelSelector) {
75134
public InformerConfiguration<R> build() {
76135
return new DefaultInformerConfiguration<>(labelSelector, resourceClass,
77136
secondaryToPrimaryMapper,
78-
namespaces);
137+
namespaces, inheritControllerNamespacesOnChange);
79138
}
80139
}
81140

@@ -84,12 +143,19 @@ static <R extends HasMetadata> InformerConfigurationBuilder<R> from(
84143
return new InformerConfigurationBuilder<>(resourceClass);
85144
}
86145

87-
146+
/**
147+
* Creates a configuration builder that inherits namespaces from the controller and follows
148+
* namespaces changes.
149+
*
150+
* @param resourceClass secondary resource class
151+
* @param eventSourceContext of the initializer
152+
* @return builder
153+
* @param <R> secondary resource type
154+
*/
88155
static <R extends HasMetadata> InformerConfigurationBuilder<R> from(
89-
InformerConfiguration<R> configuration) {
90-
return new InformerConfigurationBuilder<R>(configuration.getResourceClass())
91-
.withNamespaces(configuration.getNamespaces())
92-
.withLabelSelector(configuration.getLabelSelector())
93-
.withSecondaryToPrimaryMapper(configuration.getSecondaryToPrimaryMapper());
156+
Class<R> resourceClass, EventSourceContext<?> eventSourceContext) {
157+
return new InformerConfigurationBuilder<>(resourceClass)
158+
.withNamespacesInheritedFromController(eventSourceContext);
94159
}
160+
95161
}

0 commit comments

Comments
 (0)