Skip to content

dynamic namespace config #1187

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 24 commits into from
May 5, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 63 additions & 1 deletion docs/documentation/features.md
Original file line number Diff line number Diff line change
Expand Up @@ -458,9 +458,71 @@ following attributes are available in most parts of reconciliation logic and dur

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

## Dynamically Changing Target Namespaces

A controller can be configured to watch a specific set of namespaces in addition of the
namespace in which it is currently deployed or the whole cluster. The framework supports
dynamically changing the list of these namespaces while the operator is running.
When a reconciler is registered, an instance of
[`RegisteredController`](https://github.com/java-operator-sdk/java-operator-sdk/blob/ec37025a15046d8f409c77616110024bf32c3416/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/RegisteredController.java#L5)
is returned, providing access to the methods allowing users to change watched namespaces as the
operator is running.

A typical scenario would probably involve extracting the list of target namespaces from a
`ConfigMap` or some other input but this part is out of the scope of the framework since this is
use-case specific. For example, reacting to changes to a `ConfigMap` would probably involve
registering an associated `Informer` and then calling the `changeNamespaces` method on
`RegisteredController`.

```java

public static void main(String[] args) throws IOException {
KubernetesClient client = new DefaultKubernetesClient();
Operator operator = new Operator(client);
RegisteredController registeredController = operator.register(new WebPageReconciler(client));
operator.installShutdownHook();
operator.start();

// call registeredController further while operator is running
}

```

If watched namespaces change for a controller, it might be desirable to propagate these changes to
`InformerEventSources` associated with the controller. In order to express this,
`InformerEventSource` implementations interested in following such changes need to be
configured appropriately so that the `followControllerNamespaceChanges` method returns `true`:

```java

@ControllerConfiguration
public class MyReconciler
implements Reconciler<TestCustomResource>, EventSourceInitializer<TestCustomResource>{

@Override
public Map<String, EventSource> prepareEventSources(
EventSourceContext<ChangeNamespaceTestCustomResource> context) {

InformerEventSource<ConfigMap, TestCustomResource> configMapES =
new InformerEventSource<>(InformerConfiguration.from(ConfigMap.class)
.withNamespacesInheritedFromController(context)
.build(), context);

return EventSourceInitializer.nameEventSources(configMapES);
}

}
```

As seen in the above code snippet, the informer will have the initial namespaces inherited from controller, but
also will adjust the target namespaces if it changes for the controller.

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)
for this feature.

## Monitoring with Micrometer

## Automatic generation of CRDs
## Automatic Generation of CRDs

Note that this is feature of [Fabric8 Kubernetes Client](https://github.com/fabric8io/kubernetes-client) not the JOSDK.
But it's worth to mention here.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -129,11 +129,11 @@ public void stop() throws OperatorException {
* @param <R> the {@code CustomResource} type associated with the reconciler
* @throws OperatorException if a problem occurred during the registration process
*/
public <R extends HasMetadata> void register(Reconciler<R> reconciler)
public <R extends HasMetadata> RegisteredController register(Reconciler<R> reconciler)
throws OperatorException {
final var controllerConfiguration =
ConfigurationServiceProvider.instance().getConfigurationFor(reconciler);
register(reconciler, controllerConfiguration);
return register(reconciler, controllerConfiguration);
}

/**
Expand All @@ -148,7 +148,7 @@ public <R extends HasMetadata> void register(Reconciler<R> reconciler)
* @param <R> the {@code HasMetadata} type associated with the reconciler
* @throws OperatorException if a problem occurred during the registration process
*/
public <R extends HasMetadata> void register(Reconciler<R> reconciler,
public <R extends HasMetadata> RegisteredController register(Reconciler<R> reconciler,
ControllerConfiguration<R> configuration)
throws OperatorException {

Expand All @@ -173,6 +173,7 @@ public <R extends HasMetadata> void register(Reconciler<R> reconciler,
configuration.getName(),
configuration.getResourceClass(),
watchedNS);
return controller;
}

/**
Expand All @@ -182,13 +183,13 @@ public <R extends HasMetadata> void register(Reconciler<R> reconciler,
* @param configOverrider consumer to use to change config values
* @param <R> the {@code HasMetadata} type associated with the reconciler
*/
public <R extends HasMetadata> void register(Reconciler<R> reconciler,
public <R extends HasMetadata> RegisteredController register(Reconciler<R> reconciler,
Consumer<ControllerConfigurationOverrider<R>> configOverrider) {
final var controllerConfiguration =
ConfigurationServiceProvider.instance().getConfigurationFor(reconciler);
var configToOverride = ControllerConfigurationOverrider.override(controllerConfiguration);
configOverrider.accept(configToOverride);
register(reconciler, configToOverride.build());
return register(reconciler, configToOverride.build());
}

static class ControllerManager implements LifecycleAware {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package io.javaoperatorsdk.operator;

import io.javaoperatorsdk.operator.api.config.NamespaceChangeable;

public interface RegisteredController extends NamespaceChangeable {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package io.javaoperatorsdk.operator.api.config;

import java.util.Set;

public interface NamespaceChangeable {

/**
* If the controller and possibly registered
* {@link io.javaoperatorsdk.operator.processing.event.source.informer.InformerEventSource}
* watches a set of namespaces this set can be adjusted dynamically, this when the operator is
* running.
*
* @param namespaces target namespaces to watch
*/
void changeNamespaces(Set<String> namespaces);

default void changeNamespaces(String... namespaces) {
changeNamespaces(
namespaces != null ? Set.of(namespaces) : ResourceConfiguration.DEFAULT_NAMESPACES);
}

default boolean allowsNamespaceChanges() {
return true;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@

public interface ResourceConfiguration<R extends HasMetadata> {

Set<String> DEFAULT_NAMESPACES = Set.of(Constants.WATCH_ALL_NAMESPACES);
Set<String> CURRENT_NAMESPACE_ONLY = Set.of(Constants.WATCH_CURRENT_NAMESPACE);
Set<String> DEFAULT_NAMESPACES = Collections.singleton(Constants.WATCH_ALL_NAMESPACES);
Set<String> CURRENT_NAMESPACE_ONLY = Collections.singleton(Constants.WATCH_CURRENT_NAMESPACE);

default String getResourceTypeName() {
return ReconcilerUtils.getResourceTypeName(getResourceClass());
Expand Down Expand Up @@ -64,7 +64,6 @@ static void failIfNotValid(Set<String> namespaces) {
return;
}
}

throw new IllegalArgumentException(
"Must specify namespaces. To watch all namespaces, use only '"
+ Constants.WATCH_ALL_NAMESPACES
Expand Down
Original file line number Diff line number Diff line change
@@ -1,41 +1,53 @@
package io.javaoperatorsdk.operator.api.config.informer;

import java.util.Collections;
import java.util.Objects;
import java.util.Set;

import io.fabric8.kubernetes.api.model.HasMetadata;
import io.javaoperatorsdk.operator.api.config.DefaultResourceConfiguration;
import io.javaoperatorsdk.operator.api.config.ResourceConfiguration;
import io.javaoperatorsdk.operator.api.reconciler.EventSourceContext;
import io.javaoperatorsdk.operator.processing.event.source.SecondaryToPrimaryMapper;
import io.javaoperatorsdk.operator.processing.event.source.informer.Mappers;

@SuppressWarnings("rawtypes")
public interface InformerConfiguration<R extends HasMetadata>
extends ResourceConfiguration<R> {

class DefaultInformerConfiguration<R extends HasMetadata> extends
DefaultResourceConfiguration<R> implements InformerConfiguration<R> {

private final SecondaryToPrimaryMapper<R> secondaryToPrimaryMapper;
private final boolean followControllerNamespaceChanges;

protected DefaultInformerConfiguration(String labelSelector,
Class<R> resourceClass,
SecondaryToPrimaryMapper<R> secondaryToPrimaryMapper,
Set<String> namespaces) {
Set<String> namespaces, boolean followControllerNamespaceChanges) {
super(labelSelector, resourceClass, namespaces);
this.followControllerNamespaceChanges = followControllerNamespaceChanges;
this.secondaryToPrimaryMapper =
Objects.requireNonNullElse(secondaryToPrimaryMapper,
Mappers.fromOwnerReference());
}

public boolean followControllerNamespaceChanges() {
return followControllerNamespaceChanges;
}

public SecondaryToPrimaryMapper<R> getSecondaryToPrimaryMapper() {
return secondaryToPrimaryMapper;
}

}

/**
* Used in case the watched namespaces are changed dynamically, thus when operator is running (See
* {@link io.javaoperatorsdk.operator.RegisteredController}). If true, changing the target
* namespaces of a controller would result to change target namespaces for the
* InformerEventSource.
*/
boolean followControllerNamespaceChanges();

SecondaryToPrimaryMapper<R> getSecondaryToPrimaryMapper();

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

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

public InformerConfigurationBuilder<R> withNamespaces(String... namespaces) {
this.namespaces = namespaces != null ? Set.of(namespaces) : Collections.emptySet();
return this;
return withNamespaces(
namespaces != null ? Set.of(namespaces) : ResourceConfiguration.DEFAULT_NAMESPACES);
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This DEFAULT_NAMESPACES is a little confusing. Should we rename this to WATCH_ALL_NAMESPACES_ON_CLUSTER or such?

Since it is already defined as:
Set<String> DEFAULT_NAMESPACES = Collections.singleton(Constants.WATCH_ALL_NAMESPACES);

see:
https://github.com/java-operator-sdk/java-operator-sdk/blob/b9ee05e6e3c803be0b1e95633f465a632bd1dd52/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ResourceConfiguration.java#L13-L14

@metacosm

Copy link
Collaborator

@metacosm metacosm May 5, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I used DEFAULT_NAMESPACES because I thought it'd be a good idea to decouple the notion of what is used for the default value from the actual value so that it's clear at the usage site that we're using the default value because that's what should be used at that spot instead of an arbitrary value.

I do agree that it's a little confusing but I think it's better to keep this semantics to make it explicit that the default value is used at these spots. This way, the code using the value should also not change if the default value were to change.

Copy link
Collaborator Author

@csviri csviri May 5, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes that makes sense, I just I think found some places where it was not used, just the value directly, will rather then create PR for that if I find it again (can't remember where it was)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

will take a look

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, that's possible, unfortunately, ran into a couple of such occurrences myself as well and tried to fix them when I could.

}

public InformerConfigurationBuilder<R> withNamespaces(Set<String> namespaces) {
this.namespaces = namespaces != null ? namespaces : Collections.emptySet();
return withNamespaces(namespaces, false);
}

/**
* Sets the initial set of namespaces to watch (typically extracted from the parent
* {@link io.javaoperatorsdk.operator.processing.Controller}'s configuration), specifying
* whether changes made to the parent controller configured namespaces should be tracked or not.
*
* @param namespaces the initial set of namespaces to watch
* @param followChanges {@code true} to follow the changes made to the parent controller
* namespaces, {@code false} otherwise
* @return the builder instance so that calls can be chained fluently
*/
public InformerConfigurationBuilder<R> withNamespaces(Set<String> namespaces,
boolean followChanges) {
this.namespaces = namespaces != null ? namespaces : ResourceConfiguration.DEFAULT_NAMESPACES;
this.inheritControllerNamespacesOnChange = true;
return this;
}

/**
* Configures the informer to watch and track the same namespaces as the parent
* {@link io.javaoperatorsdk.operator.processing.Controller}, meaning that the informer will be
* restarted to watch the new namespaces if the parent controller's namespace configuration
* changes.
*
* @param context {@link EventSourceContext} from which the parent
* {@link io.javaoperatorsdk.operator.processing.Controller}'s configuration is retrieved
* @param <P> the primary resource type associated with the parent controller
* @return the builder instance so that calls can be chained fluently
*/
public <P extends HasMetadata> InformerConfigurationBuilder<R> withNamespacesInheritedFromController(
EventSourceContext<P> context) {
namespaces = context.getControllerConfiguration().getEffectiveNamespaces();
this.inheritControllerNamespacesOnChange = true;
return this;
}

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

public InformerConfigurationBuilder<R> withLabelSelector(String labelSelector) {
this.labelSelector = labelSelector;
Expand All @@ -75,7 +134,7 @@ public InformerConfigurationBuilder<R> withLabelSelector(String labelSelector) {
public InformerConfiguration<R> build() {
return new DefaultInformerConfiguration<>(labelSelector, resourceClass,
secondaryToPrimaryMapper,
namespaces);
namespaces, inheritControllerNamespacesOnChange);
}
}

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


/**
* Creates a configuration builder that inherits namespaces from the controller and follows
* namespaces changes.
*
* @param resourceClass secondary resource class
* @param eventSourceContext of the initializer
* @return builder
* @param <R> secondary resource type
*/
static <R extends HasMetadata> InformerConfigurationBuilder<R> from(
InformerConfiguration<R> configuration) {
return new InformerConfigurationBuilder<R>(configuration.getResourceClass())
.withNamespaces(configuration.getNamespaces())
.withLabelSelector(configuration.getLabelSelector())
.withSecondaryToPrimaryMapper(configuration.getSecondaryToPrimaryMapper());
Class<R> resourceClass, EventSourceContext<?> eventSourceContext) {
return new InformerConfigurationBuilder<>(resourceClass)
.withNamespacesInheritedFromController(eventSourceContext);
}

}
Loading