Skip to content

Commit b4444ea

Browse files
committed
Merge branch 'validate-k8s-resource-references-against-rfc1123' into refactor-secret-ref-and-cluster-reference-to-resource-ref
# Conflicts: # operator/src/main/java/it/aboutbits/postgresql/core/ClusterReference.java # operator/src/main/java/it/aboutbits/postgresql/core/SecretRef.java # operator/src/main/java/it/aboutbits/postgresql/core/schema_customizer/HostCustomizer.java # operator/src/main/java/it/aboutbits/postgresql/crd/clusterconnection/ClusterConnectionSpec.java
2 parents ba04f01 + a0f9e2b commit b4444ea

6 files changed

Lines changed: 163 additions & 26 deletions

File tree

operator/src/main/java/it/aboutbits/postgresql/PostgreSQLInstanceReadinessCheck.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,11 @@ public HealthCheckResponse call() {
3030
var connections = kubernetesClient.resources(ClusterConnection.class).list().getItems();
3131

3232
boolean allUp = connections.stream()
33-
.allMatch(connection -> checkInstance(
33+
.map(connection -> checkInstance(
3434
connection,
3535
builder
36-
));
36+
))
37+
.reduce(true, Boolean::logicalAnd);
3738

3839
return builder.status(allUp).build();
3940
}

operator/src/main/java/it/aboutbits/postgresql/core/PostgreSQLContextFactory.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import jakarta.enterprise.context.ApplicationScoped;
66
import lombok.RequiredArgsConstructor;
77
import org.jooq.CloseableDSLContext;
8+
import org.jooq.exception.DataAccessException;
89
import org.jooq.impl.DSL;
910
import org.jspecify.annotations.NullMarked;
1011

@@ -21,7 +22,7 @@ public class PostgreSQLContextFactory {
2122
private final KubernetesClient kubernetesClient;
2223

2324
/// Create a DSLContext with a JDBC connection to the PostgreSQL maintenance database.
24-
public CloseableDSLContext getDSLContext(ClusterConnection clusterConnection) {
25+
public CloseableDSLContext getDSLContext(ClusterConnection clusterConnection) throws DataAccessException {
2526
return getDSLContext(
2627
clusterConnection,
2728
clusterConnection.getSpec().getDatabase()
@@ -32,7 +33,7 @@ public CloseableDSLContext getDSLContext(ClusterConnection clusterConnection) {
3233
public CloseableDSLContext getDSLContext(
3334
ClusterConnection clusterConnection,
3435
String database
35-
) {
36+
) throws DataAccessException {
3637
var credentials = kubernetesService.getSecretRefCredentials(
3738
kubernetesClient,
3839
clusterConnection

operator/src/main/java/it/aboutbits/postgresql/core/HostnameRFC1123Customizer.java renamed to operator/src/main/java/it/aboutbits/postgresql/core/schema_customizer/HostCustomizer.java

Lines changed: 33 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,22 @@
1-
package it.aboutbits.postgresql.core;
1+
package it.aboutbits.postgresql.core.schema_customizer;
22

33
import io.fabric8.crdv2.generator.v1.SchemaCustomizer;
44
import io.fabric8.kubernetes.api.model.apiextensions.v1.JSONSchemaProps;
55
import io.fabric8.kubernetes.client.utils.KubernetesSerialization;
66
import org.jspecify.annotations.NullMarked;
77

88
import java.util.Arrays;
9+
import java.util.List;
910
import java.util.Set;
1011
import java.util.stream.Collectors;
1112

1213
/// A [SchemaCustomizer.Customizer] that sets the `format` of string properties
13-
/// to `"hostname"` (RFC 1123) in the generated CRD JSON Schema.
14+
/// to `{"anyOf":[{"format":"hostname"},{"format":"ipv4"},{"format":"ipv6"}]}`
15+
/// in the generated CRD JSON Schema.
1416
///
1517
/// This customizer is intended to be used with the
1618
/// [@SchemaCustomizer][SchemaCustomizer] annotation on a class whose properties
17-
/// should be validated as RFC 1123 hostnames by the Kubernetes API server.
19+
/// should be validated to valid hosts defined.
1820
///
1921
/// ### Behavior
2022
///
@@ -28,28 +30,28 @@
2830
/// **Apply to all string properties:**
2931
///
3032
/// ```java
31-
/// @SchemaCustomizer(HostnameRFC1123Customizer.class)
32-
/// public class ResourceRef {
33-
/// private String name = ""; // gets format: "hostname"
34-
/// private String namespace; // gets format: "hostname"
33+
/// @SchemaCustomizer(value = HostCustomizer.class)
34+
/// public class ClusterConnectionSpec {
35+
/// private String host = ""; // gets custom format
36+
/// private String anotherHost = ""; // gets custom format
3537
/// }
3638
/// ```
3739
///
3840
/// **Apply to specific properties only:**
3941
///
4042
/// ```java
41-
/// @SchemaCustomizer(value = HostnameRFC1123Customizer.class, input = "host")
43+
/// @SchemaCustomizer(value = HostCustomizer.class, input = "host,anotherHost")
4244
/// public class ClusterConnectionSpec {
43-
/// private String host = ""; // gets format: "hostname"
44-
/// private String anotherHost = ""; // gets format: "hostname"
45-
/// private String database = ""; // unchanged
45+
/// private String host = ""; // gets custom format
46+
/// private String anotherHost = ""; // gets custom format
47+
/// private String unchangedHost = ""; // unchanged
4648
/// }
4749
/// ```
4850
///
4951
/// @see SchemaCustomizer
5052
/// @see SchemaCustomizer.Customizer
5153
@NullMarked
52-
public class HostnameRFC1123Customizer implements SchemaCustomizer.Customizer {
54+
public class HostCustomizer implements SchemaCustomizer.Customizer {
5355
@Override
5456
public JSONSchemaProps apply(
5557
JSONSchemaProps jsonSchemaProps,
@@ -69,10 +71,25 @@ public JSONSchemaProps apply(
6971

7072
for (var entry : properties.entrySet()) {
7173
var prop = entry.getValue();
72-
if ("string".equals(prop.getType())) {
73-
if (targetFields.isEmpty() || targetFields.contains(entry.getKey())) {
74-
prop.setFormat("hostname");
75-
}
74+
if ("string".equals(prop.getType())
75+
&& (targetFields.isEmpty() || targetFields.contains(entry.getKey()))
76+
) {
77+
prop.setFormat(null);
78+
79+
var hostnameProp = new JSONSchemaProps();
80+
hostnameProp.setFormat("hostname");
81+
82+
var ipv4Prop = new JSONSchemaProps();
83+
ipv4Prop.setFormat("ipv4");
84+
85+
var ipv6Prop = new JSONSchemaProps();
86+
ipv6Prop.setFormat("ipv6");
87+
88+
prop.setAnyOf(List.of(
89+
hostnameProp,
90+
ipv4Prop,
91+
ipv6Prop
92+
));
7693
}
7794
}
7895

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
package it.aboutbits.postgresql.core.schema_customizer;
2+
3+
import io.fabric8.crdv2.generator.v1.SchemaCustomizer;
4+
import io.fabric8.kubernetes.api.model.apiextensions.v1.JSONSchemaProps;
5+
import io.fabric8.kubernetes.client.utils.KubernetesSerialization;
6+
import org.jspecify.annotations.NullMarked;
7+
8+
import java.util.Arrays;
9+
import java.util.Set;
10+
import java.util.stream.Collectors;
11+
12+
/// A [SchemaCustomizer.Customizer] that adds a Kubernetes name validation
13+
/// `pattern` (RFC 1123 DNS label) to string properties in the generated CRD
14+
/// JSON Schema.
15+
///
16+
/// The pattern enforces:
17+
/// - Contain at most 63 characters
18+
/// - Contain only lowercase alphanumeric characters or '-'
19+
/// - Start with an alphabetic character
20+
/// - End with an alphanumeric character
21+
///
22+
/// This customizer is intended to be used with the
23+
/// [@SchemaCustomizer][SchemaCustomizer] annotation on a class whose string
24+
/// properties represent Kubernetes resource names.
25+
///
26+
/// ### Behavior
27+
///
28+
/// - If `input` is **blank** (the default), the `"hostname"` format
29+
/// is applied to **all** string properties of the annotated class.
30+
/// - If `input` contains a **comma-separated list** of field names,
31+
/// the format is applied **only** to the specified properties.
32+
///
33+
/// ### Usage examples
34+
///
35+
/// **Apply to all string properties:**
36+
///
37+
/// ```java
38+
/// @SchemaCustomizer(KubernetesNameCustomizer.class)
39+
/// public class SecretRef {
40+
/// private String name = ""; // gets pattern: Kubernetes name regex
41+
/// private String namespace; // gets pattern: Kubernetes name regex
42+
/// }
43+
/// ```
44+
///
45+
/// **Apply to specific properties only:**
46+
///
47+
/// ```java
48+
/// @SchemaCustomizer(value = KubernetesNameCustomizer.class, input = "name,anotherName")
49+
/// public class SecretRef {
50+
/// private String name = ""; // gets pattern: Kubernetes name regex
51+
/// private String anotherName = ""; // gets pattern: Kubernetes name regex
52+
/// private String namespace; // unchanged
53+
/// }
54+
/// ```
55+
///
56+
/// @see SchemaCustomizer
57+
/// @see SchemaCustomizer.Customizer
58+
@NullMarked
59+
public class KubernetesNameCustomizer implements SchemaCustomizer.Customizer {
60+
static final String KUBERNETES_NAME_PATTERN = "^[a-z]([a-z0-9\\-]{0,61}[a-z0-9])?$";
61+
62+
@Override
63+
public JSONSchemaProps apply(
64+
JSONSchemaProps jsonSchemaProps,
65+
String input,
66+
KubernetesSerialization kubernetesSerialization
67+
) {
68+
var properties = jsonSchemaProps.getProperties();
69+
if (properties == null) {
70+
return jsonSchemaProps;
71+
}
72+
73+
var targetFields = input.isBlank()
74+
? Set.<String>of()
75+
: Arrays.stream(input.split(","))
76+
.map(String::trim)
77+
.collect(Collectors.toSet());
78+
79+
for (var entry : properties.entrySet()) {
80+
var prop = entry.getValue();
81+
if ("string".equals(prop.getType())
82+
&& (targetFields.isEmpty() || targetFields.contains(entry.getKey()))
83+
) {
84+
prop.setPattern(KUBERNETES_NAME_PATTERN);
85+
}
86+
}
87+
88+
return jsonSchemaProps;
89+
}
90+
}

operator/src/main/java/it/aboutbits/postgresql/crd/clusterconnection/ClusterConnectionSpec.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@
55
import io.fabric8.generator.annotation.Min;
66
import io.fabric8.generator.annotation.Required;
77
import io.fabric8.generator.annotation.ValidationRule;
8-
import it.aboutbits.postgresql.core.HostnameRFC1123Customizer;
98
import it.aboutbits.postgresql.core.ResourceRef;
9+
import it.aboutbits.postgresql.core.schema_customizer.HostCustomizer;
1010
import lombok.Getter;
1111
import lombok.Setter;
1212
import org.jspecify.annotations.NullMarked;
@@ -17,7 +17,7 @@
1717
@NullMarked
1818
@Getter
1919
@Setter
20-
@SchemaCustomizer(value = HostnameRFC1123Customizer.class, input = "host")
20+
@SchemaCustomizer(value = HostCustomizer.class, input = "host")
2121
public class ClusterConnectionSpec {
2222
@Required
2323
@ValidationRule(

operator/src/test/java/it/aboutbits/postgresql/PostgreSQLInstanceReadinessCheckTest.java

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import org.junit.jupiter.api.BeforeEach;
1212
import org.junit.jupiter.api.Test;
1313

14+
import java.util.Map;
1415
import java.util.Objects;
1516

1617
import static org.assertj.core.api.Assertions.assertThat;
@@ -67,13 +68,35 @@ void call_whenSomeConnectionsDown_shouldReturnDown() {
6768
given.one()
6869
.clusterConnection()
6970
.withName("db-1")
70-
.returnFirst();
71+
.apply();
7172

7273
given.one()
7374
.clusterConnection()
7475
.withName("db-2")
75-
.withHost("non-existent-host")
76-
.returnFirst();
76+
.withHost("localhost")
77+
.withPort(2345) // Wrong port
78+
.apply();
79+
80+
given.one()
81+
.clusterConnection()
82+
.withName("db-3")
83+
.withHost("127.0.0.1")
84+
.withPort(2345) // Wrong port
85+
.apply();
86+
87+
given.one()
88+
.clusterConnection()
89+
.withName("db-4")
90+
.withHost("::1")
91+
.withPort(2345) // Wrong port
92+
.apply();
93+
94+
given.one()
95+
.clusterConnection()
96+
.withName("db-5")
97+
.withHost("0:0:0:0:0:0:0:1")
98+
.withPort(2345) // Wrong port
99+
.apply();
77100

78101
var response = readinessCheck.call();
79102

@@ -93,7 +116,12 @@ void call_whenSomeConnectionsDown_shouldReturnDown() {
93116

94117
assertThat(dbStatus.toString()).startsWith("UP (PostgreSQL");
95118

96-
assertThat(data).containsEntry("db-2", "DOWN");
119+
assertThat(data).containsAllEntriesOf(Map.of(
120+
"db-2", "DOWN",
121+
"db-3", "DOWN",
122+
"db-4", "DOWN",
123+
"db-5", "DOWN"
124+
));
97125
});
98126
}
99127
}

0 commit comments

Comments
 (0)