Skip to content

Using Annotations to Identify primary for a secondary object if no owner reference can be added #1197

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 7 commits into from
May 11, 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
@@ -1,5 +1,6 @@
package io.javaoperatorsdk.operator.processing.dependent.kubernetes;

import java.util.HashMap;
import java.util.Optional;
import java.util.Set;

Expand All @@ -11,6 +12,7 @@
import io.fabric8.kubernetes.client.KubernetesClient;
import io.fabric8.kubernetes.client.dsl.NonNamespaceOperation;
import io.fabric8.kubernetes.client.dsl.Resource;
import io.javaoperatorsdk.operator.OperatorException;
import io.javaoperatorsdk.operator.api.config.informer.InformerConfiguration;
import io.javaoperatorsdk.operator.api.reconciler.Context;
import io.javaoperatorsdk.operator.api.reconciler.EventSourceContext;
Expand Down Expand Up @@ -57,18 +59,30 @@ public void configureWith(KubernetesDependentResourceConfig config) {
@SuppressWarnings("unchecked")
private void configureWith(String labelSelector, Set<String> namespaces,
boolean inheritNamespacesOnChange) {
final SecondaryToPrimaryMapper<R> primaryResourcesRetriever =
(this instanceof SecondaryToPrimaryMapper) ? (SecondaryToPrimaryMapper<R>) this
: Mappers.fromOwnerReference();

var ic = InformerConfiguration.from(resourceType())
.withLabelSelector(labelSelector)
.withSecondaryToPrimaryMapper(primaryResourcesRetriever)
.withSecondaryToPrimaryMapper(getSecondaryToPrimaryMapper())
.withNamespaces(namespaces, inheritNamespacesOnChange)
.build();

configureWith(new InformerEventSource<>(ic, client));
}

@SuppressWarnings("unchecked")
private SecondaryToPrimaryMapper<R> getSecondaryToPrimaryMapper() {
if (this instanceof SecondaryToPrimaryMapper) {
return (SecondaryToPrimaryMapper<R>) this;
} else if (garbageCollected) {
return Mappers.fromOwnerReference();
} else if (useDefaultAnnotationsToIdentifyPrimary()) {
return Mappers.fromDefaultAnnotations();
} else {
throw new OperatorException("Provide a SecondaryToPrimaryMapper to associate " +
"this resource with the primary resource. DependentResource: " + getClass().getName());
}
}

/**
* Use to share informers between event more resources.
*
Expand Down Expand Up @@ -129,6 +143,8 @@ protected NonNamespaceOperation<R, KubernetesResourceList<R>, Resource<R>> prepa
ResourceID.fromResource(desired));
if (addOwnerReference()) {
desired.addOwnerReference(primary);
} else if (useDefaultAnnotationsToIdentifyPrimary()) {
addDefaultSecondaryToPrimaryMapperAnnotations(desired, primary);
}
Class<R> targetClass = (Class<R>) desired.getClass();
return client.resources(targetClass).inNamespace(desired.getMetadata().getNamespace());
Expand All @@ -142,6 +158,24 @@ protected InformerEventSource<R, P> createEventSource(EventSourceContext<P> cont
return eventSource();
}

private boolean useDefaultAnnotationsToIdentifyPrimary() {
return !(this instanceof SecondaryToPrimaryMapper) && !garbageCollected && creatable;
}

private void addDefaultSecondaryToPrimaryMapperAnnotations(R desired, P primary) {
var annotations = desired.getMetadata().getAnnotations();
if (annotations == null) {
annotations = new HashMap<>();
desired.getMetadata().setAnnotations(annotations);
}
annotations.put(Mappers.DEFAULT_ANNOTATION_FOR_NAME, primary.getMetadata().getName());
var primaryNamespaces = primary.getMetadata().getNamespace();
if (primaryNamespaces != null) {
annotations.put(
Mappers.DEFAULT_ANNOTATION_FOR_NAMESPACE, primary.getMetadata().getNamespace());
}
}

protected boolean addOwnerReference() {
return garbageCollected;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ public KubernetesDependentResourceConfig(Set<String> namespaces, String labelSel
boolean configuredNS) {
this.namespaces = namespaces;
this.labelSelector = labelSelector;
namespacesWereConfigured = configuredNS;
this.namespacesWereConfigured = configuredNS;
}

public KubernetesDependentResourceConfig(Set<String> namespaces, String labelSelector) {
Expand Down Expand Up @@ -47,4 +47,5 @@ public String labelSelector() {
public boolean wereNamespacesConfigured() {
return namespacesWereConfigured;
}

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

public class Mappers {

public static final String DEFAULT_ANNOTATION_FOR_NAME = "io.javaoperatorsdk/primary-name";
public static final String DEFAULT_ANNOTATION_FOR_NAMESPACE =
"io.javaoperatorsdk/primary-namespace";

private Mappers() {}

public static <T extends HasMetadata> SecondaryToPrimaryMapper<T> fromAnnotation(
Expand All @@ -26,6 +30,10 @@ public static <T extends HasMetadata> SecondaryToPrimaryMapper<T> fromLabel(
return fromMetadata(nameKey, null, true);
}

public static <T extends HasMetadata> SecondaryToPrimaryMapper<T> fromDefaultAnnotations() {
return fromMetadata(DEFAULT_ANNOTATION_FOR_NAME, DEFAULT_ANNOTATION_FOR_NAMESPACE, false);
}

public static <T extends HasMetadata> SecondaryToPrimaryMapper<T> fromLabel(
String nameKey, String namespaceKey) {
return fromMetadata(nameKey, namespaceKey, true);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package io.javaoperatorsdk.operator;

import java.time.Duration;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

import io.fabric8.kubernetes.api.model.ConfigMap;
import io.fabric8.kubernetes.api.model.ObjectMetaBuilder;
import io.javaoperatorsdk.operator.junit.LocalOperatorExtension;
import io.javaoperatorsdk.operator.sample.dependentannotationsecondarymapper.DependentAnnotationSecondaryMapperReconciler;
import io.javaoperatorsdk.operator.sample.dependentannotationsecondarymapper.DependentAnnotationSecondaryMapperResource;

import static io.javaoperatorsdk.operator.processing.event.source.informer.Mappers.DEFAULT_ANNOTATION_FOR_NAME;
import static io.javaoperatorsdk.operator.processing.event.source.informer.Mappers.DEFAULT_ANNOTATION_FOR_NAMESPACE;
import static org.assertj.core.api.Assertions.assertThat;
import static org.awaitility.Awaitility.await;

class DependentAnnotationSecondaryMapperIT {

public static final String TEST_RESOURCE_NAME = "test1";

@RegisterExtension
LocalOperatorExtension operator =
LocalOperatorExtension.builder()
.withReconciler(DependentAnnotationSecondaryMapperReconciler.class)
.build();

@Test
void mapsSecondaryByAnnotation() {
operator.create(DependentAnnotationSecondaryMapperResource.class, testResource());

var reconciler =
operator.getReconcilerOfType(DependentAnnotationSecondaryMapperReconciler.class);

await().pollDelay(Duration.ofMillis(150)).untilAsserted(() -> {
assertThat(reconciler.getNumberOfExecutions()).isEqualTo(1);
});
var configMap = operator.get(ConfigMap.class, TEST_RESOURCE_NAME);

var annotations = configMap.getMetadata().getAnnotations();

assertThat(annotations)
.containsEntry(DEFAULT_ANNOTATION_FOR_NAME, TEST_RESOURCE_NAME)
.containsEntry(DEFAULT_ANNOTATION_FOR_NAMESPACE, operator.getNamespace());

assertThat(configMap.getMetadata().getOwnerReferences()).isEmpty();

configMap.getData().put("additional_data", "data");
operator.replace(ConfigMap.class, configMap);

await().pollDelay(Duration.ofMillis(150)).untilAsserted(() -> {
assertThat(reconciler.getNumberOfExecutions()).isEqualTo(2);
});
}


DependentAnnotationSecondaryMapperResource testResource() {
var res = new DependentAnnotationSecondaryMapperResource();
res.setMetadata(new ObjectMetaBuilder()
.withName(TEST_RESOURCE_NAME)
.build());
return res;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package io.javaoperatorsdk.operator.sample.dependentannotationsecondarymapper;

import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;

import io.fabric8.kubernetes.api.model.ConfigMap;
import io.fabric8.kubernetes.api.model.ObjectMetaBuilder;
import io.javaoperatorsdk.operator.api.reconciler.*;
import io.javaoperatorsdk.operator.api.reconciler.dependent.Deleter;
import io.javaoperatorsdk.operator.api.reconciler.dependent.Dependent;
import io.javaoperatorsdk.operator.processing.dependent.Creator;
import io.javaoperatorsdk.operator.processing.dependent.Updater;
import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependentResource;
import io.javaoperatorsdk.operator.support.TestExecutionInfoProvider;

@ControllerConfiguration(dependents = {@Dependent(
type = DependentAnnotationSecondaryMapperReconciler.ConfigMapDependentResource.class)})
public class DependentAnnotationSecondaryMapperReconciler
implements Reconciler<DependentAnnotationSecondaryMapperResource>, TestExecutionInfoProvider {

private final AtomicInteger numberOfExecutions = new AtomicInteger(0);

@Override
public UpdateControl<DependentAnnotationSecondaryMapperResource> reconcile(
DependentAnnotationSecondaryMapperResource resource,
Context<DependentAnnotationSecondaryMapperResource> context) {
numberOfExecutions.addAndGet(1);
return UpdateControl.noUpdate();
}

public int getNumberOfExecutions() {
return numberOfExecutions.get();
}

public static class ConfigMapDependentResource extends
KubernetesDependentResource<ConfigMap, DependentAnnotationSecondaryMapperResource>
implements Creator<ConfigMap, DependentAnnotationSecondaryMapperResource>,
Updater<ConfigMap, DependentAnnotationSecondaryMapperResource>,
Deleter<DependentAnnotationSecondaryMapperResource> {

public ConfigMapDependentResource() {
super(ConfigMap.class);
}

@Override
protected ConfigMap desired(DependentAnnotationSecondaryMapperResource primary,
Context<DependentAnnotationSecondaryMapperResource> context) {
ConfigMap configMap = new ConfigMap();
configMap.setMetadata(new ObjectMetaBuilder()
.withName(primary.getMetadata().getName())
.withNamespace(primary.getMetadata().getNamespace())
.build());
configMap.setData(Map.of("data", primary.getMetadata().getName()));
return configMap;
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package io.javaoperatorsdk.operator.sample.dependentannotationsecondarymapper;

import io.fabric8.kubernetes.api.model.Namespaced;
import io.fabric8.kubernetes.client.CustomResource;
import io.fabric8.kubernetes.model.annotation.Group;
import io.fabric8.kubernetes.model.annotation.Kind;
import io.fabric8.kubernetes.model.annotation.ShortNames;
import io.fabric8.kubernetes.model.annotation.Version;

@Group("sample.javaoperatorsdk")
@Version("v1")
@Kind("MaxIntervalTestCustomResource")
@ShortNames("mit")
public class DependentAnnotationSecondaryMapperResource
extends CustomResource<Void, DependentAnnotationSecondaryMapperResourceStatus>
implements Namespaced {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package io.javaoperatorsdk.operator.sample.dependentannotationsecondarymapper;

public class DependentAnnotationSecondaryMapperResourceStatus {

}