Skip to content

feat: make default watched namespaces behavior explicit #1177

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 2 commits into from
Apr 22, 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
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,8 @@ public boolean isGenerationAware() {

@Override
public Set<String> getNamespaces() {
return Set.of(valueOrDefault(annotation, ControllerConfiguration::namespaces, new String[] {}));
return Set.of(valueOrDefault(annotation, ControllerConfiguration::namespaces,
new String[] {Constants.WATCH_ALL_NAMESPACES}));
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ public class ControllerConfigurationOverrider<R extends HasMetadata> {

private String finalizer;
private boolean generationAware;
private final Set<String> namespaces;
private Set<String> namespaces;
private RetryConfiguration retry;
private String labelSelector;
private ResourceEventFilter<R> customResourcePredicate;
Expand Down Expand Up @@ -52,8 +52,8 @@ public ControllerConfigurationOverrider<R> withGenerationAware(boolean generatio
return this;
}

public ControllerConfigurationOverrider<R> withCurrentNamespace() {
namespaces.clear();
public ControllerConfigurationOverrider<R> watchingOnlyCurrentNamespace() {
this.namespaces = ResourceConfiguration.CURRENT_NAMESPACE_ONLY;
return this;
}

Expand All @@ -64,6 +64,9 @@ public ControllerConfigurationOverrider<R> addingNamespaces(String... namespaces

public ControllerConfigurationOverrider<R> removingNamespaces(String... namespaces) {
List.of(namespaces).forEach(this.namespaces::remove);
if (this.namespaces.isEmpty()) {
this.namespaces = ResourceConfiguration.DEFAULT_NAMESPACES;
}
return this;
}

Expand All @@ -73,6 +76,11 @@ public ControllerConfigurationOverrider<R> settingNamespace(String namespace) {
return this;
}

public ControllerConfigurationOverrider<R> watchingAllNamespaces() {
this.namespaces = ResourceConfiguration.DEFAULT_NAMESPACES;
return this;
}

public ControllerConfigurationOverrider<R> withRetry(RetryConfiguration retry) {
this.retry = retry;
return this;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package io.javaoperatorsdk.operator.api.config;

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

import io.fabric8.kubernetes.api.model.HasMetadata;
Expand All @@ -10,21 +9,22 @@ public class DefaultResourceConfiguration<R extends HasMetadata>

private final String labelSelector;
private final Set<String> namespaces;
private final boolean watchAllNamespaces;
private final Class<R> resourceClass;

public DefaultResourceConfiguration(String labelSelector, Class<R> resourceClass,
String... namespaces) {
this(labelSelector, resourceClass,
namespaces != null ? Set.of(namespaces) : Collections.emptySet());
namespaces == null || namespaces.length == 0 ? ResourceConfiguration.DEFAULT_NAMESPACES
: Set.of(namespaces));
}

public DefaultResourceConfiguration(String labelSelector, Class<R> resourceClass,
Set<String> namespaces) {
this.labelSelector = labelSelector;
this.resourceClass = resourceClass;
this.namespaces = namespaces != null ? namespaces : Collections.emptySet();
this.watchAllNamespaces = this.namespaces.isEmpty();
this.namespaces =
namespaces == null || namespaces.isEmpty() ? ResourceConfiguration.DEFAULT_NAMESPACES
: namespaces;
}

@Override
Expand All @@ -42,11 +42,6 @@ public Set<String> getNamespaces() {
return namespaces;
}

@Override
public boolean watchAllNamespaces() {
return watchAllNamespaces;
}

@Override
public Class<R> getResourceClass() {
return resourceClass;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,18 @@

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);

default String getResourceTypeName() {
return ReconcilerUtils.getResourceTypeName(getResourceClass());
}

/**
* Retrieves the label selector that is used to filter which resources are actually watched by the
* associated event source. See
* https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/ for more details on
* syntax.
* associated event source. See the official documentation on the
* <a href="https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/">topic</a>
* for more details on syntax.
*
* @return the label selector filtering watched resources
*/
Expand All @@ -32,25 +35,41 @@ default Class<R> getResourceClass() {
}

default Set<String> getNamespaces() {
return Collections.emptySet();
return DEFAULT_NAMESPACES;
}

default boolean watchAllNamespaces() {
return allNamespacesWatched(getNamespaces());
}

static boolean allNamespacesWatched(Set<String> namespaces) {
return namespaces == null || namespaces.isEmpty();
failIfNotValid(namespaces);
return DEFAULT_NAMESPACES.equals(namespaces);
}

default boolean watchCurrentNamespace() {
return currentNamespaceWatched(getNamespaces());
}

static boolean currentNamespaceWatched(Set<String> namespaces) {
return namespaces != null
&& namespaces.size() == 1
&& namespaces.contains(Constants.WATCH_CURRENT_NAMESPACE);
failIfNotValid(namespaces);
return CURRENT_NAMESPACE_ONLY.equals(namespaces);
}

static void failIfNotValid(Set<String> namespaces) {
if (namespaces != null && !namespaces.isEmpty()) {
final var present = namespaces.contains(Constants.WATCH_CURRENT_NAMESPACE)
|| namespaces.contains(Constants.WATCH_ALL_NAMESPACES);
if (!present || namespaces.size() == 1) {
return;
}
}

throw new IllegalArgumentException(
"Must specify namespaces. To watch all namespaces, use only '"
+ Constants.WATCH_ALL_NAMESPACES
+ "'. To watch only the namespace in which the operator is deployed, use only '"
+ Constants.WATCH_CURRENT_NAMESPACE + "'");
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ public final class Constants {

public static final String NO_VALUE_SET = "";
public static final String WATCH_CURRENT_NAMESPACE = "JOSDK_WATCH_CURRENT";
public static final String WATCH_ALL_NAMESPACES = "JOSDK_ALL_NAMESPACES";
public static final long NO_RECONCILIATION_MAX_INTERVAL = -1L;

private Constants() {}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
*
* @return the list of namespaces this controller monitors
*/
String[] namespaces() default {};
String[] namespaces() default Constants.WATCH_ALL_NAMESPACES;

/**
* Optional label selector used to identify the set of custom resources the controller will acc
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
@Target({ElementType.TYPE})
public @interface KubernetesDependent {
String SAME_AS_PARENT = "JOSDK_SAME_AS_PARENT";
String WATCH_ALL_NAMESPACES = "JOSDK_ALL_NAMESPACES";

String[] DEFAULT_NAMESPACES = {SAME_AS_PARENT};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,7 @@ public KubernetesDependentResourceConfig setLabelSelector(String labelSelector)
}

public Set<String> namespaces() {
if (!namespaces.contains(KubernetesDependent.WATCH_ALL_NAMESPACES)) {
return namespaces;
} else {
return Collections.emptySet();
}
return namespaces;
}

public String labelSelector() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,20 +10,17 @@
import io.fabric8.kubernetes.client.KubernetesClient;
import io.javaoperatorsdk.operator.api.config.AbstractConfigurationService;
import io.javaoperatorsdk.operator.api.config.ConfigurationServiceProvider;
import io.javaoperatorsdk.operator.api.config.ControllerConfiguration;
import io.javaoperatorsdk.operator.api.config.MockControllerConfiguration;
import io.javaoperatorsdk.operator.api.reconciler.Context;
import io.javaoperatorsdk.operator.api.reconciler.Reconciler;
import io.javaoperatorsdk.operator.api.reconciler.UpdateControl;

import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

@SuppressWarnings("rawtypes")
class OperatorTest {

private final KubernetesClient kubernetesClient = MockKubernetesClient.client(ConfigMap.class);
private final ControllerConfiguration configuration = mock(ControllerConfiguration.class);
private final Operator operator = new Operator(kubernetesClient);
private final FooReconciler fooReconciler = new FooReconciler();

Expand All @@ -35,10 +32,9 @@ static void setUpConfigurationServiceProvider() {

@Test
@DisplayName("should register `Reconciler` to Controller")
@SuppressWarnings("unchecked")
public void shouldRegisterReconcilerToController() {
// given
when(configuration.getResourceClass()).thenReturn(ConfigMap.class);
final var configuration = MockControllerConfiguration.forResource(ConfigMap.class);

// when
operator.register(fooReconciler, configuration);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import org.junit.jupiter.api.Test;

import io.fabric8.kubernetes.api.model.ConfigMap;
import io.javaoperatorsdk.operator.api.reconciler.Constants;
import io.javaoperatorsdk.operator.api.reconciler.Context;
import io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration;
import io.javaoperatorsdk.operator.api.reconciler.Reconciler;
Expand Down Expand Up @@ -107,6 +108,63 @@ private io.javaoperatorsdk.operator.api.config.ControllerConfiguration<?> create
return new AnnotationControllerConfiguration<>(reconciler);
}

@ControllerConfiguration(namespaces = "foo")
private static class WatchCurrentReconciler implements Reconciler<ConfigMap> {

@Override
public UpdateControl<ConfigMap> reconcile(ConfigMap resource, Context<ConfigMap> context)
throws Exception {
return null;
}
}

@Test
void overridingNamespacesShouldWork() {
var configuration = createConfiguration(new WatchCurrentReconciler());
assertEquals(Set.of("foo"), configuration.getNamespaces());
assertFalse(configuration.watchAllNamespaces());
assertFalse(configuration.watchCurrentNamespace());

configuration = ControllerConfigurationOverrider.override(configuration)
.addingNamespaces("foo", "bar")
.build();
assertEquals(Set.of("foo", "bar"), configuration.getNamespaces());
assertFalse(configuration.watchAllNamespaces());
assertFalse(configuration.watchCurrentNamespace());

configuration = ControllerConfigurationOverrider.override(configuration)
.removingNamespaces("bar")
.build();
assertEquals(Set.of("foo"), configuration.getNamespaces());
assertFalse(configuration.watchAllNamespaces());
assertFalse(configuration.watchCurrentNamespace());

configuration = ControllerConfigurationOverrider.override(configuration)
.removingNamespaces("foo")
.build();
assertTrue(configuration.watchAllNamespaces());
assertFalse(configuration.watchCurrentNamespace());

configuration = ControllerConfigurationOverrider.override(configuration)
.settingNamespace("foo")
.build();
assertFalse(configuration.watchAllNamespaces());
assertFalse(configuration.watchCurrentNamespace());
assertEquals(Set.of("foo"), configuration.getNamespaces());

configuration = ControllerConfigurationOverrider.override(configuration)
.watchingOnlyCurrentNamespace()
.build();
assertFalse(configuration.watchAllNamespaces());
assertTrue(configuration.watchCurrentNamespace());

configuration = ControllerConfigurationOverrider.override(configuration)
.watchingAllNamespaces()
.build();
assertTrue(configuration.watchAllNamespaces());
assertFalse(configuration.watchCurrentNamespace());
}

@Test
void configuredDependentShouldNotChangeOnParentOverrideEvenWhenInitialConfigIsSame() {
var configuration = createConfiguration(new OverriddenNSOnDepReconciler());
Expand Down Expand Up @@ -140,7 +198,7 @@ void dependentShouldWatchAllNamespacesIfParentDoesAsWell() {
var config = extractFirstDependentKubernetesResourceConfig(configuration);

// check that the DependentResource inherits the controller's configuration if applicable
assertEquals(0, config.namespaces().size());
assertTrue(ResourceConfiguration.allNamespacesWatched(config.namespaces()));

// override the NS
final var newNS = "bar";
Expand All @@ -160,7 +218,7 @@ void shouldBePossibleToForceDependentToWatchAllNamespaces() {
var config = extractFirstDependentKubernetesResourceConfig(configuration);

// check that the DependentResource inherits the controller's configuration if applicable
assertEquals(0, config.namespaces().size());
assertTrue(ResourceConfiguration.allNamespacesWatched(config.namespaces()));

// override the NS
final var newNS = "bar";
Expand All @@ -169,7 +227,7 @@ void shouldBePossibleToForceDependentToWatchAllNamespaces() {

// check that dependent config is still configured to watch all NS
config = extractFirstDependentKubernetesResourceConfig(configuration);
assertEquals(0, config.namespaces().size());
assertTrue(ResourceConfiguration.allNamespacesWatched(config.namespaces()));
}

@Test
Expand Down Expand Up @@ -224,7 +282,8 @@ void replaceNamedDependentResourceConfigShouldWork() {
final var dependentResourceName = DependentResource.defaultNameFor(ReadOnlyDependent.class);
assertTrue(dependents.stream().anyMatch(dr -> dr.getName().equals(dependentResourceName)));

var dependentSpec = dependents.stream().filter(dr -> dr.getName().equals(dependentResourceName))
var dependentSpec = dependents.stream()
.filter(dr -> dr.getName().equals(dependentResourceName))
.findFirst().get();
assertEquals(ReadOnlyDependent.class, dependentSpec.getDependentResourceClass());
var maybeConfig = dependentSpec.getDependentResourceConfiguration();
Expand Down Expand Up @@ -292,7 +351,7 @@ public ReadOnlyDependent() {
}
}

@KubernetesDependent(namespaces = KubernetesDependent.WATCH_ALL_NAMESPACES)
@KubernetesDependent(namespaces = Constants.WATCH_ALL_NAMESPACES)
private static class WatchAllNSDependent
extends KubernetesDependentResource<ConfigMap, ConfigMap> {

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package io.javaoperatorsdk.operator.api.config;

import io.fabric8.kubernetes.api.model.HasMetadata;

import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

public class MockControllerConfiguration {
@SuppressWarnings({"unchecked", "rawtypes"})
public static <R extends HasMetadata> ControllerConfiguration<R> forResource(
Class<R> resourceType) {
final ControllerConfiguration configuration = mock(ControllerConfiguration.class);
when(configuration.getResourceClass()).thenReturn(resourceType);
when(configuration.getNamespaces()).thenReturn(ResourceConfiguration.DEFAULT_NAMESPACES);
when(configuration.getEffectiveNamespaces()).thenCallRealMethod();
return configuration;
}
}
Loading