Skip to content

Commit e5df81f

Browse files
committed
trigger the RoleReconciler if the referenced secret changes
1 parent d5c888e commit e5df81f

14 files changed

Lines changed: 205 additions & 51 deletions

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ If you want to learn more about building native executables, please consult <htt
9494
## Related Guides
9595

9696
- Operator SDK ([guide](https://docs.quarkiverse.io/quarkus-operator-sdk/dev/index.html)): Quarkus extension for the Java Operator SDK (https://javaoperatorsdk.io)
97-
- Helm ([guide](https://docs.quarkiverse.io/quarkus-helm/dev/index.html)): Quarkus extension for Kubernetes Helm chars
97+
- Helm ([guide](https://docs.quarkiverse.io/quarkus-helm/dev/index.html)): Quarkus extension for Kubernetes Helm charts
9898
- SmallRye Health ([guide](https://quarkus.io/guides/smallrye-health)): Monitor service health
9999
- Micrometer metrics ([guide](https://quarkus.io/guides/micrometer)): Instrument the runtime and your application with dimensional metrics using Micrometer.
100100
- YAML Configuration ([guide](https://quarkus.io/guides/config-yaml)): Use YAML to configure your Quarkus application

build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ dependencies {
8787
* Testing
8888
*/
8989
testImplementation("io.quarkus:quarkus-junit5")
90+
testImplementation("org.awaitility:awaitility")
9091
testImplementation(libs.assertj)
9192
testImplementation(libs.datafaker)
9293
}

src/main/java/it/aboutbits/postgresql/core/BaseReconciler.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,21 +40,23 @@ public S initializeStatus(CR resource) {
4040
}
4141

4242
public String getResourceNamespaceOrOwn(
43+
CR resource,
4344
@Nullable String resourceNamespace
4445
) {
4546
if (resourceNamespace != null) {
4647
return resourceNamespace;
4748
}
4849

49-
return ((HasMetadata) this).getMetadata().getNamespace();
50+
return resource.getMetadata().getNamespace();
5051
}
5152

5253
public Optional<ClusterConnection> getReferencedClusterConnection(
5354
KubernetesClient kubernetesClient,
55+
CR resource,
5456
ClusterReference clusterRef
5557
) {
5658
var connectionName = clusterRef.getName();
57-
var connectionNamespace = getResourceNamespaceOrOwn(clusterRef.getNamespace());
59+
var connectionNamespace = getResourceNamespaceOrOwn(resource, clusterRef.getNamespace());
5860

5961
var clusterConnection = kubernetesClient.resources(ClusterConnection.class)
6062
.inNamespace(connectionNamespace)

src/main/java/it/aboutbits/postgresql/core/CRStatus.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,15 +13,15 @@
1313
/**
1414
* Status Object for the Custom Resources.
1515
* <p>
16-
* This object captures the current state of a bucket as observed by the reconciler.
16+
* This object captures the current state of a Custom Resource as observed by the reconciler.
1717
*/
1818
@NullMarked
1919
@Getter
2020
@Setter
2121
@Accessors(chain = true)
2222
public class CRStatus {
2323
/**
24-
* The actual bucket name in MinIO (may differ from metadata.name).
24+
* The Custom Resource name (may differ from metadata.name).
2525
*/
2626
@Nullable
2727
private String name = null;

src/main/java/it/aboutbits/postgresql/core/ClusterReference.java

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,7 @@
11
package it.aboutbits.postgresql.core;
22

33
import io.fabric8.generator.annotation.Required;
4-
import lombok.AllArgsConstructor;
54
import lombok.Getter;
6-
import lombok.NoArgsConstructor;
75
import lombok.Setter;
86
import org.jspecify.annotations.NullMarked;
97
import org.jspecify.annotations.Nullable;

src/main/java/it/aboutbits/postgresql/core/KubernetesUtil.java

Lines changed: 19 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -18,34 +18,38 @@ public static Credentials getSecretRefCredentials(
1818
KubernetesClient kubernetesClient,
1919
ClusterConnection clusterConnection
2020
) {
21-
var spec = clusterConnection.getSpec();
22-
var metadata = clusterConnection.getMetadata();
21+
return getSecretRefCredentials(
22+
kubernetesClient,
23+
clusterConnection.getSpec().getAdminSecretRef(),
24+
clusterConnection.getMetadata().getNamespace()
25+
);
26+
}
2327

24-
var passwordSecretRef = spec.getAdminSecretRef();
28+
public static Credentials getSecretRefCredentials(
29+
KubernetesClient kubernetesClient,
30+
SecretRef secretRef,
31+
String defaultNamespace
32+
) {
33+
var secretNamespace = secretRef.getNamespace() != null
34+
? secretRef.getNamespace()
35+
: defaultNamespace;
2536

26-
var secretNamespace = passwordSecretRef.getNamespace() != null
27-
? passwordSecretRef.getNamespace()
28-
: clusterConnection.getMetadata().getNamespace();
29-
var secretName = passwordSecretRef.getName();
37+
var secretName = secretRef.getName();
3038

3139
var secret = kubernetesClient.secrets()
3240
.inNamespace(secretNamespace)
3341
.withName(secretName)
3442
.get();
3543

3644
if (secret == null) {
37-
throw new IllegalStateException("ClusterConnection SecretRef not found [clusterConnection.namespace=%s, clusterConnection.name=%s, secret.namespace=%s, secret.name=%s]".formatted(
38-
metadata.getNamespace(),
39-
metadata.getName(),
45+
throw new IllegalStateException("SecretRef not found [secret.namespace=%s, secret.name=%s]".formatted(
4046
secretNamespace,
4147
secretName
4248
));
4349
}
4450

4551
if (!secret.getType().equals(SECRET_TYPE_BASIC_AUTH)) {
46-
throw new IllegalArgumentException("The ClusterConnection SecretRef is of the wrong type [clusterConnection.namespace=%s, clusterConnection.name=%s, secret.namespace=%s, secret.name=%s, expected.secret.type=%s, actual.secret.type=%s]".formatted(
47-
metadata.getNamespace(),
48-
metadata.getName(),
52+
throw new IllegalArgumentException("The SecretRef is of the wrong type [secret.namespace=%s, secret.name=%s, expected.secret.type=%s, actual.secret.type=%s]".formatted(
4953
secretNamespace,
5054
secretName,
5155
SECRET_TYPE_BASIC_AUTH,
@@ -55,9 +59,7 @@ public static Credentials getSecretRefCredentials(
5559

5660
var data = secret.getData();
5761
if (data == null || data.isEmpty()) {
58-
throw new IllegalStateException("The ClusterConnection SecretRef has no data set [clusterConnection.namespace=%s, clusterConnection.name=%s, secret.namespace=%s, secret.name=%s]".formatted(
59-
metadata.getNamespace(),
60-
metadata.getName(),
62+
throw new IllegalStateException("The SecretRef has no data set [secret.namespace=%s, secret.name=%s]".formatted(
6163
secretNamespace,
6264
secretName
6365
));
@@ -72,9 +74,7 @@ public static Credentials getSecretRefCredentials(
7274

7375
var passwordBase64 = data.get(SECRET_DATA_BASIC_AUTH_PASSWORD_KEY);
7476
if (passwordBase64 == null) {
75-
throw new IllegalStateException("The ClusterConnection SecretRef is missing required data password [clusterConnection.namespace=%s, clusterConnection.name=%s, secret.namespace=%s, secret.name=%s]".formatted(
76-
metadata.getNamespace(),
77-
metadata.getName(),
77+
throw new IllegalStateException("The SecretRef is missing required data password [secret.namespace=%s, secret.name=%s]".formatted(
7878
secretNamespace,
7979
secretName
8080
));

src/main/java/it/aboutbits/postgresql/core/SecretRef.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,7 @@
11
package it.aboutbits.postgresql.core;
22

33
import io.fabric8.generator.annotation.Required;
4-
import lombok.AllArgsConstructor;
54
import lombok.Getter;
6-
import lombok.NoArgsConstructor;
75
import lombok.Setter;
86
import org.jspecify.annotations.NullMarked;
97
import org.jspecify.annotations.Nullable;
@@ -15,6 +13,10 @@ public class SecretRef {
1513
@Required
1614
private String name;
1715

16+
/**
17+
* The namespace where the Secret is located.
18+
* If it is null, it means the Secret is in the same namespace as the resource referencing it.
19+
*/
1820
@Nullable
1921
@io.fabric8.generator.annotation.Nullable
2022
private String namespace;

src/main/java/it/aboutbits/postgresql/crd/role/RoleReconciler.java

Lines changed: 66 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,16 @@
11
package it.aboutbits.postgresql.crd.role;
22

3+
import io.fabric8.kubernetes.api.model.Secret;
34
import io.fabric8.kubernetes.client.KubernetesClient;
5+
import io.javaoperatorsdk.operator.api.config.informer.InformerEventSourceConfiguration;
46
import io.javaoperatorsdk.operator.api.reconciler.Context;
7+
import io.javaoperatorsdk.operator.api.reconciler.EventSourceContext;
58
import io.javaoperatorsdk.operator.api.reconciler.Reconciler;
69
import io.javaoperatorsdk.operator.api.reconciler.UpdateControl;
10+
import io.javaoperatorsdk.operator.processing.event.ResourceID;
11+
import io.javaoperatorsdk.operator.processing.event.source.EventSource;
12+
import io.javaoperatorsdk.operator.processing.event.source.SecondaryToPrimaryMapper;
13+
import io.javaoperatorsdk.operator.processing.event.source.informer.InformerEventSource;
714
import it.aboutbits.postgresql.core.BaseReconciler;
815
import it.aboutbits.postgresql.core.CRPhase;
916
import it.aboutbits.postgresql.core.CRStatus;
@@ -14,7 +21,9 @@
1421
import org.jspecify.annotations.NonNull;
1522

1623
import java.sql.SQLException;
24+
import java.util.List;
1725
import java.util.concurrent.TimeUnit;
26+
import java.util.stream.Collectors;
1827

1928
@RequiredArgsConstructor
2029
public class RoleReconciler
@@ -36,13 +45,14 @@ public UpdateControl<Role> reconcile(
3645

3746
var clusterConnectionOptional = getReferencedClusterConnection(
3847
kubernetesClient,
48+
resource,
3949
clusterRef
4050
);
4151

4252
if (clusterConnectionOptional.isEmpty()) {
4353
status.setPhase(CRPhase.PENDING)
4454
.setMessage("The specified ClusterConnection does not exist or is not ready yet [clusterRef=%s/%s]".formatted(
45-
getResourceNamespaceOrOwn(clusterRef.getNamespace()),
55+
getResourceNamespaceOrOwn(resource, clusterRef.getNamespace()),
4656
clusterRef.getName()
4757
));
4858

@@ -56,13 +66,15 @@ public UpdateControl<Role> reconcile(
5666
expectedFlags.getRole().sort(String.CASE_INSENSITIVE_ORDER);
5767
expectedFlags.getInRole().sort(String.CASE_INSENSITIVE_ORDER);
5868

59-
var loginExpected = spec.getPasswordSecretRef() != null;
69+
var passwordSecretRef = spec.getPasswordSecretRef();
70+
var loginExpected = passwordSecretRef != null;
6071

6172
String password;
6273
if (loginExpected) {
6374
password = KubernetesUtil.getSecretRefCredentials(
6475
kubernetesClient,
65-
clusterConnection
76+
passwordSecretRef,
77+
resource.getMetadata().getNamespace()
6678
).password();
6779
} else {
6880
password = null;
@@ -139,8 +151,59 @@ public UpdateControl<Role> reconcile(
139151
return updateControl;
140152
}
141153

154+
@Override
155+
public List<EventSource<?, Role>> prepareEventSources(EventSourceContext<Role> context) {
156+
// 1. Define the Mapper
157+
// We define how to find the Primary Resource (Role) when a Secret changes
158+
SecondaryToPrimaryMapper<Secret> secretToRoleMapper = (Secret secret) -> {
159+
// 2. Filter Roles that reference this specific Secret
160+
return context.getPrimaryCache()
161+
.list()
162+
.filter(role -> isReferencedBy(role, secret))
163+
.map(ResourceID::fromResource)
164+
.collect(Collectors.toSet());
165+
};
166+
167+
// 2. Build the Event Source Configuration which binds the InformerConfig + Mapper
168+
var eventSourceConfig = InformerEventSourceConfiguration.from(Secret.class, Role.class)
169+
.withSecondaryToPrimaryMapper(secretToRoleMapper)
170+
// or .withWatchAllNamespaces() if we want to have the secret in another namespace than the Role CR instance
171+
.withNamespacesInheritedFromController()
172+
.build();
173+
174+
// 3. Create the Event Source
175+
// This will watch for Secret changes and run the mapper
176+
var secretEventSource = new InformerEventSource<>(
177+
eventSourceConfig,
178+
context
179+
);
180+
181+
return List.of(secretEventSource);
182+
}
183+
142184
@Override
143185
protected @NonNull CRStatus newStatus() {
144186
return new CRStatus();
145187
}
188+
189+
/**
190+
* Checks if the given Role's spec.passwordSecretRef points to the changed Secret.
191+
*/
192+
private boolean isReferencedBy(
193+
Role role,
194+
Secret secret
195+
) {
196+
var spec = role.getSpec();
197+
198+
if (spec.getPasswordSecretRef() == null) {
199+
return false;
200+
}
201+
202+
var ref = spec.getPasswordSecretRef();
203+
var refName = ref.getName();
204+
var refNamespace = getResourceNamespaceOrOwn(role, ref.getNamespace());
205+
206+
return refName.equals(secret.getMetadata().getName()) &&
207+
refNamespace.equals(secret.getMetadata().getNamespace());
208+
}
146209
}

src/test/java/it/aboutbits/postgresql/_support/testdata/base/TestDataCreator.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ public static String randomKubernetesNameSuffix(String name) {
5151

5252
if (name.length() > 60) {
5353
throw new IllegalArgumentException(
54-
"The name is too long (must be <= 60 to allow '-' + at least 2 random chars, max %d total) [name=%s, lenght=%d]".formatted(
54+
"The name is too long (must be <= 60 to allow '-' + at least 2 random chars, max %d total) [name=%s, length=%d]".formatted(
5555
maxLength,
5656
name,
5757
name.length()

src/test/java/it/aboutbits/postgresql/_support/testdata/persisted/Given.java

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,8 +65,7 @@ public Item describedAs(String description) {
6565
public SecretRefCreate secretRef() {
6666
return new SecretRefCreate(
6767
numberOfItems,
68-
kubernetesClient,
69-
dbConnectionDetails()
68+
kubernetesClient
7069
);
7170
}
7271

0 commit comments

Comments
 (0)