From 31860e8b6fc1e2537cb869e61ee6ed414b161761 Mon Sep 17 00:00:00 2001 From: Thomas Sapelza Date: Wed, 25 Feb 2026 16:38:40 +0100 Subject: [PATCH 01/10] update to Quarkus 3.32.1 and update dependencies to latest versions --- gradle.properties | 4 ++-- gradle/libs.versions.toml | 12 ++++++------ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/gradle.properties b/gradle.properties index f646d00..3b9d1fa 100644 --- a/gradle.properties +++ b/gradle.properties @@ -7,9 +7,9 @@ org.gradle.logging.level=INFO # Quarkus quarkusPluginId=io.quarkus -quarkusPluginVersion=3.31.2 +quarkusPluginVersion=3.32.1 # https://mvnrepository.com/artifact/io.quarkus.platform/quarkus-bom quarkusPlatformGroupId=io.quarkus.platform quarkusPlatformArtifactId=quarkus-bom -quarkusPlatformVersion=3.31.2 +quarkusPlatformVersion=3.32.1 systemProp.quarkus.analytics.disabled=true diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 8242d32..14846fe 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,6 +1,6 @@ [versions] ## AboutBits Libraries ## -checkstyleConfig = "2.0.0-RC1" +checkstyleConfig = "2.0.0-RC2" # Axion Release Plugin # axionReleasePlugin = "1.21.1" @@ -9,15 +9,15 @@ axionReleasePlugin = "1.21.1" jooq = "3.20.11" jSpecify = "1.0.0" lombok = "1.18.42" -postgresql = "42.7.9" -quarkiverse-helm = "1.2.7" +postgresql = "42.7.10" +quarkiverse-helm = "1.3.0" scram-client = "3.2" ## Testing ## assertj = "3.27.7" -checkstyle = "13.1.0" -datafaker = "2.5.3" -errorProne = "2.46.0" +checkstyle = "13.2.0" +datafaker = "2.5.4" +errorProne = "2.47.0" errorPronePlugin = "5.0.0" nullAway = "0.13.1" From 841b02d04b71eef300d8714ce6a5aacbe2226dbc Mon Sep 17 00:00:00 2001 From: Thomas Sapelza Date: Thu, 26 Feb 2026 07:57:11 +0100 Subject: [PATCH 02/10] validate K8s resource references against the RFC 1123 hostname format Kubernetes uses --- operator/build.gradle.kts | 1 + .../postgresql/core/ClusterReference.java | 2 + .../core/HostnameRFC1123Customizer.java | 81 +++++++++++++++++++ .../aboutbits/postgresql/core/SecretRef.java | 2 + .../ClusterConnectionSpec.java | 3 + 5 files changed, 89 insertions(+) create mode 100644 operator/src/main/java/it/aboutbits/postgresql/core/HostnameRFC1123Customizer.java diff --git a/operator/build.gradle.kts b/operator/build.gradle.kts index cbe6c1f..1f2f0bb 100644 --- a/operator/build.gradle.kts +++ b/operator/build.gradle.kts @@ -20,6 +20,7 @@ dependencies { * Fabric8 Kubernetes Client */ implementation("io.fabric8:generator-annotations") + implementation("io.fabric8:crd-generator-api-v2") /** * jOOQ diff --git a/operator/src/main/java/it/aboutbits/postgresql/core/ClusterReference.java b/operator/src/main/java/it/aboutbits/postgresql/core/ClusterReference.java index b1b9a7d..237d9de 100644 --- a/operator/src/main/java/it/aboutbits/postgresql/core/ClusterReference.java +++ b/operator/src/main/java/it/aboutbits/postgresql/core/ClusterReference.java @@ -1,5 +1,6 @@ package it.aboutbits.postgresql.core; +import io.fabric8.crdv2.generator.v1.SchemaCustomizer; import io.fabric8.generator.annotation.Required; import io.fabric8.generator.annotation.ValidationRule; import lombok.Getter; @@ -10,6 +11,7 @@ @NullMarked @Getter @Setter +@SchemaCustomizer(HostnameRFC1123Customizer.class) public class ClusterReference { @Required @ValidationRule( diff --git a/operator/src/main/java/it/aboutbits/postgresql/core/HostnameRFC1123Customizer.java b/operator/src/main/java/it/aboutbits/postgresql/core/HostnameRFC1123Customizer.java new file mode 100644 index 0000000..6ec967b --- /dev/null +++ b/operator/src/main/java/it/aboutbits/postgresql/core/HostnameRFC1123Customizer.java @@ -0,0 +1,81 @@ +package it.aboutbits.postgresql.core; + +import io.fabric8.crdv2.generator.v1.SchemaCustomizer; +import io.fabric8.kubernetes.api.model.apiextensions.v1.JSONSchemaProps; +import io.fabric8.kubernetes.client.utils.KubernetesSerialization; +import org.jspecify.annotations.NullMarked; + +import java.util.Arrays; +import java.util.Set; +import java.util.stream.Collectors; + +/// A [SchemaCustomizer.Customizer] that sets the `format` of string properties +/// to `"hostname"` (RFC 1123) in the generated CRD JSON Schema. +/// +/// This customizer is intended to be used with the +/// [@SchemaCustomizer][SchemaCustomizer] annotation on a class whose properties +/// should be validated as RFC 1123 hostnames by the Kubernetes API server. +/// +/// ### Behavior +/// +/// - If `input` is **blank** (the default), the `"hostname"` format +/// is applied to **all** string properties of the annotated class. +/// - If `input` contains a **comma-separated list** of field names, +/// the format is applied **only** to the specified properties. +/// +/// ### Usage examples +/// +/// **Apply to all string properties:** +/// +/// ```java +/// @SchemaCustomizer(HostnameRFC1123Customizer.class) +/// public class SecretRef { +/// private String name = ""; // gets format: "hostname" +/// private String namespace; // gets format: "hostname" +/// } +/// ``` +/// +/// **Apply to specific properties only:** +/// +/// ```java +/// @SchemaCustomizer(value = HostnameRFC1123Customizer.class, input = "host") +/// public class ClusterConnectionSpec { +/// private String host = ""; // gets format: "hostname" +/// private String anotherHost = ""; // gets format: "hostname" +/// private String database = ""; // unchanged +/// } +/// ``` +/// +/// @see SchemaCustomizer +/// @see SchemaCustomizer.Customizer +@NullMarked +public class HostnameRFC1123Customizer implements SchemaCustomizer.Customizer { + @Override + public JSONSchemaProps apply( + JSONSchemaProps jsonSchemaProps, + String input, + KubernetesSerialization kubernetesSerialization + ) { + var properties = jsonSchemaProps.getProperties(); + if (properties == null) { + return jsonSchemaProps; + } + + var targetFields = input.isBlank() + ? Set.of() + : Arrays.stream(input.split(",")) + .map(String::trim) + .collect(Collectors.toSet()); + + for (var entry : properties.entrySet()) { + var prop = entry.getValue(); + if ("string".equals(prop.getType())) { + if (targetFields.isEmpty() || targetFields.contains(entry.getKey())) { + prop.setFormat("hostname"); + } + } + } + + return jsonSchemaProps; + } +} diff --git a/operator/src/main/java/it/aboutbits/postgresql/core/SecretRef.java b/operator/src/main/java/it/aboutbits/postgresql/core/SecretRef.java index 9e4b57a..6f71b88 100644 --- a/operator/src/main/java/it/aboutbits/postgresql/core/SecretRef.java +++ b/operator/src/main/java/it/aboutbits/postgresql/core/SecretRef.java @@ -1,5 +1,6 @@ package it.aboutbits.postgresql.core; +import io.fabric8.crdv2.generator.v1.SchemaCustomizer; import io.fabric8.generator.annotation.Required; import io.fabric8.generator.annotation.ValidationRule; import lombok.Getter; @@ -10,6 +11,7 @@ @NullMarked @Getter @Setter +@SchemaCustomizer(HostnameRFC1123Customizer.class) public class SecretRef { @Required @ValidationRule( diff --git a/operator/src/main/java/it/aboutbits/postgresql/crd/clusterconnection/ClusterConnectionSpec.java b/operator/src/main/java/it/aboutbits/postgresql/crd/clusterconnection/ClusterConnectionSpec.java index 65b4ec8..8e5fc11 100644 --- a/operator/src/main/java/it/aboutbits/postgresql/crd/clusterconnection/ClusterConnectionSpec.java +++ b/operator/src/main/java/it/aboutbits/postgresql/crd/clusterconnection/ClusterConnectionSpec.java @@ -1,10 +1,12 @@ package it.aboutbits.postgresql.crd.clusterconnection; +import io.fabric8.crdv2.generator.v1.SchemaCustomizer; import io.fabric8.generator.annotation.Max; import io.fabric8.generator.annotation.Min; import io.fabric8.generator.annotation.Required; import io.fabric8.generator.annotation.ValidationRule; import it.aboutbits.postgresql.core.SecretRef; +import it.aboutbits.postgresql.core.HostnameRFC1123Customizer; import lombok.Getter; import lombok.Setter; import org.jspecify.annotations.NullMarked; @@ -15,6 +17,7 @@ @NullMarked @Getter @Setter +@SchemaCustomizer(value = HostnameRFC1123Customizer.class, input = "host") public class ClusterConnectionSpec { @Required @ValidationRule( From ba04f010c008a1a3299c0b4447a4d73be84351a0 Mon Sep 17 00:00:00 2001 From: Thomas Sapelza Date: Thu, 26 Feb 2026 08:27:02 +0100 Subject: [PATCH 03/10] refactor SecretRef and ClusterReference to one common ResourceRef type --- docs/cluster-connection.md | 12 ++--- docs/database.md | 26 +++++------ docs/default-privilege.md | 28 ++++++------ docs/grant.md | 28 ++++++------ docs/role.md | 39 +++++++--------- docs/schema.md | 28 ++++++------ .../postgresql/core/BaseReconciler.java | 2 +- .../postgresql/core/ClusterReference.java | 26 ----------- .../core/HostnameRFC1123Customizer.java | 2 +- .../postgresql/core/KubernetesService.java | 10 ++--- .../postgresql/core/ResourceRef.java | 44 +++++++++++++++++++ .../aboutbits/postgresql/core/SecretRef.java | 30 ------------- .../ClusterConnectionSpec.java | 4 +- .../postgresql/crd/database/DatabaseSpec.java | 4 +- .../DefaultPrivilegeSpec.java | 4 +- .../postgresql/crd/grant/GrantSpec.java | 4 +- .../postgresql/crd/role/RoleSpec.java | 7 ++- .../postgresql/crd/schema/SchemaSpec.java | 4 +- .../creator/ClusterConnectionCreate.java | 6 +-- .../persisted/creator/DatabaseCreate.java | 4 +- .../creator/DefaultPrivilegeCreate.java | 4 +- .../persisted/creator/GrantCreate.java | 4 +- .../persisted/creator/RoleCreate.java | 7 ++- .../persisted/creator/SchemaCreate.java | 4 +- .../persisted/creator/SecretRefCreate.java | 8 ++-- .../crd/role/RoleReconcilerTest.java | 4 +- 26 files changed, 161 insertions(+), 182 deletions(-) delete mode 100644 operator/src/main/java/it/aboutbits/postgresql/core/ClusterReference.java create mode 100644 operator/src/main/java/it/aboutbits/postgresql/core/ResourceRef.java delete mode 100644 operator/src/main/java/it/aboutbits/postgresql/core/SecretRef.java diff --git a/docs/cluster-connection.md b/docs/cluster-connection.md index d65f2ae..0b88e39 100644 --- a/docs/cluster-connection.md +++ b/docs/cluster-connection.md @@ -12,15 +12,15 @@ Other Custom Resources (like `Database`, `Role`, `Schema`, `Grant`, `DefaultPriv | `host` | `string` | The hostname of the PostgreSQL instance. | Yes | | `port` | `integer` | The port of the PostgreSQL instance (1-65535). | Yes | | `database` | `string` | The database to connect to (usually `postgres` for admin operations). | Yes | -| `adminSecretRef` | `SecretRef` | Reference to the secret containing admin credentials. | Yes | +| `adminSecretRef` | `ResourceRef` | Reference to the Kubernetes Secret containing the admin credentials. | Yes | | `parameters` | `map[string]string` | Additional connection parameters. | No | -### SecretRef +### ResourceRef (`adminSecretRef`) -| Field | Type | Description | Required | -|-------------|----------|---------------------------------------------------------------------|----------| -| `name` | `string` | Name of the secret. | Yes | -| `namespace` | `string` | Namespace of the secret. If not specified, uses the CR's namespace. | No | +| Field | Type | Description | Required | +|-------------|----------|----------------------------------------------------------------------------------------------------|----------| +| `namespace` | `string` | Namespace of the referenced Kubernetes `Secret`. If not specified, uses the owning CR's namespace. | No | +| `name` | `string` | Name of the referenced Kubernetes `Secret`. | Yes | The referenced secret must be of type `kubernetes.io/basic-auth` and contain the keys `username` and `password`. diff --git a/docs/database.md b/docs/database.md index 4f4a88c..d1c7df4 100644 --- a/docs/database.md +++ b/docs/database.md @@ -4,19 +4,19 @@ The `Database` Custom Resource Definition (CRD) is responsible for managing Post ## Spec -| Field | Type | Description | Required | Immutable | -|-----------------|--------------------|------------------------------------------------------------------------------------------------------|----------|-----------| -| `clusterRef` | `ClusterReference` | Reference to the `ClusterConnection` to use. | Yes | No | -| `name` | `string` | The name of the database to create. | Yes | Yes | -| `owner` | `string` | The owner of the database. | No | No | -| `reclaimPolicy` | `string` | The policy for reclaiming the database when the CR is deleted. Values: `Retain` (Default), `Delete`. | No | No | - -### ClusterReference - -| Field | Type | Description | Required | -|-------------|----------|----------------------------------------------------------------------------------|----------| -| `name` | `string` | Name of the `ClusterConnection`. | Yes | -| `namespace` | `string` | Namespace of the `ClusterConnection`. If not specified, uses the CR's namespace. | No | +| Field | Type | Description | Required | Immutable | +|-----------------|---------------|------------------------------------------------------------------------------------------------------|----------|-----------| +| `clusterRef` | `ResourceRef` | Reference to the `ClusterConnection` to use. | Yes | No | +| `name` | `string` | The name of the database to create. | Yes | Yes | +| `owner` | `string` | The owner of the database. | No | No | +| `reclaimPolicy` | `string` | The policy for reclaiming the database when the CR is deleted. Values: `Retain` (Default), `Delete`. | No | No | + +### ResourceRef (`clusterRef`) + +| Field | Type | Description | Required | +|-------------|----------|----------------------------------------------------------------------------------------------------|----------| +| `namespace` | `string` | Namespace of the referenced `ClusterConnection`. If not specified, uses the owning CR's namespace. | No | +| `name` | `string` | Name of the referenced `ClusterConnection`. | Yes | ### Reclaim Policy diff --git a/docs/default-privilege.md b/docs/default-privilege.md index e532671..19d24ae 100644 --- a/docs/default-privilege.md +++ b/docs/default-privilege.md @@ -4,15 +4,15 @@ The `DefaultPrivilege` Custom Resource Definition (CRD) manages default privileg ## Spec -| Field | Type | Description | Required | Immutable | -|--------------|--------------------|---------------------------------------------------------------------------------------------------------|-------------|-----------| -| `clusterRef` | `ClusterReference` | Reference to the `ClusterConnection` to use. | Yes | No | -| `database` | `string` | The database where default privileges apply. | Yes | Yes | -| `role` | `string` | The role to which default privileges are granted. | Yes | Yes | -| `owner` | `string` | The role that owns the objects (the creator). Default privileges apply to objects created by this role. | Yes | Yes | -| `schema` | `string` | The schema where default privileges apply. Required, unless `objectType` is `schema`. | Conditional | Yes | -| `objectType` | `string` | The type of object. | Yes | Yes | -| `privileges` | `array[string]` | List of privileges to grant. | Yes | No | +| Field | Type | Description | Required | Immutable | +|--------------|-----------------|---------------------------------------------------------------------------------------------------------|-------------|-----------| +| `clusterRef` | `ResourceRef` | Reference to the `ClusterConnection` to use. | Yes | No | +| `database` | `string` | The database where default privileges apply. | Yes | Yes | +| `role` | `string` | The role to which default privileges are granted. | Yes | Yes | +| `owner` | `string` | The role that owns the objects (the creator). Default privileges apply to objects created by this role. | Yes | Yes | +| `schema` | `string` | The schema where default privileges apply. Required, unless `objectType` is `schema`. | Conditional | Yes | +| `objectType` | `string` | The type of object. | Yes | Yes | +| `privileges` | `array[string]` | List of privileges to grant. | Yes | No | ### Object Types @@ -39,12 +39,12 @@ Supported privileges depend on the `objectType`: - `update` - `usage` -### ClusterReference +### ResourceRef (`clusterRef`) -| Field | Type | Description | Required | -|-------------|----------|----------------------------------------------------------------------------------|----------| -| `name` | `string` | Name of the `ClusterConnection`. | Yes | -| `namespace` | `string` | Namespace of the `ClusterConnection`. If not specified, uses the CR's namespace. | No | +| Field | Type | Description | Required | +|-------------|----------|----------------------------------------------------------------------------------------------------|----------| +| `namespace` | `string` | Namespace of the referenced `ClusterConnection`. If not specified, uses the owning CR's namespace. | No | +| `name` | `string` | Name of the referenced `ClusterConnection`. | Yes | ## Example diff --git a/docs/grant.md b/docs/grant.md index 1517d1f..7c24d9f 100644 --- a/docs/grant.md +++ b/docs/grant.md @@ -4,15 +4,15 @@ The `Grant` Custom Resource Definition (CRD) is responsible for managing privile ## Spec -| Field | Type | Description | Required | Immutable | -|--------------|--------------------|--------------------------------------------------------------------------------------------------------------------------------------------|-------------|-----------| -| `clusterRef` | `ClusterReference` | Reference to the `ClusterConnection` to use. | Yes | No | -| `database` | `string` | The database containing the objects. | Yes | Yes | -| `role` | `string` | The role to which privileges are granted. | Yes | Yes | -| `schema` | `string` | The schema containing the objects. Required, unless `objectType` is `database`. | Conditional | Yes | -| `objectType` | `string` | The type of object. | Yes | Yes | -| `objects` | `array[string]` | List of object names. If empty, all objects of this `objectType` will be granted. Required, unless `objectType` is `database` or `schema`. | Conditional | No | -| `privileges` | `array[string]` | List of privileges to grant. | Yes | No | +| Field | Type | Description | Required | Immutable | +|--------------|-----------------|--------------------------------------------------------------------------------------------------------------------------------------------|-------------|-----------| +| `clusterRef` | `ResourceRef` | Reference to the `ClusterConnection` to use. | Yes | No | +| `database` | `string` | The database containing the objects. | Yes | Yes | +| `role` | `string` | The role to which privileges are granted. | Yes | Yes | +| `schema` | `string` | The schema containing the objects. Required, unless `objectType` is `database`. | Conditional | Yes | +| `objectType` | `string` | The type of object. | Yes | Yes | +| `objects` | `array[string]` | List of object names. If empty, all objects of this `objectType` will be granted. Required, unless `objectType` is `database` or `schema`. | Conditional | No | +| `privileges` | `array[string]` | List of privileges to grant. | Yes | No | ### Object Types @@ -40,12 +40,12 @@ Supported privileges depend on the `objectType`: - `update` - `usage` -### ClusterReference +### ResourceRef (`clusterRef`) -| Field | Type | Description | Required | -|-------------|----------|----------------------------------------------------------------------------------|----------| -| `name` | `string` | Name of the `ClusterConnection`. | Yes | -| `namespace` | `string` | Namespace of the `ClusterConnection`. If not specified, uses the CR's namespace. | No | +| Field | Type | Description | Required | +|-------------|----------|----------------------------------------------------------------------------------------------------|----------| +| `namespace` | `string` | Namespace of the referenced `ClusterConnection`. If not specified, uses the owning CR's namespace. | No | +| `name` | `string` | Name of the referenced `ClusterConnection`. | Yes | ## Example diff --git a/docs/role.md b/docs/role.md index 75ee361..dddeb59 100644 --- a/docs/role.md +++ b/docs/role.md @@ -4,20 +4,24 @@ The `Role` Custom Resource Definition (CRD) manages PostgreSQL roles (users). ## Spec -| Field | Type | Description | Required | Immutable | -|---------------------|--------------------|-------------------------------------------------------------------------------------|----------|-----------| -| `clusterRef` | `ClusterReference` | Reference to the `ClusterConnection` to use. | Yes | No | -| `name` | `string` | The name of the role to create in the database. | Yes | Yes | -| `comment` | `string` | A comment to add to the role. | No | No | -| `passwordSecretRef` | `SecretRef` | Reference to a secret containing the password for the role to make it a LOGIN role. | No | No | -| `flags` | `RoleFlags` | Flags and attributes for the role. | No | No | +| Field | Type | Description | Required | Immutable | +|---------------------|---------------|-------------------------------------------------------------------------------------|----------|-----------| +| `clusterRef` | `ResourceRef` | Reference to the `ClusterConnection` to use. | Yes | No | +| `name` | `string` | The name of the role to create in the database. | Yes | Yes | +| `comment` | `string` | A comment to add to the role. | No | No | +| `passwordSecretRef` | `ResourceRef` | Reference to a secret containing the password for the role to make it a LOGIN role. | No | No | +| `flags` | `RoleFlags` | Flags and attributes for the role. | No | No | -### ClusterReference +### ResourceRef (`clusterRef` and `passwordSecretRef`) -| Field | Type | Description | Required | -|-------------|----------|----------------------------------------------------------------------------------|----------| -| `name` | `string` | Name of the `ClusterConnection`. | Yes | -| `namespace` | `string` | Namespace of the `ClusterConnection`. If not specified, uses the CR's namespace. | No | +| Field | Type | Description | Required | +|-------------|----------|-----------------------------------------------------------------------------------------|----------| +| `namespace` | `string` | Namespace of the referenced resource. If not specified, uses the owning CR's namespace. | No | +| `name` | `string` | Name of the referenced Kubernetes resource. | Yes | + +**Note**: +When used as `passwordSecretRef`, the referenced Kubernetes Secret must be of type `kubernetes.io/basic-auth`. +The `username` key in the Secret is not strictly required, as the role name is specified by the `name` field in the CRD. Only the `password` key is used. ### RoleFlags @@ -34,17 +38,6 @@ The `Role` Custom Resource Definition (CRD) manages PostgreSQL roles (users). | `superuser` | `boolean` | `false` | Superuser status. | | `validUntil` | `string` | `null` | Date and time until the password is valid (ISO 8601). | -### SecretRef - -| Field | Type | Description | Required | -|-------------|----------|---------------------------------------------------------------------|----------| -| `name` | `string` | Name of the secret. | Yes | -| `namespace` | `string` | Namespace of the secret. If not specified, uses the CR's namespace. | No | - -The referenced secret must be of type `kubernetes.io/basic-auth`. - -**Note**: The `username` key in the secret is not strictly required, as the role name is specified by the `name` field in the CRD. Only the `password` key is used. - ### Login vs No-Login Roles The operator uses the presence of the `passwordSecretRef` field to determine if the role should have the `LOGIN` privilege (User) or not (Group). diff --git a/docs/schema.md b/docs/schema.md index 0d08235..edd70a4 100644 --- a/docs/schema.md +++ b/docs/schema.md @@ -4,20 +4,20 @@ The `Schema` Custom Resource Definition (CRD) is responsible for managing Postgr ## Spec -| Field | Type | Description | Required | Immutable | -|-----------------|--------------------|----------------------------------------------------------------------------------------------------|----------|-----------| -| `clusterRef` | `ClusterReference` | Reference to the `ClusterConnection` to use. | Yes | No | -| `database` | `string` | The name of the database in which the schema is created. | Yes | Yes | -| `name` | `string` | The name of the schema to create. | Yes | Yes | -| `owner` | `string` | The owner of the schema. | No | No | -| `reclaimPolicy` | `string` | The policy for reclaiming the schema when the CR is deleted. Values: `Retain` (Default), `Delete`. | No | No | - -### ClusterReference - -| Field | Type | Description | Required | -|-------------|----------|----------------------------------------------------------------------------------|----------| -| `name` | `string` | Name of the `ClusterConnection`. | Yes | -| `namespace` | `string` | Namespace of the `ClusterConnection`. If not specified, uses the CR's namespace. | No | +| Field | Type | Description | Required | Immutable | +|-----------------|---------------|----------------------------------------------------------------------------------------------------|----------|-----------| +| `clusterRef` | `ResourceRef` | Reference to the `ClusterConnection` to use. | Yes | No | +| `database` | `string` | The name of the database in which the schema is created. | Yes | Yes | +| `name` | `string` | The name of the schema to create. | Yes | Yes | +| `owner` | `string` | The owner of the schema. | No | No | +| `reclaimPolicy` | `string` | The policy for reclaiming the schema when the CR is deleted. Values: `Retain` (Default), `Delete`. | No | No | + +### ResourceRef (`clusterRef`) + +| Field | Type | Description | Required | +|-------------|----------|----------------------------------------------------------------------------------------------------|----------| +| `namespace` | `string` | Namespace of the referenced `ClusterConnection`. If not specified, uses the owning CR's namespace. | No | +| `name` | `string` | Name of the referenced `ClusterConnection`. | Yes | ### Reclaim Policy diff --git a/operator/src/main/java/it/aboutbits/postgresql/core/BaseReconciler.java b/operator/src/main/java/it/aboutbits/postgresql/core/BaseReconciler.java index f12d71d..306b069 100644 --- a/operator/src/main/java/it/aboutbits/postgresql/core/BaseReconciler.java +++ b/operator/src/main/java/it/aboutbits/postgresql/core/BaseReconciler.java @@ -49,7 +49,7 @@ public String getResourceNamespaceOrOwn( public Optional getReferencedClusterConnection( KubernetesClient kubernetesClient, CR resource, - ClusterReference clusterRef + ResourceRef clusterRef ) { var connectionName = clusterRef.getName(); var connectionNamespace = getResourceNamespaceOrOwn(resource, clusterRef.getNamespace()); diff --git a/operator/src/main/java/it/aboutbits/postgresql/core/ClusterReference.java b/operator/src/main/java/it/aboutbits/postgresql/core/ClusterReference.java deleted file mode 100644 index 237d9de..0000000 --- a/operator/src/main/java/it/aboutbits/postgresql/core/ClusterReference.java +++ /dev/null @@ -1,26 +0,0 @@ -package it.aboutbits.postgresql.core; - -import io.fabric8.crdv2.generator.v1.SchemaCustomizer; -import io.fabric8.generator.annotation.Required; -import io.fabric8.generator.annotation.ValidationRule; -import lombok.Getter; -import lombok.Setter; -import org.jspecify.annotations.NullMarked; -import org.jspecify.annotations.Nullable; - -@NullMarked -@Getter -@Setter -@SchemaCustomizer(HostnameRFC1123Customizer.class) -public class ClusterReference { - @Required - @ValidationRule( - value = "self.trim().size() > 0", - message = "The ClusterReference name must not be empty." - ) - private String name = ""; - - @Nullable - @io.fabric8.generator.annotation.Nullable - private String namespace; -} diff --git a/operator/src/main/java/it/aboutbits/postgresql/core/HostnameRFC1123Customizer.java b/operator/src/main/java/it/aboutbits/postgresql/core/HostnameRFC1123Customizer.java index 6ec967b..638e902 100644 --- a/operator/src/main/java/it/aboutbits/postgresql/core/HostnameRFC1123Customizer.java +++ b/operator/src/main/java/it/aboutbits/postgresql/core/HostnameRFC1123Customizer.java @@ -29,7 +29,7 @@ /// /// ```java /// @SchemaCustomizer(HostnameRFC1123Customizer.class) -/// public class SecretRef { +/// public class ResourceRef { /// private String name = ""; // gets format: "hostname" /// private String namespace; // gets format: "hostname" /// } diff --git a/operator/src/main/java/it/aboutbits/postgresql/core/KubernetesService.java b/operator/src/main/java/it/aboutbits/postgresql/core/KubernetesService.java index 2252b1a..203cdd5 100644 --- a/operator/src/main/java/it/aboutbits/postgresql/core/KubernetesService.java +++ b/operator/src/main/java/it/aboutbits/postgresql/core/KubernetesService.java @@ -28,7 +28,7 @@ public Credentials getSecretRefCredentials( public Credentials getSecretRefCredentials( KubernetesClient kubernetesClient, - SecretRef secretRef, + ResourceRef secretRef, String defaultNamespace ) { var secretNamespace = secretRef.getNamespace() != null @@ -43,14 +43,14 @@ public Credentials getSecretRefCredentials( .get(); if (secret == null) { - throw new IllegalStateException("SecretRef not found [secret.namespace=%s, secret.name=%s]".formatted( + throw new IllegalStateException("Secret reference not found [secret.namespace=%s, secret.name=%s]".formatted( secretNamespace, secretName )); } if (!secret.getType().equals(SECRET_TYPE_BASIC_AUTH)) { - 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( + throw new IllegalArgumentException("The Secret reference is of the wrong type [secret.namespace=%s, secret.name=%s, expected.secret.type=%s, actual.secret.type=%s]".formatted( secretNamespace, secretName, SECRET_TYPE_BASIC_AUTH, @@ -60,7 +60,7 @@ public Credentials getSecretRefCredentials( var data = secret.getData(); if (data == null || data.isEmpty()) { - throw new IllegalStateException("The SecretRef has no data set [secret.namespace=%s, secret.name=%s]".formatted( + throw new IllegalStateException("The Secret reference has no data set [secret.namespace=%s, secret.name=%s]".formatted( secretNamespace, secretName )); @@ -76,7 +76,7 @@ public Credentials getSecretRefCredentials( var passwordBase64 = data.get(SECRET_DATA_BASIC_AUTH_PASSWORD_KEY); if (passwordBase64 == null) { - throw new IllegalStateException("The SecretRef is missing required data password [secret.namespace=%s, secret.name=%s]".formatted( + throw new IllegalStateException("The Secret reference is missing required data password [secret.namespace=%s, secret.name=%s]".formatted( secretNamespace, secretName )); diff --git a/operator/src/main/java/it/aboutbits/postgresql/core/ResourceRef.java b/operator/src/main/java/it/aboutbits/postgresql/core/ResourceRef.java new file mode 100644 index 0000000..11f28f5 --- /dev/null +++ b/operator/src/main/java/it/aboutbits/postgresql/core/ResourceRef.java @@ -0,0 +1,44 @@ +package it.aboutbits.postgresql.core; + +import io.fabric8.crdv2.generator.v1.SchemaCustomizer; +import io.fabric8.generator.annotation.Required; +import io.fabric8.generator.annotation.ValidationRule; +import lombok.Getter; +import lombok.Setter; +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; + +/// A reference to a Kubernetes resource identified by [#namespace] (optional) and [#name]. +/// +/// This class is used wherever a CRD spec needs to point to another Kubernetes resource. +/// +/// ### Namespace resolution +/// +/// The [#namespace] field is **nullable**. When it is `null` (or omitted +/// in the CR manifest), the operator resolves the target resource in the +/// **same namespace as the CR that contains the reference**. This convention +/// keeps single-namespace deployments simple — users only need to set +/// `namespace` when referring to a resource in a *different* namespace. +/// +/// | `namespace` value | Resolved namespace | +/// |-------------------|-----------------------------------------------------| +/// | non-null | the explicit namespace | +/// | `null` (omitted) | the namespace of the CR that owns this reference | +@NullMarked +@Getter +@Setter +@SchemaCustomizer(HostnameRFC1123Customizer.class) +public class ResourceRef { + /// The namespace of the referenced Kubernetes resource. + /// If `null`, defaults to the namespace of the CR that defines this reference. + @Nullable + @io.fabric8.generator.annotation.Nullable + private String namespace; + + @Required + @ValidationRule( + value = "self.trim().size() > 0", + message = "The name must not be empty." + ) + private String name = ""; +} diff --git a/operator/src/main/java/it/aboutbits/postgresql/core/SecretRef.java b/operator/src/main/java/it/aboutbits/postgresql/core/SecretRef.java deleted file mode 100644 index 6f71b88..0000000 --- a/operator/src/main/java/it/aboutbits/postgresql/core/SecretRef.java +++ /dev/null @@ -1,30 +0,0 @@ -package it.aboutbits.postgresql.core; - -import io.fabric8.crdv2.generator.v1.SchemaCustomizer; -import io.fabric8.generator.annotation.Required; -import io.fabric8.generator.annotation.ValidationRule; -import lombok.Getter; -import lombok.Setter; -import org.jspecify.annotations.NullMarked; -import org.jspecify.annotations.Nullable; - -@NullMarked -@Getter -@Setter -@SchemaCustomizer(HostnameRFC1123Customizer.class) -public class SecretRef { - @Required - @ValidationRule( - value = "self.trim().size() > 0", - message = "The SecretRef name must not be empty." - ) - private String name = ""; - - /** - * The namespace where the Secret is located. - * If it is null, it means the Secret is in the same namespace as the resource referencing it. - */ - @Nullable - @io.fabric8.generator.annotation.Nullable - private String namespace; -} diff --git a/operator/src/main/java/it/aboutbits/postgresql/crd/clusterconnection/ClusterConnectionSpec.java b/operator/src/main/java/it/aboutbits/postgresql/crd/clusterconnection/ClusterConnectionSpec.java index 8e5fc11..25d85a1 100644 --- a/operator/src/main/java/it/aboutbits/postgresql/crd/clusterconnection/ClusterConnectionSpec.java +++ b/operator/src/main/java/it/aboutbits/postgresql/crd/clusterconnection/ClusterConnectionSpec.java @@ -5,8 +5,8 @@ import io.fabric8.generator.annotation.Min; import io.fabric8.generator.annotation.Required; import io.fabric8.generator.annotation.ValidationRule; -import it.aboutbits.postgresql.core.SecretRef; import it.aboutbits.postgresql.core.HostnameRFC1123Customizer; +import it.aboutbits.postgresql.core.ResourceRef; import lombok.Getter; import lombok.Setter; import org.jspecify.annotations.NullMarked; @@ -39,7 +39,7 @@ public class ClusterConnectionSpec { private String database = "postgres"; @Required - private SecretRef adminSecretRef = new SecretRef(); + private ResourceRef adminSecretRef = new ResourceRef(); @io.fabric8.generator.annotation.Nullable private Map parameters = new HashMap<>(); diff --git a/operator/src/main/java/it/aboutbits/postgresql/crd/database/DatabaseSpec.java b/operator/src/main/java/it/aboutbits/postgresql/crd/database/DatabaseSpec.java index 0eadc44..dd4837c 100644 --- a/operator/src/main/java/it/aboutbits/postgresql/crd/database/DatabaseSpec.java +++ b/operator/src/main/java/it/aboutbits/postgresql/crd/database/DatabaseSpec.java @@ -2,7 +2,7 @@ import io.fabric8.generator.annotation.Required; import io.fabric8.generator.annotation.ValidationRule; -import it.aboutbits.postgresql.core.ClusterReference; +import it.aboutbits.postgresql.core.ResourceRef; import it.aboutbits.postgresql.core.ReclaimPolicy; import lombok.Getter; import lombok.Setter; @@ -14,7 +14,7 @@ @Setter public class DatabaseSpec { @Required - private ClusterReference clusterRef = new ClusterReference(); + private ResourceRef clusterRef = new ResourceRef(); @Required @ValidationRule( diff --git a/operator/src/main/java/it/aboutbits/postgresql/crd/defaultprivilege/DefaultPrivilegeSpec.java b/operator/src/main/java/it/aboutbits/postgresql/crd/defaultprivilege/DefaultPrivilegeSpec.java index 51e8ef0..ecd2bb1 100644 --- a/operator/src/main/java/it/aboutbits/postgresql/crd/defaultprivilege/DefaultPrivilegeSpec.java +++ b/operator/src/main/java/it/aboutbits/postgresql/crd/defaultprivilege/DefaultPrivilegeSpec.java @@ -4,7 +4,7 @@ import com.fasterxml.jackson.annotation.JsonInclude; import io.fabric8.generator.annotation.Required; import io.fabric8.generator.annotation.ValidationRule; -import it.aboutbits.postgresql.core.ClusterReference; +import it.aboutbits.postgresql.core.ResourceRef; import it.aboutbits.postgresql.core.Privilege; import lombok.Getter; import lombok.Setter; @@ -23,7 +23,7 @@ ) public class DefaultPrivilegeSpec { @Required - private ClusterReference clusterRef = new ClusterReference(); + private ResourceRef clusterRef = new ResourceRef(); /// The database to grant default privileges on for this role. @Required diff --git a/operator/src/main/java/it/aboutbits/postgresql/crd/grant/GrantSpec.java b/operator/src/main/java/it/aboutbits/postgresql/crd/grant/GrantSpec.java index 62ccafe..dfdef7a 100644 --- a/operator/src/main/java/it/aboutbits/postgresql/crd/grant/GrantSpec.java +++ b/operator/src/main/java/it/aboutbits/postgresql/crd/grant/GrantSpec.java @@ -4,7 +4,7 @@ import com.fasterxml.jackson.annotation.JsonInclude; import io.fabric8.generator.annotation.Required; import io.fabric8.generator.annotation.ValidationRule; -import it.aboutbits.postgresql.core.ClusterReference; +import it.aboutbits.postgresql.core.ResourceRef; import it.aboutbits.postgresql.core.Privilege; import lombok.Getter; import lombok.Setter; @@ -27,7 +27,7 @@ ) public class GrantSpec { @Required - private ClusterReference clusterRef = new ClusterReference(); + private ResourceRef clusterRef = new ResourceRef(); /// The database to grant privileges on for this role. @Required diff --git a/operator/src/main/java/it/aboutbits/postgresql/crd/role/RoleSpec.java b/operator/src/main/java/it/aboutbits/postgresql/crd/role/RoleSpec.java index 1207cfe..55775db 100644 --- a/operator/src/main/java/it/aboutbits/postgresql/crd/role/RoleSpec.java +++ b/operator/src/main/java/it/aboutbits/postgresql/crd/role/RoleSpec.java @@ -2,8 +2,7 @@ import io.fabric8.generator.annotation.Required; import io.fabric8.generator.annotation.ValidationRule; -import it.aboutbits.postgresql.core.ClusterReference; -import it.aboutbits.postgresql.core.SecretRef; +import it.aboutbits.postgresql.core.ResourceRef; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.Setter; @@ -19,7 +18,7 @@ @Setter public class RoleSpec { @Required - private ClusterReference clusterRef = new ClusterReference(); + private ResourceRef clusterRef = new ResourceRef(); @Required @ValidationRule( @@ -38,7 +37,7 @@ public class RoleSpec { @Nullable @io.fabric8.generator.annotation.Nullable - private SecretRef passwordSecretRef; + private ResourceRef passwordSecretRef; @io.fabric8.generator.annotation.Nullable private Flags flags = new Flags(); diff --git a/operator/src/main/java/it/aboutbits/postgresql/crd/schema/SchemaSpec.java b/operator/src/main/java/it/aboutbits/postgresql/crd/schema/SchemaSpec.java index b9568a8..9264a77 100644 --- a/operator/src/main/java/it/aboutbits/postgresql/crd/schema/SchemaSpec.java +++ b/operator/src/main/java/it/aboutbits/postgresql/crd/schema/SchemaSpec.java @@ -2,7 +2,7 @@ import io.fabric8.generator.annotation.Required; import io.fabric8.generator.annotation.ValidationRule; -import it.aboutbits.postgresql.core.ClusterReference; +import it.aboutbits.postgresql.core.ResourceRef; import it.aboutbits.postgresql.core.ReclaimPolicy; import lombok.Getter; import lombok.Setter; @@ -14,7 +14,7 @@ @Setter public class SchemaSpec { @Required - private ClusterReference clusterRef = new ClusterReference(); + private ResourceRef clusterRef = new ResourceRef(); @Required @ValidationRule( diff --git a/operator/src/test/java/it/aboutbits/postgresql/_support/testdata/persisted/creator/ClusterConnectionCreate.java b/operator/src/test/java/it/aboutbits/postgresql/_support/testdata/persisted/creator/ClusterConnectionCreate.java index 169fd19..d1f1699 100644 --- a/operator/src/test/java/it/aboutbits/postgresql/_support/testdata/persisted/creator/ClusterConnectionCreate.java +++ b/operator/src/test/java/it/aboutbits/postgresql/_support/testdata/persisted/creator/ClusterConnectionCreate.java @@ -4,7 +4,7 @@ import io.fabric8.kubernetes.client.KubernetesClient; import it.aboutbits.postgresql._support.testdata.base.TestDataCreator; import it.aboutbits.postgresql._support.testdata.persisted.Given; -import it.aboutbits.postgresql.core.SecretRef; +import it.aboutbits.postgresql.core.ResourceRef; import it.aboutbits.postgresql.crd.clusterconnection.ClusterConnection; import it.aboutbits.postgresql.crd.clusterconnection.ClusterConnectionSpec; import lombok.AccessLevel; @@ -44,7 +44,7 @@ public class ClusterConnectionCreate extends TestDataCreator private String withDatabase; @Nullable - private SecretRef withAdminSecretRef; + private ResourceRef withAdminSecretRef; @Nullable private String withApplicationName; @@ -161,7 +161,7 @@ private String getDatabase() { return withDatabase; } - private SecretRef getAdminSecretRef() { + private ResourceRef getAdminSecretRef() { if (withAdminSecretRef != null) { return withAdminSecretRef; } diff --git a/operator/src/test/java/it/aboutbits/postgresql/_support/testdata/persisted/creator/DatabaseCreate.java b/operator/src/test/java/it/aboutbits/postgresql/_support/testdata/persisted/creator/DatabaseCreate.java index f4988ba..197735f 100644 --- a/operator/src/test/java/it/aboutbits/postgresql/_support/testdata/persisted/creator/DatabaseCreate.java +++ b/operator/src/test/java/it/aboutbits/postgresql/_support/testdata/persisted/creator/DatabaseCreate.java @@ -4,7 +4,7 @@ import io.fabric8.kubernetes.client.KubernetesClient; import it.aboutbits.postgresql._support.testdata.base.TestDataCreator; import it.aboutbits.postgresql._support.testdata.persisted.Given; -import it.aboutbits.postgresql.core.ClusterReference; +import it.aboutbits.postgresql.core.ResourceRef; import it.aboutbits.postgresql.core.ReclaimPolicy; import it.aboutbits.postgresql.crd.database.Database; import it.aboutbits.postgresql.crd.database.DatabaseSpec; @@ -72,7 +72,7 @@ protected Database create(int index) { .build() ); - var clusterRef = new ClusterReference(); + var clusterRef = new ResourceRef(); clusterRef.setName(getClusterConnectionName()); clusterRef.setNamespace(withClusterConnectionNamespace); diff --git a/operator/src/test/java/it/aboutbits/postgresql/_support/testdata/persisted/creator/DefaultPrivilegeCreate.java b/operator/src/test/java/it/aboutbits/postgresql/_support/testdata/persisted/creator/DefaultPrivilegeCreate.java index 50b1cf0..9e41482 100644 --- a/operator/src/test/java/it/aboutbits/postgresql/_support/testdata/persisted/creator/DefaultPrivilegeCreate.java +++ b/operator/src/test/java/it/aboutbits/postgresql/_support/testdata/persisted/creator/DefaultPrivilegeCreate.java @@ -4,7 +4,7 @@ import io.fabric8.kubernetes.client.KubernetesClient; import it.aboutbits.postgresql._support.testdata.base.TestDataCreator; import it.aboutbits.postgresql._support.testdata.persisted.Given; -import it.aboutbits.postgresql.core.ClusterReference; +import it.aboutbits.postgresql.core.ResourceRef; import it.aboutbits.postgresql.core.Privilege; import it.aboutbits.postgresql.crd.defaultprivilege.DefaultPrivilege; import it.aboutbits.postgresql.crd.defaultprivilege.DefaultPrivilegeObjectType; @@ -101,7 +101,7 @@ protected DefaultPrivilege create(int index) { .build() ); - var clusterRef = new ClusterReference(); + var clusterRef = new ResourceRef(); clusterRef.setName(getClusterConnectionName()); clusterRef.setNamespace(withClusterConnectionNamespace); diff --git a/operator/src/test/java/it/aboutbits/postgresql/_support/testdata/persisted/creator/GrantCreate.java b/operator/src/test/java/it/aboutbits/postgresql/_support/testdata/persisted/creator/GrantCreate.java index 4314969..7b38777 100644 --- a/operator/src/test/java/it/aboutbits/postgresql/_support/testdata/persisted/creator/GrantCreate.java +++ b/operator/src/test/java/it/aboutbits/postgresql/_support/testdata/persisted/creator/GrantCreate.java @@ -4,7 +4,7 @@ import io.fabric8.kubernetes.client.KubernetesClient; import it.aboutbits.postgresql._support.testdata.base.TestDataCreator; import it.aboutbits.postgresql._support.testdata.persisted.Given; -import it.aboutbits.postgresql.core.ClusterReference; +import it.aboutbits.postgresql.core.ResourceRef; import it.aboutbits.postgresql.core.Privilege; import it.aboutbits.postgresql.crd.grant.Grant; import it.aboutbits.postgresql.crd.grant.GrantObjectType; @@ -114,7 +114,7 @@ protected Grant create(int index) { .build() ); - var clusterRef = new ClusterReference(); + var clusterRef = new ResourceRef(); clusterRef.setName(getClusterConnectionName()); clusterRef.setNamespace(withClusterConnectionNamespace); diff --git a/operator/src/test/java/it/aboutbits/postgresql/_support/testdata/persisted/creator/RoleCreate.java b/operator/src/test/java/it/aboutbits/postgresql/_support/testdata/persisted/creator/RoleCreate.java index b0d48da..9d5ca0e 100644 --- a/operator/src/test/java/it/aboutbits/postgresql/_support/testdata/persisted/creator/RoleCreate.java +++ b/operator/src/test/java/it/aboutbits/postgresql/_support/testdata/persisted/creator/RoleCreate.java @@ -4,8 +4,7 @@ import io.fabric8.kubernetes.client.KubernetesClient; import it.aboutbits.postgresql._support.testdata.base.TestDataCreator; import it.aboutbits.postgresql._support.testdata.persisted.Given; -import it.aboutbits.postgresql.core.ClusterReference; -import it.aboutbits.postgresql.core.SecretRef; +import it.aboutbits.postgresql.core.ResourceRef; import it.aboutbits.postgresql.crd.role.Role; import it.aboutbits.postgresql.crd.role.RoleSpec; import lombok.AccessLevel; @@ -42,7 +41,7 @@ public class RoleCreate extends TestDataCreator { private String withClusterConnectionNamespace; @Nullable - private SecretRef withPasswordSecretRef; + private ResourceRef withPasswordSecretRef; private RoleSpec.@Nullable Flags withFlags; @@ -93,7 +92,7 @@ protected Role create(int index) { .build() ); - var clusterRef = new ClusterReference(); + var clusterRef = new ResourceRef(); clusterRef.setName(getClusterConnectionName()); clusterRef.setNamespace(withClusterConnectionNamespace); diff --git a/operator/src/test/java/it/aboutbits/postgresql/_support/testdata/persisted/creator/SchemaCreate.java b/operator/src/test/java/it/aboutbits/postgresql/_support/testdata/persisted/creator/SchemaCreate.java index 9136b9c..8854f53 100644 --- a/operator/src/test/java/it/aboutbits/postgresql/_support/testdata/persisted/creator/SchemaCreate.java +++ b/operator/src/test/java/it/aboutbits/postgresql/_support/testdata/persisted/creator/SchemaCreate.java @@ -4,7 +4,7 @@ import io.fabric8.kubernetes.client.KubernetesClient; import it.aboutbits.postgresql._support.testdata.base.TestDataCreator; import it.aboutbits.postgresql._support.testdata.persisted.Given; -import it.aboutbits.postgresql.core.ClusterReference; +import it.aboutbits.postgresql.core.ResourceRef; import it.aboutbits.postgresql.core.ReclaimPolicy; import it.aboutbits.postgresql.crd.schema.Schema; import it.aboutbits.postgresql.crd.schema.SchemaSpec; @@ -80,7 +80,7 @@ protected Schema create(int index) { // We have to create the database first which also modifies the specified withClusterConnectionName so the connection points to the newly created DB var database = getDatabase(); - var clusterRef = new ClusterReference(); + var clusterRef = new ResourceRef(); clusterRef.setName(getClusterConnectionName()); clusterRef.setNamespace(withClusterConnectionNamespace); diff --git a/operator/src/test/java/it/aboutbits/postgresql/_support/testdata/persisted/creator/SecretRefCreate.java b/operator/src/test/java/it/aboutbits/postgresql/_support/testdata/persisted/creator/SecretRefCreate.java index 11ccb49..d085751 100644 --- a/operator/src/test/java/it/aboutbits/postgresql/_support/testdata/persisted/creator/SecretRefCreate.java +++ b/operator/src/test/java/it/aboutbits/postgresql/_support/testdata/persisted/creator/SecretRefCreate.java @@ -3,7 +3,7 @@ import io.fabric8.kubernetes.api.model.SecretBuilder; import io.fabric8.kubernetes.client.KubernetesClient; import it.aboutbits.postgresql._support.testdata.base.TestDataCreator; -import it.aboutbits.postgresql.core.SecretRef; +import it.aboutbits.postgresql.core.ResourceRef; import lombok.AccessLevel; import lombok.Setter; import lombok.experimental.Accessors; @@ -17,7 +17,7 @@ @NullMarked @Setter @Accessors(fluent = true, chain = true) -public class SecretRefCreate extends TestDataCreator { +public class SecretRefCreate extends TestDataCreator { private final KubernetesClient kubernetesClient; @Nullable @@ -65,7 +65,7 @@ public SecretRefCreate withoutPassword() { } @Override - protected SecretRef create(int index) { + protected ResourceRef create(int index) { var namespace = getNamespace(); var name = getName(); @@ -84,7 +84,7 @@ protected SecretRef create(int index) { .resource(secret) .serverSideApply(); - var secretRef = new SecretRef(); + var secretRef = new ResourceRef(); secretRef.setName(name); secretRef.setNamespace(namespace); diff --git a/operator/src/test/java/it/aboutbits/postgresql/crd/role/RoleReconcilerTest.java b/operator/src/test/java/it/aboutbits/postgresql/crd/role/RoleReconcilerTest.java index 41df236..16b312b 100644 --- a/operator/src/test/java/it/aboutbits/postgresql/crd/role/RoleReconcilerTest.java +++ b/operator/src/test/java/it/aboutbits/postgresql/crd/role/RoleReconcilerTest.java @@ -9,7 +9,7 @@ import it.aboutbits.postgresql.core.CRStatus; import it.aboutbits.postgresql.core.PostgreSQLAuthenticationService; import it.aboutbits.postgresql.core.PostgreSQLContextFactory; -import it.aboutbits.postgresql.core.SecretRef; +import it.aboutbits.postgresql.core.ResourceRef; import lombok.RequiredArgsConstructor; import org.jooq.DSLContext; import org.jooq.Field; @@ -203,7 +203,7 @@ void createRole_withMissingClusterConnection_setsPending() { var now = OffsetDateTime.now(ZoneOffset.UTC); - var dummySecretRef = new SecretRef(); + var dummySecretRef = new ResourceRef(); dummySecretRef.setName("dummy"); // when From 638359c428fe9105a7f6e820a522b08c5c80d869 Mon Sep 17 00:00:00 2001 From: Thomas Sapelza Date: Thu, 26 Feb 2026 09:32:06 +0100 Subject: [PATCH 04/10] fix the test by having a own SchemaCustomizer for Kubernetes names --- .../postgresql/core/ClusterReference.java | 3 +- .../aboutbits/postgresql/core/SecretRef.java | 3 +- .../HostCustomizer.java} | 49 ++++++---- .../KubernetesNameCustomizer.java | 90 +++++++++++++++++++ .../ClusterConnectionSpec.java | 4 +- .../PostgreSQLInstanceReadinessCheckTest.java | 36 +++++++- 6 files changed, 161 insertions(+), 24 deletions(-) rename operator/src/main/java/it/aboutbits/postgresql/core/{HostnameRFC1123Customizer.java => schema_customizer/HostCustomizer.java} (55%) create mode 100644 operator/src/main/java/it/aboutbits/postgresql/core/schema_customizer/KubernetesNameCustomizer.java diff --git a/operator/src/main/java/it/aboutbits/postgresql/core/ClusterReference.java b/operator/src/main/java/it/aboutbits/postgresql/core/ClusterReference.java index 237d9de..b5aaa24 100644 --- a/operator/src/main/java/it/aboutbits/postgresql/core/ClusterReference.java +++ b/operator/src/main/java/it/aboutbits/postgresql/core/ClusterReference.java @@ -3,6 +3,7 @@ import io.fabric8.crdv2.generator.v1.SchemaCustomizer; import io.fabric8.generator.annotation.Required; import io.fabric8.generator.annotation.ValidationRule; +import it.aboutbits.postgresql.core.schema_customizer.KubernetesNameCustomizer; import lombok.Getter; import lombok.Setter; import org.jspecify.annotations.NullMarked; @@ -11,7 +12,7 @@ @NullMarked @Getter @Setter -@SchemaCustomizer(HostnameRFC1123Customizer.class) +@SchemaCustomizer(KubernetesNameCustomizer.class) public class ClusterReference { @Required @ValidationRule( diff --git a/operator/src/main/java/it/aboutbits/postgresql/core/SecretRef.java b/operator/src/main/java/it/aboutbits/postgresql/core/SecretRef.java index 6f71b88..8c3bacf 100644 --- a/operator/src/main/java/it/aboutbits/postgresql/core/SecretRef.java +++ b/operator/src/main/java/it/aboutbits/postgresql/core/SecretRef.java @@ -3,6 +3,7 @@ import io.fabric8.crdv2.generator.v1.SchemaCustomizer; import io.fabric8.generator.annotation.Required; import io.fabric8.generator.annotation.ValidationRule; +import it.aboutbits.postgresql.core.schema_customizer.KubernetesNameCustomizer; import lombok.Getter; import lombok.Setter; import org.jspecify.annotations.NullMarked; @@ -11,7 +12,7 @@ @NullMarked @Getter @Setter -@SchemaCustomizer(HostnameRFC1123Customizer.class) +@SchemaCustomizer(KubernetesNameCustomizer.class) public class SecretRef { @Required @ValidationRule( diff --git a/operator/src/main/java/it/aboutbits/postgresql/core/HostnameRFC1123Customizer.java b/operator/src/main/java/it/aboutbits/postgresql/core/schema_customizer/HostCustomizer.java similarity index 55% rename from operator/src/main/java/it/aboutbits/postgresql/core/HostnameRFC1123Customizer.java rename to operator/src/main/java/it/aboutbits/postgresql/core/schema_customizer/HostCustomizer.java index 6ec967b..64d5717 100644 --- a/operator/src/main/java/it/aboutbits/postgresql/core/HostnameRFC1123Customizer.java +++ b/operator/src/main/java/it/aboutbits/postgresql/core/schema_customizer/HostCustomizer.java @@ -1,4 +1,4 @@ -package it.aboutbits.postgresql.core; +package it.aboutbits.postgresql.core.schema_customizer; import io.fabric8.crdv2.generator.v1.SchemaCustomizer; import io.fabric8.kubernetes.api.model.apiextensions.v1.JSONSchemaProps; @@ -6,15 +6,17 @@ import org.jspecify.annotations.NullMarked; import java.util.Arrays; +import java.util.List; import java.util.Set; import java.util.stream.Collectors; /// A [SchemaCustomizer.Customizer] that sets the `format` of string properties -/// to `"hostname"` (RFC 1123) in the generated CRD JSON Schema. +/// to `{"anyOf":[{"format":"hostname"},{"format":"ipv4"},{"format":"ipv6"}]}` +/// in the generated CRD JSON Schema. /// /// This customizer is intended to be used with the /// [@SchemaCustomizer][SchemaCustomizer] annotation on a class whose properties -/// should be validated as RFC 1123 hostnames by the Kubernetes API server. +/// should be validated to valid hosts defined. /// /// ### Behavior /// @@ -28,28 +30,28 @@ /// **Apply to all string properties:** /// /// ```java -/// @SchemaCustomizer(HostnameRFC1123Customizer.class) -/// public class SecretRef { -/// private String name = ""; // gets format: "hostname" -/// private String namespace; // gets format: "hostname" +/// @SchemaCustomizer(value = HostCustomizer.class) +/// public class ClusterConnectionSpec { +/// private String host = ""; // gets custom format +/// private String anotherHost = ""; // gets custom format /// } /// ``` /// /// **Apply to specific properties only:** /// /// ```java -/// @SchemaCustomizer(value = HostnameRFC1123Customizer.class, input = "host") +/// @SchemaCustomizer(value = HostCustomizer.class, input = "host,anotherHost") /// public class ClusterConnectionSpec { -/// private String host = ""; // gets format: "hostname" -/// private String anotherHost = ""; // gets format: "hostname" -/// private String database = ""; // unchanged +/// private String host = ""; // gets custom format +/// private String anotherHost = ""; // gets custom format +/// private String unchangedHost = ""; // unchanged /// } /// ``` /// /// @see SchemaCustomizer /// @see SchemaCustomizer.Customizer @NullMarked -public class HostnameRFC1123Customizer implements SchemaCustomizer.Customizer { +public class HostCustomizer implements SchemaCustomizer.Customizer { @Override public JSONSchemaProps apply( JSONSchemaProps jsonSchemaProps, @@ -69,10 +71,25 @@ public JSONSchemaProps apply( for (var entry : properties.entrySet()) { var prop = entry.getValue(); - if ("string".equals(prop.getType())) { - if (targetFields.isEmpty() || targetFields.contains(entry.getKey())) { - prop.setFormat("hostname"); - } + if ("string".equals(prop.getType()) + && (targetFields.isEmpty() || targetFields.contains(entry.getKey())) + ) { + prop.setFormat(null); + + var hostnameProp = new JSONSchemaProps(); + hostnameProp.setFormat("hostname"); + + var ipv4Prop = new JSONSchemaProps(); + ipv4Prop.setFormat("ipv4"); + + var ipv6Prop = new JSONSchemaProps(); + ipv6Prop.setFormat("ipv6"); + + prop.setAnyOf(List.of( + hostnameProp, + ipv4Prop, + ipv6Prop + )); } } diff --git a/operator/src/main/java/it/aboutbits/postgresql/core/schema_customizer/KubernetesNameCustomizer.java b/operator/src/main/java/it/aboutbits/postgresql/core/schema_customizer/KubernetesNameCustomizer.java new file mode 100644 index 0000000..baf3f5d --- /dev/null +++ b/operator/src/main/java/it/aboutbits/postgresql/core/schema_customizer/KubernetesNameCustomizer.java @@ -0,0 +1,90 @@ +package it.aboutbits.postgresql.core.schema_customizer; + +import io.fabric8.crdv2.generator.v1.SchemaCustomizer; +import io.fabric8.kubernetes.api.model.apiextensions.v1.JSONSchemaProps; +import io.fabric8.kubernetes.client.utils.KubernetesSerialization; +import org.jspecify.annotations.NullMarked; + +import java.util.Arrays; +import java.util.Set; +import java.util.stream.Collectors; + +/// A [SchemaCustomizer.Customizer] that adds a Kubernetes name validation +/// `pattern` (RFC 1123 DNS label) to string properties in the generated CRD +/// JSON Schema. +/// +/// The pattern enforces: +/// - Contain at most 63 characters +/// - Contain only lowercase alphanumeric characters or '-' +/// - Start with an alphabetic character +/// - End with an alphanumeric character +/// +/// This customizer is intended to be used with the +/// [@SchemaCustomizer][SchemaCustomizer] annotation on a class whose string +/// properties represent Kubernetes resource names. +/// +/// ### Behavior +/// +/// - If `input` is **blank** (the default), the `"hostname"` format +/// is applied to **all** string properties of the annotated class. +/// - If `input` contains a **comma-separated list** of field names, +/// the format is applied **only** to the specified properties. +/// +/// ### Usage examples +/// +/// **Apply to all string properties:** +/// +/// ```java +/// @SchemaCustomizer(KubernetesNameCustomizer.class) +/// public class SecretRef { +/// private String name = ""; // gets pattern: Kubernetes name regex +/// private String namespace; // gets pattern: Kubernetes name regex +/// } +/// ``` +/// +/// **Apply to specific properties only:** +/// +/// ```java +/// @SchemaCustomizer(value = KubernetesNameCustomizer.class, input = "name,anotherName") +/// public class SecretRef { +/// private String name = ""; // gets pattern: Kubernetes name regex +/// private String anotherName = ""; // gets pattern: Kubernetes name regex +/// private String namespace; // unchanged +/// } +/// ``` +/// +/// @see SchemaCustomizer +/// @see SchemaCustomizer.Customizer +@NullMarked +public class KubernetesNameCustomizer implements SchemaCustomizer.Customizer { + static final String KUBERNETES_NAME_PATTERN = "^[a-z]([a-z0-9\\-]{0,61}[a-z0-9])?$"; + + @Override + public JSONSchemaProps apply( + JSONSchemaProps jsonSchemaProps, + String input, + KubernetesSerialization kubernetesSerialization + ) { + var properties = jsonSchemaProps.getProperties(); + if (properties == null) { + return jsonSchemaProps; + } + + var targetFields = input.isBlank() + ? Set.of() + : Arrays.stream(input.split(",")) + .map(String::trim) + .collect(Collectors.toSet()); + + for (var entry : properties.entrySet()) { + var prop = entry.getValue(); + if ("string".equals(prop.getType()) + && (targetFields.isEmpty() || targetFields.contains(entry.getKey())) + ) { + prop.setPattern(KUBERNETES_NAME_PATTERN); + } + } + + return jsonSchemaProps; + } +} diff --git a/operator/src/main/java/it/aboutbits/postgresql/crd/clusterconnection/ClusterConnectionSpec.java b/operator/src/main/java/it/aboutbits/postgresql/crd/clusterconnection/ClusterConnectionSpec.java index 8e5fc11..b4c472c 100644 --- a/operator/src/main/java/it/aboutbits/postgresql/crd/clusterconnection/ClusterConnectionSpec.java +++ b/operator/src/main/java/it/aboutbits/postgresql/crd/clusterconnection/ClusterConnectionSpec.java @@ -6,7 +6,7 @@ import io.fabric8.generator.annotation.Required; import io.fabric8.generator.annotation.ValidationRule; import it.aboutbits.postgresql.core.SecretRef; -import it.aboutbits.postgresql.core.HostnameRFC1123Customizer; +import it.aboutbits.postgresql.core.schema_customizer.HostCustomizer; import lombok.Getter; import lombok.Setter; import org.jspecify.annotations.NullMarked; @@ -17,7 +17,7 @@ @NullMarked @Getter @Setter -@SchemaCustomizer(value = HostnameRFC1123Customizer.class, input = "host") +@SchemaCustomizer(value = HostCustomizer.class, input = "host") public class ClusterConnectionSpec { @Required @ValidationRule( diff --git a/operator/src/test/java/it/aboutbits/postgresql/PostgreSQLInstanceReadinessCheckTest.java b/operator/src/test/java/it/aboutbits/postgresql/PostgreSQLInstanceReadinessCheckTest.java index 548a2e0..d2b7464 100644 --- a/operator/src/test/java/it/aboutbits/postgresql/PostgreSQLInstanceReadinessCheckTest.java +++ b/operator/src/test/java/it/aboutbits/postgresql/PostgreSQLInstanceReadinessCheckTest.java @@ -11,6 +11,7 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import java.util.Map; import java.util.Objects; import static org.assertj.core.api.Assertions.assertThat; @@ -67,13 +68,35 @@ void call_whenSomeConnectionsDown_shouldReturnDown() { given.one() .clusterConnection() .withName("db-1") - .returnFirst(); + .apply(); given.one() .clusterConnection() .withName("db-2") - .withHost("non-existent-host") - .returnFirst(); + .withHost("localhost") + .withPort(2345) // Wrong port + .apply(); + + given.one() + .clusterConnection() + .withName("db-3") + .withHost("127.0.0.1") + .withPort(2345) // Wrong port + .apply(); + + given.one() + .clusterConnection() + .withName("db-4") + .withHost("::1") + .withPort(2345) // Wrong port + .apply(); + + given.one() + .clusterConnection() + .withName("db-5") + .withHost("0:0:0:0:0:0:0:1") + .withPort(2345) // Wrong port + .apply(); var response = readinessCheck.call(); @@ -93,7 +116,12 @@ void call_whenSomeConnectionsDown_shouldReturnDown() { assertThat(dbStatus.toString()).startsWith("UP (PostgreSQL"); - assertThat(data).containsEntry("db-2", "DOWN"); + assertThat(data).containsAllEntriesOf(Map.of( + "db-2", "DOWN", + "db-3", "DOWN", + "db-4", "DOWN", + "db-5", "DOWN" + )); }); } } From acddd5ab593e87552ffb428fa2fb5f04c17acfb5 Mon Sep 17 00:00:00 2001 From: Thomas Sapelza Date: Thu, 26 Feb 2026 09:42:42 +0100 Subject: [PATCH 05/10] do not short-circuit the PostgreSQLInstanceReadinessCheck check once one instance is down --- .../postgresql/PostgreSQLInstanceReadinessCheck.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/operator/src/main/java/it/aboutbits/postgresql/PostgreSQLInstanceReadinessCheck.java b/operator/src/main/java/it/aboutbits/postgresql/PostgreSQLInstanceReadinessCheck.java index 20f16db..3b86277 100644 --- a/operator/src/main/java/it/aboutbits/postgresql/PostgreSQLInstanceReadinessCheck.java +++ b/operator/src/main/java/it/aboutbits/postgresql/PostgreSQLInstanceReadinessCheck.java @@ -30,10 +30,11 @@ public HealthCheckResponse call() { var connections = kubernetesClient.resources(ClusterConnection.class).list().getItems(); boolean allUp = connections.stream() - .allMatch(connection -> checkInstance( + .map(connection -> checkInstance( connection, builder - )); + )) + .reduce(true, Boolean::logicalAnd); return builder.status(allUp).build(); } From a0f9e2bb4aa80a1c00ff3142326bbf93cccd4cbe Mon Sep 17 00:00:00 2001 From: Thomas Sapelza Date: Thu, 26 Feb 2026 09:42:53 +0100 Subject: [PATCH 06/10] let the PostgreSQLContextFactory exception bubble up --- .../aboutbits/postgresql/core/PostgreSQLContextFactory.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/operator/src/main/java/it/aboutbits/postgresql/core/PostgreSQLContextFactory.java b/operator/src/main/java/it/aboutbits/postgresql/core/PostgreSQLContextFactory.java index fdb5bb8..216a4fe 100644 --- a/operator/src/main/java/it/aboutbits/postgresql/core/PostgreSQLContextFactory.java +++ b/operator/src/main/java/it/aboutbits/postgresql/core/PostgreSQLContextFactory.java @@ -5,6 +5,7 @@ import jakarta.enterprise.context.ApplicationScoped; import lombok.RequiredArgsConstructor; import org.jooq.CloseableDSLContext; +import org.jooq.exception.DataAccessException; import org.jooq.impl.DSL; import org.jspecify.annotations.NullMarked; @@ -21,7 +22,7 @@ public class PostgreSQLContextFactory { private final KubernetesClient kubernetesClient; /// Create a DSLContext with a JDBC connection to the PostgreSQL maintenance database. - public CloseableDSLContext getDSLContext(ClusterConnection clusterConnection) { + public CloseableDSLContext getDSLContext(ClusterConnection clusterConnection) throws DataAccessException { return getDSLContext( clusterConnection, clusterConnection.getSpec().getDatabase() @@ -32,7 +33,7 @@ public CloseableDSLContext getDSLContext(ClusterConnection clusterConnection) { public CloseableDSLContext getDSLContext( ClusterConnection clusterConnection, String database - ) { + ) throws DataAccessException { var credentials = kubernetesService.getSecretRefCredentials( kubernetesClient, clusterConnection From c95e06f67411f80918eaeb599ec4b13558ca34a4 Mon Sep 17 00:00:00 2001 From: Thomas Sapelza Date: Thu, 26 Feb 2026 09:49:05 +0100 Subject: [PATCH 07/10] reformat code --- .../java/it/aboutbits/postgresql/crd/database/DatabaseSpec.java | 2 +- .../postgresql/crd/defaultprivilege/DefaultPrivilegeSpec.java | 2 +- .../main/java/it/aboutbits/postgresql/crd/grant/GrantSpec.java | 2 +- .../java/it/aboutbits/postgresql/crd/schema/SchemaSpec.java | 2 +- .../_support/testdata/persisted/creator/DatabaseCreate.java | 2 +- .../testdata/persisted/creator/DefaultPrivilegeCreate.java | 2 +- .../_support/testdata/persisted/creator/GrantCreate.java | 2 +- .../_support/testdata/persisted/creator/SchemaCreate.java | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) diff --git a/operator/src/main/java/it/aboutbits/postgresql/crd/database/DatabaseSpec.java b/operator/src/main/java/it/aboutbits/postgresql/crd/database/DatabaseSpec.java index dd4837c..168d1c7 100644 --- a/operator/src/main/java/it/aboutbits/postgresql/crd/database/DatabaseSpec.java +++ b/operator/src/main/java/it/aboutbits/postgresql/crd/database/DatabaseSpec.java @@ -2,8 +2,8 @@ import io.fabric8.generator.annotation.Required; import io.fabric8.generator.annotation.ValidationRule; -import it.aboutbits.postgresql.core.ResourceRef; import it.aboutbits.postgresql.core.ReclaimPolicy; +import it.aboutbits.postgresql.core.ResourceRef; import lombok.Getter; import lombok.Setter; import org.jspecify.annotations.NullMarked; diff --git a/operator/src/main/java/it/aboutbits/postgresql/crd/defaultprivilege/DefaultPrivilegeSpec.java b/operator/src/main/java/it/aboutbits/postgresql/crd/defaultprivilege/DefaultPrivilegeSpec.java index ecd2bb1..a29afa0 100644 --- a/operator/src/main/java/it/aboutbits/postgresql/crd/defaultprivilege/DefaultPrivilegeSpec.java +++ b/operator/src/main/java/it/aboutbits/postgresql/crd/defaultprivilege/DefaultPrivilegeSpec.java @@ -4,8 +4,8 @@ import com.fasterxml.jackson.annotation.JsonInclude; import io.fabric8.generator.annotation.Required; import io.fabric8.generator.annotation.ValidationRule; -import it.aboutbits.postgresql.core.ResourceRef; import it.aboutbits.postgresql.core.Privilege; +import it.aboutbits.postgresql.core.ResourceRef; import lombok.Getter; import lombok.Setter; import org.jspecify.annotations.NullMarked; diff --git a/operator/src/main/java/it/aboutbits/postgresql/crd/grant/GrantSpec.java b/operator/src/main/java/it/aboutbits/postgresql/crd/grant/GrantSpec.java index dfdef7a..09b3568 100644 --- a/operator/src/main/java/it/aboutbits/postgresql/crd/grant/GrantSpec.java +++ b/operator/src/main/java/it/aboutbits/postgresql/crd/grant/GrantSpec.java @@ -4,8 +4,8 @@ import com.fasterxml.jackson.annotation.JsonInclude; import io.fabric8.generator.annotation.Required; import io.fabric8.generator.annotation.ValidationRule; -import it.aboutbits.postgresql.core.ResourceRef; import it.aboutbits.postgresql.core.Privilege; +import it.aboutbits.postgresql.core.ResourceRef; import lombok.Getter; import lombok.Setter; import org.jspecify.annotations.NullMarked; diff --git a/operator/src/main/java/it/aboutbits/postgresql/crd/schema/SchemaSpec.java b/operator/src/main/java/it/aboutbits/postgresql/crd/schema/SchemaSpec.java index 9264a77..6843400 100644 --- a/operator/src/main/java/it/aboutbits/postgresql/crd/schema/SchemaSpec.java +++ b/operator/src/main/java/it/aboutbits/postgresql/crd/schema/SchemaSpec.java @@ -2,8 +2,8 @@ import io.fabric8.generator.annotation.Required; import io.fabric8.generator.annotation.ValidationRule; -import it.aboutbits.postgresql.core.ResourceRef; import it.aboutbits.postgresql.core.ReclaimPolicy; +import it.aboutbits.postgresql.core.ResourceRef; import lombok.Getter; import lombok.Setter; import org.jspecify.annotations.NullMarked; diff --git a/operator/src/test/java/it/aboutbits/postgresql/_support/testdata/persisted/creator/DatabaseCreate.java b/operator/src/test/java/it/aboutbits/postgresql/_support/testdata/persisted/creator/DatabaseCreate.java index 197735f..9125706 100644 --- a/operator/src/test/java/it/aboutbits/postgresql/_support/testdata/persisted/creator/DatabaseCreate.java +++ b/operator/src/test/java/it/aboutbits/postgresql/_support/testdata/persisted/creator/DatabaseCreate.java @@ -4,8 +4,8 @@ import io.fabric8.kubernetes.client.KubernetesClient; import it.aboutbits.postgresql._support.testdata.base.TestDataCreator; import it.aboutbits.postgresql._support.testdata.persisted.Given; -import it.aboutbits.postgresql.core.ResourceRef; import it.aboutbits.postgresql.core.ReclaimPolicy; +import it.aboutbits.postgresql.core.ResourceRef; import it.aboutbits.postgresql.crd.database.Database; import it.aboutbits.postgresql.crd.database.DatabaseSpec; import lombok.AccessLevel; diff --git a/operator/src/test/java/it/aboutbits/postgresql/_support/testdata/persisted/creator/DefaultPrivilegeCreate.java b/operator/src/test/java/it/aboutbits/postgresql/_support/testdata/persisted/creator/DefaultPrivilegeCreate.java index 9e41482..ec88f17 100644 --- a/operator/src/test/java/it/aboutbits/postgresql/_support/testdata/persisted/creator/DefaultPrivilegeCreate.java +++ b/operator/src/test/java/it/aboutbits/postgresql/_support/testdata/persisted/creator/DefaultPrivilegeCreate.java @@ -4,8 +4,8 @@ import io.fabric8.kubernetes.client.KubernetesClient; import it.aboutbits.postgresql._support.testdata.base.TestDataCreator; import it.aboutbits.postgresql._support.testdata.persisted.Given; -import it.aboutbits.postgresql.core.ResourceRef; import it.aboutbits.postgresql.core.Privilege; +import it.aboutbits.postgresql.core.ResourceRef; import it.aboutbits.postgresql.crd.defaultprivilege.DefaultPrivilege; import it.aboutbits.postgresql.crd.defaultprivilege.DefaultPrivilegeObjectType; import it.aboutbits.postgresql.crd.defaultprivilege.DefaultPrivilegeSpec; diff --git a/operator/src/test/java/it/aboutbits/postgresql/_support/testdata/persisted/creator/GrantCreate.java b/operator/src/test/java/it/aboutbits/postgresql/_support/testdata/persisted/creator/GrantCreate.java index 7b38777..0dd78f9 100644 --- a/operator/src/test/java/it/aboutbits/postgresql/_support/testdata/persisted/creator/GrantCreate.java +++ b/operator/src/test/java/it/aboutbits/postgresql/_support/testdata/persisted/creator/GrantCreate.java @@ -4,8 +4,8 @@ import io.fabric8.kubernetes.client.KubernetesClient; import it.aboutbits.postgresql._support.testdata.base.TestDataCreator; import it.aboutbits.postgresql._support.testdata.persisted.Given; -import it.aboutbits.postgresql.core.ResourceRef; import it.aboutbits.postgresql.core.Privilege; +import it.aboutbits.postgresql.core.ResourceRef; import it.aboutbits.postgresql.crd.grant.Grant; import it.aboutbits.postgresql.crd.grant.GrantObjectType; import it.aboutbits.postgresql.crd.grant.GrantSpec; diff --git a/operator/src/test/java/it/aboutbits/postgresql/_support/testdata/persisted/creator/SchemaCreate.java b/operator/src/test/java/it/aboutbits/postgresql/_support/testdata/persisted/creator/SchemaCreate.java index 8854f53..14ac58f 100644 --- a/operator/src/test/java/it/aboutbits/postgresql/_support/testdata/persisted/creator/SchemaCreate.java +++ b/operator/src/test/java/it/aboutbits/postgresql/_support/testdata/persisted/creator/SchemaCreate.java @@ -4,8 +4,8 @@ import io.fabric8.kubernetes.client.KubernetesClient; import it.aboutbits.postgresql._support.testdata.base.TestDataCreator; import it.aboutbits.postgresql._support.testdata.persisted.Given; -import it.aboutbits.postgresql.core.ResourceRef; import it.aboutbits.postgresql.core.ReclaimPolicy; +import it.aboutbits.postgresql.core.ResourceRef; import it.aboutbits.postgresql.crd.schema.Schema; import it.aboutbits.postgresql.crd.schema.SchemaSpec; import lombok.AccessLevel; From 4f0adccaab3331a29093400562504367c308a7d9 Mon Sep 17 00:00:00 2001 From: Thomas Sapelza Date: Thu, 26 Feb 2026 10:16:06 +0100 Subject: [PATCH 08/10] fix compile issue --- .../main/java/it/aboutbits/postgresql/core/ResourceRef.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/operator/src/main/java/it/aboutbits/postgresql/core/ResourceRef.java b/operator/src/main/java/it/aboutbits/postgresql/core/ResourceRef.java index 11f28f5..bdcbc16 100644 --- a/operator/src/main/java/it/aboutbits/postgresql/core/ResourceRef.java +++ b/operator/src/main/java/it/aboutbits/postgresql/core/ResourceRef.java @@ -3,6 +3,7 @@ import io.fabric8.crdv2.generator.v1.SchemaCustomizer; import io.fabric8.generator.annotation.Required; import io.fabric8.generator.annotation.ValidationRule; +import it.aboutbits.postgresql.core.schema_customizer.KubernetesNameCustomizer; import lombok.Getter; import lombok.Setter; import org.jspecify.annotations.NullMarked; @@ -27,7 +28,7 @@ @NullMarked @Getter @Setter -@SchemaCustomizer(HostnameRFC1123Customizer.class) +@SchemaCustomizer(KubernetesNameCustomizer.class) public class ResourceRef { /// The namespace of the referenced Kubernetes resource. /// If `null`, defaults to the namespace of the CR that defines this reference. From f41c60ceb599d3f82999850f78d7d94e849c4eec Mon Sep 17 00:00:00 2001 From: Thomas Sapelza Date: Thu, 26 Feb 2026 10:43:29 +0100 Subject: [PATCH 09/10] the namespace should come always first --- .../crd/clusterconnection/ClusterConnectionReconciler.java | 2 +- .../postgresql/crd/database/DatabaseReconciler.java | 6 +++--- .../crd/defaultprivilege/DefaultPrivilegeReconciler.java | 6 +++--- .../it/aboutbits/postgresql/crd/grant/GrantReconciler.java | 6 +++--- .../it/aboutbits/postgresql/crd/role/RoleReconciler.java | 6 +++--- .../aboutbits/postgresql/crd/schema/SchemaReconciler.java | 6 +++--- 6 files changed, 16 insertions(+), 16 deletions(-) diff --git a/operator/src/main/java/it/aboutbits/postgresql/crd/clusterconnection/ClusterConnectionReconciler.java b/operator/src/main/java/it/aboutbits/postgresql/crd/clusterconnection/ClusterConnectionReconciler.java index 56430f0..a5aca3a 100644 --- a/operator/src/main/java/it/aboutbits/postgresql/crd/clusterconnection/ClusterConnectionReconciler.java +++ b/operator/src/main/java/it/aboutbits/postgresql/crd/clusterconnection/ClusterConnectionReconciler.java @@ -26,8 +26,8 @@ public UpdateControl reconcile( ) { var status = initializeStatus(resource); - var name = resource.getMetadata().getName(); var namespace = resource.getMetadata().getNamespace(); + var name = resource.getMetadata().getName(); log.info( "Reconciling ClusterConnection [resource={}/{}, status.phase={}]", diff --git a/operator/src/main/java/it/aboutbits/postgresql/crd/database/DatabaseReconciler.java b/operator/src/main/java/it/aboutbits/postgresql/crd/database/DatabaseReconciler.java index 8e96de0..111af70 100644 --- a/operator/src/main/java/it/aboutbits/postgresql/crd/database/DatabaseReconciler.java +++ b/operator/src/main/java/it/aboutbits/postgresql/crd/database/DatabaseReconciler.java @@ -38,8 +38,8 @@ public UpdateControl reconcile( var spec = resource.getSpec(); var status = initializeStatus(resource); - var name = resource.getMetadata().getName(); var namespace = resource.getMetadata().getNamespace(); + var name = resource.getMetadata().getName(); log.info( "Reconciling Database [resource={}/{}, status.phase={}]", @@ -97,8 +97,8 @@ public DeleteControl cleanup( var spec = resource.getSpec(); var status = initializeStatus(resource); - var name = resource.getMetadata().getName(); var namespace = resource.getMetadata().getNamespace(); + var name = resource.getMetadata().getName(); log.info( "{}ing Database [resource={}/{}, spec.name={}, status.phase={}]", @@ -183,8 +183,8 @@ private UpdateControl reconcile( Database resource, CRStatus status ) { - var name = resource.getMetadata().getName(); var namespace = resource.getMetadata().getNamespace(); + var name = resource.getMetadata().getName(); var spec = resource.getSpec(); diff --git a/operator/src/main/java/it/aboutbits/postgresql/crd/defaultprivilege/DefaultPrivilegeReconciler.java b/operator/src/main/java/it/aboutbits/postgresql/crd/defaultprivilege/DefaultPrivilegeReconciler.java index 6b2c315..038c131 100644 --- a/operator/src/main/java/it/aboutbits/postgresql/crd/defaultprivilege/DefaultPrivilegeReconciler.java +++ b/operator/src/main/java/it/aboutbits/postgresql/crd/defaultprivilege/DefaultPrivilegeReconciler.java @@ -41,8 +41,8 @@ public UpdateControl reconcile( var spec = resource.getSpec(); var status = initializeStatus(resource); - var name = resource.getMetadata().getName(); var namespace = resource.getMetadata().getNamespace(); + var name = resource.getMetadata().getName(); log.info( "Reconciling DefaultPrivilege [resource={}/{}, status.phase={}]", @@ -124,8 +124,8 @@ public DeleteControl cleanup( var spec = resource.getSpec(); var status = initializeStatus(resource); - var name = resource.getMetadata().getName(); var namespace = resource.getMetadata().getNamespace(); + var name = resource.getMetadata().getName(); log.info( "Deleting DefaultPrivilege [resource={}/{}, status.phase={}]", @@ -213,8 +213,8 @@ private UpdateControl reconcileInTransaction( ) { var spec = resource.getSpec(); - var name = resource.getMetadata().getName(); var namespace = resource.getMetadata().getNamespace(); + var name = resource.getMetadata().getName(); var expectedPrivileges = Set.copyOf(spec.getPrivileges()); diff --git a/operator/src/main/java/it/aboutbits/postgresql/crd/grant/GrantReconciler.java b/operator/src/main/java/it/aboutbits/postgresql/crd/grant/GrantReconciler.java index cfd8a25..4739683 100644 --- a/operator/src/main/java/it/aboutbits/postgresql/crd/grant/GrantReconciler.java +++ b/operator/src/main/java/it/aboutbits/postgresql/crd/grant/GrantReconciler.java @@ -46,8 +46,8 @@ public UpdateControl reconcile( var spec = resource.getSpec(); var status = initializeStatus(resource); - var name = resource.getMetadata().getName(); var namespace = resource.getMetadata().getNamespace(); + var name = resource.getMetadata().getName(); log.info( "Reconciling Grant [resource={}/{}, status.phase={}]", @@ -129,8 +129,8 @@ public DeleteControl cleanup( var spec = resource.getSpec(); var status = initializeStatus(resource); - var name = resource.getMetadata().getName(); var namespace = resource.getMetadata().getNamespace(); + var name = resource.getMetadata().getName(); log.info( "Deleting Grant [resource={}/{}, status.phase={}]", @@ -220,8 +220,8 @@ private UpdateControl reconcileInTransaction( Grant resource, CRStatus status ) { - var name = resource.getMetadata().getName(); var namespace = resource.getMetadata().getNamespace(); + var name = resource.getMetadata().getName(); var spec = resource.getSpec(); diff --git a/operator/src/main/java/it/aboutbits/postgresql/crd/role/RoleReconciler.java b/operator/src/main/java/it/aboutbits/postgresql/crd/role/RoleReconciler.java index d7a062b..6ac0d10 100644 --- a/operator/src/main/java/it/aboutbits/postgresql/crd/role/RoleReconciler.java +++ b/operator/src/main/java/it/aboutbits/postgresql/crd/role/RoleReconciler.java @@ -60,8 +60,8 @@ public UpdateControl reconcile( var spec = resource.getSpec(); var status = initializeStatus(resource); - var name = resource.getMetadata().getName(); var namespace = resource.getMetadata().getNamespace(); + var name = resource.getMetadata().getName(); log.info( "Reconciling Role [resource={}/{}, status.phase={}]", @@ -140,8 +140,8 @@ public DeleteControl cleanup( var spec = resource.getSpec(); var status = initializeStatus(resource); - var name = resource.getMetadata().getName(); var namespace = resource.getMetadata().getNamespace(); + var name = resource.getMetadata().getName(); log.info( "Deleting Role [resource={}/{}, spec.name={}, status.phase={}]", @@ -249,8 +249,8 @@ private UpdateControl reconcileInTransaction( CRStatus status, @Nullable String password ) { - var name = resource.getMetadata().getName(); var namespace = resource.getMetadata().getNamespace(); + var name = resource.getMetadata().getName(); var spec = resource.getSpec(); var expectedFlags = spec.getFlags(); diff --git a/operator/src/main/java/it/aboutbits/postgresql/crd/schema/SchemaReconciler.java b/operator/src/main/java/it/aboutbits/postgresql/crd/schema/SchemaReconciler.java index 9b56749..b05f6d9 100644 --- a/operator/src/main/java/it/aboutbits/postgresql/crd/schema/SchemaReconciler.java +++ b/operator/src/main/java/it/aboutbits/postgresql/crd/schema/SchemaReconciler.java @@ -38,8 +38,8 @@ public UpdateControl reconcile( var spec = resource.getSpec(); var status = initializeStatus(resource); - var name = resource.getMetadata().getName(); var namespace = resource.getMetadata().getNamespace(); + var name = resource.getMetadata().getName(); log.info( "Reconciling Schema [resource={}/{}, status.phase={}]", @@ -100,8 +100,8 @@ public DeleteControl cleanup( var spec = resource.getSpec(); var status = initializeStatus(resource); - var name = resource.getMetadata().getName(); var namespace = resource.getMetadata().getNamespace(); + var name = resource.getMetadata().getName(); log.info( "{}ing Schema [resource={}/{}, spec.name={}, status.phase={}]", @@ -187,8 +187,8 @@ private UpdateControl reconcileInTransaction( Schema resource, CRStatus status ) { - var name = resource.getMetadata().getName(); var namespace = resource.getMetadata().getNamespace(); + var name = resource.getMetadata().getName(); var spec = resource.getSpec(); From dff124d646afc3762c1586de3cba3a4bcd9fb1db Mon Sep 17 00:00:00 2001 From: Thomas Sapelza Date: Thu, 26 Feb 2026 10:49:51 +0100 Subject: [PATCH 10/10] use mutable instead of immutable in the docs --- docs/cluster-connection.md | 14 +++++++------- docs/database.md | 12 ++++++------ docs/default-privilege.md | 18 +++++++++--------- docs/grant.md | 18 +++++++++--------- docs/role.md | 14 +++++++------- docs/schema.md | 14 +++++++------- 6 files changed, 45 insertions(+), 45 deletions(-) diff --git a/docs/cluster-connection.md b/docs/cluster-connection.md index 0b88e39..139fcfb 100644 --- a/docs/cluster-connection.md +++ b/docs/cluster-connection.md @@ -7,13 +7,13 @@ Other Custom Resources (like `Database`, `Role`, `Schema`, `Grant`, `DefaultPriv ## Spec -| Field | Type | Description | Required | -|------------------|---------------------|-----------------------------------------------------------------------|----------| -| `host` | `string` | The hostname of the PostgreSQL instance. | Yes | -| `port` | `integer` | The port of the PostgreSQL instance (1-65535). | Yes | -| `database` | `string` | The database to connect to (usually `postgres` for admin operations). | Yes | -| `adminSecretRef` | `ResourceRef` | Reference to the Kubernetes Secret containing the admin credentials. | Yes | -| `parameters` | `map[string]string` | Additional connection parameters. | No | +| Field | Type | Description | Required | Mutable | +|------------------|---------------------|-----------------------------------------------------------------------|----------|---------| +| `host` | `string` | The hostname of the PostgreSQL instance. | Yes | Yes | +| `port` | `integer` | The port of the PostgreSQL instance (1-65535). | Yes | Yes | +| `database` | `string` | The database to connect to (usually `postgres` for admin operations). | Yes | Yes | +| `adminSecretRef` | `ResourceRef` | Reference to the Kubernetes Secret containing the admin credentials. | Yes | Yes | +| `parameters` | `map[string]string` | Additional connection parameters. | No | Yes | ### ResourceRef (`adminSecretRef`) diff --git a/docs/database.md b/docs/database.md index d1c7df4..1bc7be4 100644 --- a/docs/database.md +++ b/docs/database.md @@ -4,12 +4,12 @@ The `Database` Custom Resource Definition (CRD) is responsible for managing Post ## Spec -| Field | Type | Description | Required | Immutable | -|-----------------|---------------|------------------------------------------------------------------------------------------------------|----------|-----------| -| `clusterRef` | `ResourceRef` | Reference to the `ClusterConnection` to use. | Yes | No | -| `name` | `string` | The name of the database to create. | Yes | Yes | -| `owner` | `string` | The owner of the database. | No | No | -| `reclaimPolicy` | `string` | The policy for reclaiming the database when the CR is deleted. Values: `Retain` (Default), `Delete`. | No | No | +| Field | Type | Description | Required | Mutable | +|-----------------|---------------|------------------------------------------------------------------------------------------------------|----------|---------| +| `clusterRef` | `ResourceRef` | Reference to the `ClusterConnection` to use. | Yes | Yes | +| `name` | `string` | The name of the database to create. | Yes | No | +| `owner` | `string` | The owner of the database. | No | Yes | +| `reclaimPolicy` | `string` | The policy for reclaiming the database when the CR is deleted. Values: `Retain` (Default), `Delete`. | No | Yes | ### ResourceRef (`clusterRef`) diff --git a/docs/default-privilege.md b/docs/default-privilege.md index 19d24ae..5a20264 100644 --- a/docs/default-privilege.md +++ b/docs/default-privilege.md @@ -4,15 +4,15 @@ The `DefaultPrivilege` Custom Resource Definition (CRD) manages default privileg ## Spec -| Field | Type | Description | Required | Immutable | -|--------------|-----------------|---------------------------------------------------------------------------------------------------------|-------------|-----------| -| `clusterRef` | `ResourceRef` | Reference to the `ClusterConnection` to use. | Yes | No | -| `database` | `string` | The database where default privileges apply. | Yes | Yes | -| `role` | `string` | The role to which default privileges are granted. | Yes | Yes | -| `owner` | `string` | The role that owns the objects (the creator). Default privileges apply to objects created by this role. | Yes | Yes | -| `schema` | `string` | The schema where default privileges apply. Required, unless `objectType` is `schema`. | Conditional | Yes | -| `objectType` | `string` | The type of object. | Yes | Yes | -| `privileges` | `array[string]` | List of privileges to grant. | Yes | No | +| Field | Type | Description | Required | Mutable | +|--------------|-----------------|---------------------------------------------------------------------------------------------------------|-------------|---------| +| `clusterRef` | `ResourceRef` | Reference to the `ClusterConnection` to use. | Yes | Yes | +| `database` | `string` | The database where default privileges apply. | Yes | No | +| `role` | `string` | The role to which default privileges are granted. | Yes | No | +| `owner` | `string` | The role that owns the objects (the creator). Default privileges apply to objects created by this role. | Yes | No | +| `schema` | `string` | The schema where default privileges apply. Required, unless `objectType` is `schema`. | Conditional | No | +| `objectType` | `string` | The type of object. | Yes | No | +| `privileges` | `array[string]` | List of privileges to grant. | Yes | Yes | ### Object Types diff --git a/docs/grant.md b/docs/grant.md index 7c24d9f..9f9fbfc 100644 --- a/docs/grant.md +++ b/docs/grant.md @@ -4,15 +4,15 @@ The `Grant` Custom Resource Definition (CRD) is responsible for managing privile ## Spec -| Field | Type | Description | Required | Immutable | -|--------------|-----------------|--------------------------------------------------------------------------------------------------------------------------------------------|-------------|-----------| -| `clusterRef` | `ResourceRef` | Reference to the `ClusterConnection` to use. | Yes | No | -| `database` | `string` | The database containing the objects. | Yes | Yes | -| `role` | `string` | The role to which privileges are granted. | Yes | Yes | -| `schema` | `string` | The schema containing the objects. Required, unless `objectType` is `database`. | Conditional | Yes | -| `objectType` | `string` | The type of object. | Yes | Yes | -| `objects` | `array[string]` | List of object names. If empty, all objects of this `objectType` will be granted. Required, unless `objectType` is `database` or `schema`. | Conditional | No | -| `privileges` | `array[string]` | List of privileges to grant. | Yes | No | +| Field | Type | Description | Required | Mutable | +|--------------|-----------------|--------------------------------------------------------------------------------------------------------------------------------------------|-------------|---------| +| `clusterRef` | `ResourceRef` | Reference to the `ClusterConnection` to use. | Yes | Yes | +| `database` | `string` | The database containing the objects. | Yes | No | +| `role` | `string` | The role to which privileges are granted. | Yes | No | +| `schema` | `string` | The schema containing the objects. Required, unless `objectType` is `database`. | Conditional | No | +| `objectType` | `string` | The type of object. | Yes | No | +| `objects` | `array[string]` | List of object names. If empty, all objects of this `objectType` will be granted. Required, unless `objectType` is `database` or `schema`. | Conditional | Yes | +| `privileges` | `array[string]` | List of privileges to grant. | Yes | Yes | ### Object Types diff --git a/docs/role.md b/docs/role.md index dddeb59..cdea41b 100644 --- a/docs/role.md +++ b/docs/role.md @@ -4,13 +4,13 @@ The `Role` Custom Resource Definition (CRD) manages PostgreSQL roles (users). ## Spec -| Field | Type | Description | Required | Immutable | -|---------------------|---------------|-------------------------------------------------------------------------------------|----------|-----------| -| `clusterRef` | `ResourceRef` | Reference to the `ClusterConnection` to use. | Yes | No | -| `name` | `string` | The name of the role to create in the database. | Yes | Yes | -| `comment` | `string` | A comment to add to the role. | No | No | -| `passwordSecretRef` | `ResourceRef` | Reference to a secret containing the password for the role to make it a LOGIN role. | No | No | -| `flags` | `RoleFlags` | Flags and attributes for the role. | No | No | +| Field | Type | Description | Required | Mutable | +|---------------------|---------------|-------------------------------------------------------------------------------------|----------|---------| +| `clusterRef` | `ResourceRef` | Reference to the `ClusterConnection` to use. | Yes | Yes | +| `name` | `string` | The name of the role to create in the database. | Yes | No | +| `comment` | `string` | A comment to add to the role. | No | Yes | +| `passwordSecretRef` | `ResourceRef` | Reference to a secret containing the password for the role to make it a LOGIN role. | No | Yes | +| `flags` | `RoleFlags` | Flags and attributes for the role. | No | Yes | ### ResourceRef (`clusterRef` and `passwordSecretRef`) diff --git a/docs/schema.md b/docs/schema.md index edd70a4..9a042a0 100644 --- a/docs/schema.md +++ b/docs/schema.md @@ -4,13 +4,13 @@ The `Schema` Custom Resource Definition (CRD) is responsible for managing Postgr ## Spec -| Field | Type | Description | Required | Immutable | -|-----------------|---------------|----------------------------------------------------------------------------------------------------|----------|-----------| -| `clusterRef` | `ResourceRef` | Reference to the `ClusterConnection` to use. | Yes | No | -| `database` | `string` | The name of the database in which the schema is created. | Yes | Yes | -| `name` | `string` | The name of the schema to create. | Yes | Yes | -| `owner` | `string` | The owner of the schema. | No | No | -| `reclaimPolicy` | `string` | The policy for reclaiming the schema when the CR is deleted. Values: `Retain` (Default), `Delete`. | No | No | +| Field | Type | Description | Required | Mutable | +|-----------------|---------------|----------------------------------------------------------------------------------------------------|----------|---------| +| `clusterRef` | `ResourceRef` | Reference to the `ClusterConnection` to use. | Yes | Yes | +| `database` | `string` | The name of the database in which the schema is created. | Yes | No | +| `name` | `string` | The name of the schema to create. | Yes | No | +| `owner` | `string` | The owner of the schema. | No | Yes | +| `reclaimPolicy` | `string` | The policy for reclaiming the schema when the CR is deleted. Values: `Retain` (Default), `Delete`. | No | Yes | ### ResourceRef (`clusterRef`)