Skip to content

Commit c0724cf

Browse files
committed
feat: sanitization of resources for matching
Signed-off-by: Attila Mészáros <[email protected]>
1 parent 7afb541 commit c0724cf

File tree

16 files changed

+215
-155
lines changed

16 files changed

+215
-155
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
package io.javaoperatorsdk.operator.processing.dependent.kubernetes;
2+
3+
import io.fabric8.kubernetes.api.model.HasMetadata;
4+
import io.fabric8.kubernetes.api.model.PersistentVolumeClaimStatus;
5+
import io.fabric8.kubernetes.api.model.Secret;
6+
import io.fabric8.kubernetes.api.model.apps.StatefulSet;
7+
import io.javaoperatorsdk.operator.api.reconciler.Context;
8+
9+
import static io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependentResource.useSSA;
10+
11+
public class DesiredResourceSanitizer {
12+
13+
private DesiredResourceSanitizer() {}
14+
15+
public static <R, P extends HasMetadata> void sanitizeDesired(R desired, R actual, P primary,
16+
Context<P> context) {
17+
if (useSSA(context)) {
18+
if (desired instanceof StatefulSet) {
19+
fillDefaultsOnVolumeClaimTemplate((StatefulSet) desired);
20+
}
21+
if (desired instanceof Secret) {
22+
checkIfStringDataUsed((Secret) desired);
23+
}
24+
}
25+
}
26+
27+
private static void checkIfStringDataUsed(Secret secret) {
28+
if (secret.getStringData() != null && !secret.getStringData().isEmpty()) {
29+
throw new IllegalStateException(
30+
"There is a known issue using StringData with SSA. Use data instead.");
31+
}
32+
}
33+
34+
private static void fillDefaultsOnVolumeClaimTemplate(StatefulSet statefulSet) {
35+
if (!statefulSet.getSpec().getVolumeClaimTemplates().isEmpty()) {
36+
statefulSet.getSpec().getVolumeClaimTemplates().forEach(t -> {
37+
if (t.getSpec().getVolumeMode() == null) {
38+
t.getSpec().setVolumeMode("Filesystem");
39+
}
40+
if (t.getStatus() == null) {
41+
t.setStatus(new PersistentVolumeClaimStatus());
42+
}
43+
if (t.getStatus().getPhase() == null) {
44+
t.setStatus(new PersistentVolumeClaimStatus());
45+
t.getStatus().setPhase("pending");
46+
}
47+
});
48+
}
49+
}
50+
}

operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/KubernetesDependentResource.java

+16-9
Original file line numberDiff line numberDiff line change
@@ -103,18 +103,19 @@ public void configureWith(InformerEventSource<R, P> informerEventSource) {
103103
}
104104

105105
@SuppressWarnings("unused")
106-
public R create(R target, P primary, Context<P> context) {
106+
public R create(R desired, P primary, Context<P> context) {
107107
if (useSSA(context)) {
108108
// setting resource version for SSA so only created if it doesn't exist already
109109
var createIfNotExisting = kubernetesDependentResourceConfig == null
110110
? KubernetesDependentResourceConfig.DEFAULT_CREATE_RESOURCE_ONLY_IF_NOT_EXISTING_WITH_SSA
111111
: kubernetesDependentResourceConfig.createResourceOnlyIfNotExistingWithSSA();
112112
if (createIfNotExisting) {
113-
target.getMetadata().setResourceVersion("1");
113+
desired.getMetadata().setResourceVersion("1");
114114
}
115115
}
116-
addMetadata(false, null, target, primary);
117-
final var resource = prepare(target, primary, "Creating");
116+
addMetadata(false, null, desired, primary);
117+
sanitizeDesired(desired, null, primary, context);
118+
final var resource = prepare(desired, primary, "Creating");
118119
return useSSA(context)
119120
? resource
120121
.fieldManager(context.getControllerConfiguration().fieldManager())
@@ -123,19 +124,20 @@ public R create(R target, P primary, Context<P> context) {
123124
: resource.create();
124125
}
125126

126-
public R update(R actual, R target, P primary, Context<P> context) {
127+
public R update(R actual, R desired, P primary, Context<P> context) {
127128
if (log.isDebugEnabled()) {
128129
log.debug("Updating actual resource: {} version: {}", ResourceID.fromResource(actual),
129130
actual.getMetadata().getResourceVersion());
130131
}
131132
R updatedResource;
132-
addMetadata(false, actual, target, primary);
133+
addMetadata(false, actual, desired, primary);
134+
sanitizeDesired(desired, actual, primary, context);
133135
if (useSSA(context)) {
134-
updatedResource = prepare(target, primary, "Updating")
136+
updatedResource = prepare(desired, primary, "Updating")
135137
.fieldManager(context.getControllerConfiguration().fieldManager())
136138
.forceConflicts().serverSideApply();
137139
} else {
138-
var updatedActual = updaterMatcher.updateResource(actual, target, context);
140+
var updatedActual = updaterMatcher.updateResource(actual, desired, context);
139141
updatedResource = prepare(updatedActual, primary, "Updating").update();
140142
}
141143
log.debug("Resource version after update: {}",
@@ -146,6 +148,7 @@ public R update(R actual, R target, P primary, Context<P> context) {
146148
@Override
147149
public Result<R> match(R actualResource, P primary, Context<P> context) {
148150
final var desired = desired(primary, context);
151+
sanitizeDesired(desired, actualResource, primary, context);
149152
return match(actualResource, desired, primary, updaterMatcher, context);
150153
}
151154

@@ -189,7 +192,11 @@ protected void addMetadata(boolean forMatch, R actualResource, final R target, P
189192
addReferenceHandlingMetadata(target, primary);
190193
}
191194

192-
private boolean useSSA(Context<P> context) {
195+
protected void sanitizeDesired(R desired, R actual, P primary, Context<P> context) {
196+
DesiredResourceSanitizer.sanitizeDesired(desired, actual, primary, context);
197+
}
198+
199+
public static <P extends HasMetadata> boolean useSSA(Context<P> context) {
193200
return context.getControllerConfiguration().getConfigurationService()
194201
.ssaBasedCreateUpdateMatchForDependentResources();
195202
}

operator-framework/src/test/java/io/javaoperatorsdk/operator/SSAStatefulSetMatcherIssueIT.java

-43
This file was deleted.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
package io.javaoperatorsdk.operator;
2+
3+
import java.time.Duration;
4+
5+
import org.junit.jupiter.api.Test;
6+
import org.junit.jupiter.api.extension.RegisterExtension;
7+
8+
import io.fabric8.kubernetes.api.model.ObjectMetaBuilder;
9+
import io.fabric8.kubernetes.api.model.apps.StatefulSet;
10+
import io.javaoperatorsdk.operator.junit.LocallyRunOperatorExtension;
11+
import io.javaoperatorsdk.operator.sample.statefulsetdesiredsanitizer.StatefulSetDesiredSanitizerCustomResource;
12+
import io.javaoperatorsdk.operator.sample.statefulsetdesiredsanitizer.StatefulSetDesiredSanitizerDependentResource;
13+
import io.javaoperatorsdk.operator.sample.statefulsetdesiredsanitizer.StatefulSetDesiredSanitizerReconciler;
14+
import io.javaoperatorsdk.operator.sample.statefulsetdesiredsanitizer.StatefulSetDesiredSanitizerSpec;
15+
16+
import static org.assertj.core.api.Assertions.assertThat;
17+
import static org.awaitility.Awaitility.await;
18+
19+
public class StatefulSetDesiredSanitizerIT {
20+
21+
public static final String TEST_1 = "test1";
22+
23+
@RegisterExtension
24+
LocallyRunOperatorExtension extension =
25+
LocallyRunOperatorExtension.builder()
26+
.withReconciler(new StatefulSetDesiredSanitizerReconciler())
27+
.build();
28+
29+
@Test
30+
void testSSAMatcher() {
31+
var resource = extension.create(testResource());
32+
33+
await().pollDelay(Duration.ofMillis(200)).untilAsserted(() -> {
34+
var statefulSet = extension.get(StatefulSet.class, TEST_1);
35+
assertThat(statefulSet).isNotNull();
36+
});
37+
// make sure reconciliation happens at least once more
38+
resource.getSpec().setValue("changed value");
39+
extension.replace(resource);
40+
41+
await().untilAsserted(
42+
() -> assertThat(StatefulSetDesiredSanitizerDependentResource.nonMatchedAtLeastOnce)
43+
.isFalse());
44+
}
45+
46+
StatefulSetDesiredSanitizerCustomResource testResource() {
47+
var res = new StatefulSetDesiredSanitizerCustomResource();
48+
res.setMetadata(new ObjectMetaBuilder()
49+
.withName(TEST_1)
50+
.build());
51+
res.setSpec(new StatefulSetDesiredSanitizerSpec());
52+
res.getSpec().setValue("initial value");
53+
54+
return res;
55+
}
56+
57+
}

operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/dependentssa/SSAConfigMapDependent.java

+2-2
Original file line numberDiff line numberDiff line change
@@ -33,10 +33,10 @@ protected ConfigMap desired(DependnetSSACustomResource primary,
3333
}
3434

3535
@Override
36-
public ConfigMap update(ConfigMap actual, ConfigMap target,
36+
public ConfigMap update(ConfigMap actual, ConfigMap desired,
3737
DependnetSSACustomResource primary,
3838
Context<DependnetSSACustomResource> context) {
3939
NUMBER_OF_UPDATES.incrementAndGet();
40-
return super.update(actual, target, primary, context);
40+
return super.update(actual, desired, primary, context);
4141
}
4242
}

operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/servicestrictmatcher/ServiceDependentResource.java

+2-2
Original file line numberDiff line numberDiff line change
@@ -53,10 +53,10 @@ public Matcher.Result<Service> match(Service actualResource,
5353
}
5454

5555
@Override
56-
public Service update(Service actual, Service target,
56+
public Service update(Service actual, Service desired,
5757
ServiceStrictMatcherTestCustomResource primary,
5858
Context<ServiceStrictMatcherTestCustomResource> context) {
5959
updated.addAndGet(1);
60-
return super.update(actual, target, primary, context);
60+
return super.update(actual, desired, primary, context);
6161
}
6262
}

operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/ssalegacymatcher/ServiceDependentResource.java

+4-4
Original file line numberDiff line numberDiff line change
@@ -45,17 +45,17 @@ public Result<Service> match(Service actualResource, SSALegacyMatcherCustomResou
4545

4646
// override just to check the exec count
4747
@Override
48-
public Service update(Service actual, Service target, SSALegacyMatcherCustomResource primary,
48+
public Service update(Service actual, Service desired, SSALegacyMatcherCustomResource primary,
4949
Context<SSALegacyMatcherCustomResource> context) {
5050
createUpdateCount.addAndGet(1);
51-
return super.update(actual, target, primary, context);
51+
return super.update(actual, desired, primary, context);
5252
}
5353

5454
// override just to check the exec count
5555
@Override
56-
public Service create(Service target, SSALegacyMatcherCustomResource primary,
56+
public Service create(Service desired, SSALegacyMatcherCustomResource primary,
5757
Context<SSALegacyMatcherCustomResource> context) {
5858
createUpdateCount.addAndGet(1);
59-
return super.create(target, primary, context);
59+
return super.create(desired, primary, context);
6060
}
6161
}

operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/ssastatefulsetmatcherissue/SSAStatefulSetMatcherIssueReconciler.java

-20
This file was deleted.

operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/ssastatefulsetmatcherissue/StatefulSetDependentResource.java

-28
This file was deleted.
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package io.javaoperatorsdk.operator.sample.ssastatefulsetmatcherissue;
1+
package io.javaoperatorsdk.operator.sample.statefulsetdesiredsanitizer;
22

33
import io.fabric8.kubernetes.api.model.Namespaced;
44
import io.fabric8.kubernetes.client.CustomResource;
@@ -7,8 +7,8 @@
77

88
@Group("sample.javaoperatorsdk")
99
@Version("v1")
10-
public class SSAStatefulSetMatcherIssueCustomResource
11-
extends CustomResource<Void, Void>
10+
public class StatefulSetDesiredSanitizerCustomResource
11+
extends CustomResource<StatefulSetDesiredSanitizerSpec, Void>
1212
implements Namespaced {
1313

1414
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
package io.javaoperatorsdk.operator.sample.statefulsetdesiredsanitizer;
2+
3+
import io.fabric8.kubernetes.api.model.ObjectMetaBuilder;
4+
import io.fabric8.kubernetes.api.model.apps.StatefulSet;
5+
import io.javaoperatorsdk.operator.ReconcilerUtils;
6+
import io.javaoperatorsdk.operator.api.reconciler.Context;
7+
import io.javaoperatorsdk.operator.processing.dependent.kubernetes.CRUDKubernetesDependentResource;
8+
9+
public class StatefulSetDesiredSanitizerDependentResource
10+
extends
11+
CRUDKubernetesDependentResource<StatefulSet, StatefulSetDesiredSanitizerCustomResource> {
12+
13+
public static volatile Boolean nonMatchedAtLeastOnce;
14+
15+
public StatefulSetDesiredSanitizerDependentResource() {
16+
super(StatefulSet.class);
17+
}
18+
19+
@Override
20+
protected StatefulSet desired(StatefulSetDesiredSanitizerCustomResource primary,
21+
Context<StatefulSetDesiredSanitizerCustomResource> context) {
22+
var template =
23+
ReconcilerUtils.loadYaml(StatefulSet.class, getClass(), "statefulset.yaml");
24+
template.setMetadata(new ObjectMetaBuilder()
25+
.withName(primary.getMetadata().getName())
26+
.withNamespace(primary.getMetadata().getNamespace())
27+
.build());
28+
return template;
29+
}
30+
31+
@Override
32+
public Result<StatefulSet> match(StatefulSet actualResource,
33+
StatefulSetDesiredSanitizerCustomResource primary,
34+
Context<StatefulSetDesiredSanitizerCustomResource> context) {
35+
var res = super.match(actualResource, primary, context);
36+
if (!res.matched()) {
37+
nonMatchedAtLeastOnce = true;
38+
} else if (nonMatchedAtLeastOnce == null) {
39+
nonMatchedAtLeastOnce = false;
40+
}
41+
return res;
42+
}
43+
}

0 commit comments

Comments
 (0)