Skip to content

Commit 97bc450

Browse files
authored
feat: name uniqueness validation (#79)
Signed-off-by: Attila Mészáros <[email protected]>
1 parent ff1e2af commit 97bc450

File tree

10 files changed

+217
-7
lines changed

10 files changed

+217
-7
lines changed
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package io.csviri.operator.glue.customresource;
2+
3+
import io.javaoperatorsdk.operator.api.ObservedGenerationAwareStatus;
4+
5+
public class AbstractStatus extends ObservedGenerationAwareStatus {
6+
7+
private String errorMessage;
8+
9+
public String getErrorMessage() {
10+
return errorMessage;
11+
}
12+
13+
public void setErrorMessage(String errorMessage) {
14+
this.errorMessage = errorMessage;
15+
}
16+
17+
}
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
package io.csviri.operator.glue.customresource.glue;
22

3-
import io.javaoperatorsdk.operator.api.ObservedGenerationAwareStatus;
3+
import io.csviri.operator.glue.customresource.AbstractStatus;
44

5-
public class GlueStatus extends ObservedGenerationAwareStatus {
5+
public class GlueStatus extends AbstractStatus {
66

77
}
Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
package io.csviri.operator.glue.customresource.operator;
22

3-
import io.javaoperatorsdk.operator.api.ObservedGenerationAwareStatus;
3+
import io.csviri.operator.glue.customresource.AbstractStatus;
4+
5+
public class ResourceFlowOperatorStatus extends AbstractStatus {
46

5-
public class ResourceFlowOperatorStatus extends ObservedGenerationAwareStatus {
67

78
}
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
package io.csviri.operator.glue.reconciler;
2+
3+
import java.util.ArrayList;
4+
import java.util.HashSet;
5+
import java.util.List;
6+
import java.util.Set;
7+
import java.util.function.Consumer;
8+
9+
import org.slf4j.Logger;
10+
import org.slf4j.LoggerFactory;
11+
12+
import io.csviri.operator.glue.GlueException;
13+
import io.csviri.operator.glue.customresource.AbstractStatus;
14+
import io.csviri.operator.glue.customresource.glue.DependentResourceSpec;
15+
import io.csviri.operator.glue.customresource.glue.GlueSpec;
16+
import io.csviri.operator.glue.customresource.glue.RelatedResourceSpec;
17+
import io.fabric8.kubernetes.client.CustomResource;
18+
import io.javaoperatorsdk.operator.api.reconciler.ErrorStatusUpdateControl;
19+
20+
import jakarta.inject.Singleton;
21+
22+
@Singleton
23+
public class ValidationAndErrorHandler {
24+
25+
private static final Logger log = LoggerFactory.getLogger(ValidationAndErrorHandler.class);
26+
27+
public static final String NON_UNIQUE_NAMES_FOUND_PREFIX = "Non unique names found: ";
28+
29+
public <T extends CustomResource<?, ? extends AbstractStatus>> ErrorStatusUpdateControl<T> updateStatusErrorMessage(
30+
Exception e,
31+
T resource) {
32+
log.error("Error during reconciliation of resource. Name: {} namespace: {}, Kind: {}",
33+
resource.getMetadata().getName(), resource.getMetadata().getNamespace(), resource.getKind(),
34+
e);
35+
if (e instanceof ValidationAndErrorHandler.NonUniqueNameException ex) {
36+
resource.getStatus()
37+
.setErrorMessage(NON_UNIQUE_NAMES_FOUND_PREFIX + String.join(",", ex.getDuplicates()));
38+
return ErrorStatusUpdateControl.updateStatus(resource).withNoRetry();
39+
} else {
40+
resource.getStatus().setErrorMessage("Error during reconciliation");
41+
return ErrorStatusUpdateControl.updateStatus(resource);
42+
}
43+
}
44+
45+
public void checkIfNamesAreUnique(GlueSpec glueSpec) {
46+
Set<String> seen = new HashSet<>();
47+
List<String> duplicates = new ArrayList<>();
48+
49+
Consumer<String> deduplicate = n -> {
50+
if (seen.contains(n)) {
51+
duplicates.add(n);
52+
} else {
53+
seen.add(n);
54+
}
55+
};
56+
glueSpec.getResources().stream().map(DependentResourceSpec::getName).forEach(deduplicate);
57+
glueSpec.getRelatedResources().stream().map(RelatedResourceSpec::getName).forEach(deduplicate);
58+
59+
if (!duplicates.isEmpty()) {
60+
throw new NonUniqueNameException(duplicates);
61+
}
62+
}
63+
64+
public static class NonUniqueNameException extends GlueException {
65+
66+
private final List<String> duplicates;
67+
68+
public NonUniqueNameException(List<String> duplicates) {
69+
this.duplicates = duplicates;
70+
}
71+
72+
public List<String> getDuplicates() {
73+
return duplicates;
74+
}
75+
}
76+
77+
}

src/main/java/io/csviri/operator/glue/reconciler/glue/GlueReconciler.java

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,14 @@
1010
import io.csviri.operator.glue.conditions.ReadyCondition;
1111
import io.csviri.operator.glue.customresource.glue.DependentResourceSpec;
1212
import io.csviri.operator.glue.customresource.glue.Glue;
13+
import io.csviri.operator.glue.customresource.glue.GlueStatus;
1314
import io.csviri.operator.glue.customresource.glue.condition.ConditionSpec;
1415
import io.csviri.operator.glue.customresource.glue.condition.JavaScriptConditionSpec;
1516
import io.csviri.operator.glue.customresource.glue.condition.ReadyConditionSpec;
1617
import io.csviri.operator.glue.dependent.GCGenericDependentResource;
1718
import io.csviri.operator.glue.dependent.GenericDependentResource;
1819
import io.csviri.operator.glue.dependent.GenericResourceDiscriminator;
20+
import io.csviri.operator.glue.reconciler.ValidationAndErrorHandler;
1921
import io.csviri.operator.glue.templating.GenericTemplateHandler;
2022
import io.fabric8.kubernetes.api.model.GenericKubernetesResource;
2123
import io.fabric8.kubernetes.api.model.HasMetadata;
@@ -27,16 +29,21 @@
2729
import io.javaoperatorsdk.operator.processing.dependent.workflow.KubernetesResourceDeletedCondition;
2830
import io.javaoperatorsdk.operator.processing.dependent.workflow.WorkflowBuilder;
2931

32+
import jakarta.inject.Inject;
33+
3034
import static io.csviri.operator.glue.Utils.getResourceForSSAFrom;
3135
import static io.csviri.operator.glue.reconciler.operator.GlueOperatorReconciler.PARENT_RELATED_RESOURCE_NAME;
3236

3337
@ControllerConfiguration
34-
public class GlueReconciler implements Reconciler<Glue>, Cleaner<Glue> {
38+
public class GlueReconciler implements Reconciler<Glue>, Cleaner<Glue>, ErrorStatusHandler<Glue> {
3539

3640
private static final Logger log = LoggerFactory.getLogger(GlueReconciler.class);
3741
public static final String DEPENDENT_NAME_ANNOTATION_KEY = "io.csviri.operator.resourceflow/name";
3842
public static final String PARENT_GLUE_FINALIZER_PREFIX = "io.csviri.operator.resourceflow.glue/";
3943

44+
@Inject
45+
ValidationAndErrorHandler validationAndErrorHandler;
46+
4047
private final KubernetesResourceDeletedCondition deletePostCondition =
4148
new KubernetesResourceDeletedCondition();
4249
private final InformerRegister informerRegister = new InformerRegister();
@@ -57,6 +64,9 @@ public UpdateControl<Glue> reconcile(Glue primary,
5764

5865
log.debug("Reconciling glue. name: {} namespace: {}",
5966
primary.getMetadata().getName(), primary.getMetadata().getNamespace());
67+
68+
validationAndErrorHandler.checkIfNamesAreUnique(primary.getSpec());
69+
6070
registerRelatedResourceInformers(context, primary);
6171
if (deletedGlueIfParentMarkedForDeletion(context, primary)) {
6272
return UpdateControl.noUpdate();
@@ -270,5 +280,12 @@ private String parentFinalizer(String glueName) {
270280
return PARENT_GLUE_FINALIZER_PREFIX + glueName;
271281
}
272282

273-
283+
@Override
284+
public ErrorStatusUpdateControl<Glue> updateErrorStatus(Glue resource, Context<Glue> context,
285+
Exception e) {
286+
if (resource.getStatus() == null) {
287+
resource.setStatus(new GlueStatus());
288+
}
289+
return validationAndErrorHandler.updateStatusErrorMessage(e, resource);
290+
}
274291
}

src/main/java/io/csviri/operator/glue/reconciler/operator/GlueOperatorReconciler.java

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313
import io.csviri.operator.glue.customresource.glue.RelatedResourceSpec;
1414
import io.csviri.operator.glue.customresource.operator.GlueOperator;
1515
import io.csviri.operator.glue.customresource.operator.GlueOperatorSpec;
16+
import io.csviri.operator.glue.customresource.operator.ResourceFlowOperatorStatus;
17+
import io.csviri.operator.glue.reconciler.ValidationAndErrorHandler;
1618
import io.fabric8.kubernetes.api.model.GenericKubernetesResource;
1719
import io.fabric8.kubernetes.api.model.ObjectMetaBuilder;
1820
import io.fabric8.kubernetes.client.utils.KubernetesResourceUtil;
@@ -23,17 +25,22 @@
2325
import io.javaoperatorsdk.operator.processing.event.source.EventSource;
2426
import io.javaoperatorsdk.operator.processing.event.source.informer.InformerEventSource;
2527

28+
import jakarta.inject.Inject;
29+
2630
@ControllerConfiguration
2731
public class GlueOperatorReconciler
2832
implements Reconciler<GlueOperator>, EventSourceInitializer<GlueOperator>,
29-
Cleaner<GlueOperator> {
33+
Cleaner<GlueOperator>, ErrorStatusHandler<GlueOperator> {
3034

3135
private static final Logger log = LoggerFactory.getLogger(GlueOperatorReconciler.class);
3236

3337
public static final String GLUE_LABEL_KEY = "foroperator";
3438
public static final String GLUE_LABEL_VALUE = "true";
3539
public static final String PARENT_RELATED_RESOURCE_NAME = "parent";
3640

41+
@Inject
42+
ValidationAndErrorHandler validationAndErrorHandler;
43+
3744
private InformerEventSource<Glue, GlueOperator> resourceFlowEventSource;
3845

3946
@Override
@@ -43,6 +50,8 @@ public UpdateControl<GlueOperator> reconcile(GlueOperator glueOperator,
4350
log.info("Reconciling GlueOperator {} in namespace: {}", glueOperator.getMetadata().getName(),
4451
glueOperator.getMetadata().getNamespace());
4552

53+
validationAndErrorHandler.checkIfNamesAreUnique(glueOperator.getSpec());
54+
4655
var targetCREventSource = getOrRegisterCustomResourceEventSource(glueOperator, context);
4756
targetCREventSource.list().forEach(cr -> {
4857
var actualResourceFlow = resourceFlowEventSource
@@ -128,6 +137,15 @@ public Map<String, EventSource> prepareEventSources(
128137
return EventSourceInitializer.nameEventSources(resourceFlowEventSource);
129138
}
130139

140+
@Override
141+
public ErrorStatusUpdateControl<GlueOperator> updateErrorStatus(GlueOperator resource,
142+
Context<GlueOperator> context, Exception e) {
143+
if (resource.getStatus() == null) {
144+
resource.setStatus(new ResourceFlowOperatorStatus());
145+
}
146+
return validationAndErrorHandler.updateStatusErrorMessage(e, resource);
147+
}
148+
131149
@Override
132150
public DeleteControl cleanup(GlueOperator glueOperator,
133151
Context<GlueOperator> context) {
@@ -141,4 +159,5 @@ private static String glueName(GenericKubernetesResource cr) {
141159
return KubernetesResourceUtil.sanitizeName(cr.getMetadata().getName() + "-" + cr.getKind());
142160
}
143161

162+
144163
}

src/test/java/io/csviri/operator/glue/GlueOperatorTest.java

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
import io.csviri.operator.glue.customresource.operator.GlueOperator;
1515
import io.csviri.operator.glue.customresource.operator.GlueOperatorSpec;
1616
import io.csviri.operator.glue.customresource.operator.Parent;
17+
import io.csviri.operator.glue.reconciler.ValidationAndErrorHandler;
1718
import io.fabric8.kubernetes.api.model.ConfigMap;
1819
import io.fabric8.kubernetes.api.model.ObjectMetaBuilder;
1920
import io.quarkus.test.junit.QuarkusTest;
@@ -149,6 +150,20 @@ void simpleConcurrencyForMultipleOperatorTest() {
149150
}));
150151
}
151152

153+
@Test
154+
void nonUniqueNameTest() {
155+
var go = create(TestUtils
156+
.loadResourceFlowOperator("/glueoperator/NonUniqueName.yaml"));
157+
158+
await().untilAsserted(() -> {
159+
var actual = get(GlueOperator.class, go.getMetadata().getName());
160+
161+
assertThat(actual.getStatus()).isNotNull();
162+
assertThat(actual.getStatus().getErrorMessage())
163+
.startsWith(ValidationAndErrorHandler.NON_UNIQUE_NAMES_FOUND_PREFIX);
164+
});
165+
}
166+
152167
TestCustomResource testCustomResource() {
153168
return testCustomResource(1);
154169
}

src/test/java/io/csviri/operator/glue/GlueTest.java

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
import io.csviri.operator.glue.customresource.glue.DependentResourceSpec;
1212
import io.csviri.operator.glue.customresource.glue.Glue;
13+
import io.csviri.operator.glue.reconciler.ValidationAndErrorHandler;
1314
import io.fabric8.kubernetes.api.model.ConfigMap;
1415
import io.fabric8.kubernetes.api.model.Secret;
1516
import io.quarkus.test.junit.QuarkusTest;
@@ -222,6 +223,21 @@ void changingWorkflow() {
222223
assertThat(s).isNull();
223224
});
224225
}
226+
227+
@Test
228+
void nonUniqueNameResultsInErrorMessageOnStatus() {
229+
Glue glue = create(TestUtils.loadResoureFlow("/glue/NonUniqueName.yaml"));
230+
231+
await().untilAsserted(() -> {
232+
var actualGlue = get(Glue.class, glue.getMetadata().getName());
233+
234+
assertThat(actualGlue.getStatus()).isNotNull();
235+
assertThat(actualGlue.getStatus().getErrorMessage())
236+
.startsWith(ValidationAndErrorHandler.NON_UNIQUE_NAMES_FOUND_PREFIX);
237+
});
238+
}
239+
240+
225241
//
226242
// @Disabled("Not supported in current version")
227243
// @Test
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# Invalid GLUE, presents resources with non-unique name
2+
apiVersion: io.csviri.operator.glue/v1beta1
3+
kind: Glue
4+
metadata:
5+
name: templating-sample
6+
spec:
7+
resources:
8+
- name: configMap1
9+
resource:
10+
apiVersion: v1
11+
kind: ConfigMap
12+
metadata:
13+
name: cm1
14+
data:
15+
key: "value1"
16+
- name: configMap1
17+
resource:
18+
apiVersion: v1
19+
kind: ConfigMap
20+
metadata:
21+
name: cm2
22+
data:
23+
key: "value1"
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
apiVersion: io.csviri.operator.glue/v1beta1
2+
kind: GlueOperator
3+
metadata:
4+
name: non-unique-name
5+
spec:
6+
parent:
7+
apiVersion: io.csviri.operator.glue/v1
8+
kind: TestCustomResource
9+
resources:
10+
- name: configMap1
11+
resource:
12+
apiVersion: v1
13+
kind: ConfigMap
14+
metadata:
15+
name: "{parent.metadata.name}"
16+
data:
17+
key: "{parent.spec.value}"
18+
- name: configMap1
19+
resource:
20+
apiVersion: v1
21+
kind: ConfigMap
22+
metadata:
23+
name: "{parent.metadata.name}"
24+
data:
25+
key: "{parent.spec.value}"

0 commit comments

Comments
 (0)