Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -60,3 +60,6 @@ gradle-app.setting

# Quinoa
.quinoa/

generated/out/
operator/out/
Comment thread
ThoSap marked this conversation as resolved.
Outdated
44 changes: 36 additions & 8 deletions docs/cluster-connection.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,16 @@ Other Custom Resources (like `Database`, `Role`, `Schema`, `Grant`, `DefaultPriv

## Spec

| 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 |
| 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. | No | Yes |
| `adminSecretFileRef`| `ResourceFileRef` | Reference to a file containing the admin credentials. | No | Yes |
Comment thread
ThoSap marked this conversation as resolved.
Outdated
| `parameters` | `map[string]string` | Additional connection parameters. | No | Yes |

> **Note:** Exactly one of `adminSecretRef` or `adminSecretFileRef` must be provided.

### ResourceRef (`adminSecretRef`)

Expand All @@ -24,7 +27,17 @@ Other Custom Resources (like `Database`, `Role`, `Schema`, `Grant`, `DefaultPriv

The referenced secret must be of type `kubernetes.io/basic-auth` and contain the keys `username` and `password`.

### Example
### ResourceFileRef (`adminSecretFileRef`)

| Field | Type | Description | Required |
|--------|----------|----------------------------------------------------------------|----------|
| `path` | `string` | The path to the file containing the admin credentials. | Yes |

Use this option when credentials are mounted as a file (e.g. via AWS Secrets Manager) instead of a Kubernetes Secret.
Comment thread
ThoSap marked this conversation as resolved.
Outdated

### Examples

#### Using a Kubernetes Secret (`adminSecretRef`)

```yaml
apiVersion: v1
Expand Down Expand Up @@ -54,3 +67,18 @@ spec:
#sslmode: "require" # Enforce SSL encryption
#connectTimeout: "10" # Timeout in seconds for connection attempts
```

#### Using a file reference (`adminSecretFileRef`)

```yaml
apiVersion: postgresql.aboutbits.it/v1
kind: ClusterConnection
metadata:
name: my-postgres-connection
spec:
adminSecretFileRef:
path: "/mnt/db-password"
host: localhost
port: 5432
database: postgres
```
70 changes: 69 additions & 1 deletion docs/docker-environment.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,16 @@ users:

## 2. Create PostgreSQL Connection and Secret

For the `postgresql` Dev Service, you can generate the necessary Custom Resources to test the Operator:
For the `postgresql` Dev Service, you can generate the necessary Custom Resources to test the Operator.

A `ClusterConnection` requires admin credentials, which can be provided in one of two ways:

- **`adminSecretRef`** — references a Kubernetes `basic-auth` Secret (username + password).
- **`adminSecretFileRef`** — references a JSON file mounted into the operator pod (e.g. from AWS Secrets Manager).
Comment thread
ThoSap marked this conversation as resolved.
Outdated

Exactly one of these must be specified.

### Using a Kubernetes Secret (`adminSecretRef`)

1. From the Dev UI, get the `postgresql` Dev Service properties (username, password, host, port).
2. Convert the `postgresql` Dev Service properties to a **Basic Auth Secret** and a **ClusterConnection** CR instance.
Expand Down Expand Up @@ -79,6 +88,65 @@ spec:
database: postgres
```

### Using a file reference (`adminSecretFileRef`)

Instead of a Kubernetes Secret, you can mount a JSON credentials file into the operator pod and reference its path. This is useful when credentials are managed externally (e.g. AWS Secrets Manager).

#### File format

The file must contain JSON with the following fields:

```json
{
"username": "root",
"password": "password"
}
```

- `password` — **required**
- `username` — optional (can be omitted)
Comment thread
ThoSap marked this conversation as resolved.
Outdated

#### Mount the credentials file

The file must be accessible inside the operator pod at the path specified in `adminSecretFileRef.path`. Mount it using a Volume and VolumeMount on the operator Deployment:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[follow-up] The operator is installed with the generated Helm chart, but the chart exposes no volumes or volumeMounts values.
So a chart user cannot mount this file without patching the rendered Deployment, and the feature is not usable through the documented install path.
This page also describes the local dev setup, where the operator runs on your machine and there is no pod to patch.

Fix: We will add app.volumes and app.volumeMounts chart values in a follow-up PR on our side, since quarkus-helm cannot add fields that Quarkus does not generate (quarkiverse/quarkus-helm#453).

Comment thread
ThoSap marked this conversation as resolved.
Outdated

```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: postgresql-operator
spec:
template:
spec:
containers:
- name: operator
volumeMounts:
- name: db-credentials
mountPath: /mnt/secrets
readOnly: true
volumes:
- name: db-credentials
secret:
secretName: db-credentials-secret
```

> **Note:** The volume source can be any type that provides a file (e.g. a Kubernetes Secret, a CSI volume from AWS Secrets Manager, or a ConfigMap for testing).

#### Example ClusterConnection

```yaml
apiVersion: postgresql.aboutbits.it/v1
kind: ClusterConnection
metadata:
name: quarkus-postgres-connection
spec:
adminSecretFileRef:
path: "/mnt/secrets/db-credentials.json"
host: localhost
port: 5432
database: postgres
```

![Established Cluster Connection](images/established-cluster-connection.png)

## 3. Create a Role
Expand Down
2 changes: 1 addition & 1 deletion docs/terraform.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ Every optional field of every Custom Resource is affected, in particular:

| Custom Resource | Optional fields |
|---------------------|------------------------------------------------------------------------------------------------|
| `ClusterConnection` | `parameters`, `adminSecretRef.namespace` |
| `ClusterConnection` | `parameters`, `adminSecretRef`, `adminSecretRef.namespace`, `adminSecretFileRef` |
| `Database` | `owner`, `reclaimPolicy`, `clusterRef.namespace` |
| `Schema` | `owner`, `reclaimPolicy`, `clusterRef.namespace` |
| `Role` | `comment`, `passwordSecretRef`, `flags` (including `flags.validUntil`), `clusterRef.namespace` |
Expand Down
3 changes: 3 additions & 0 deletions gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,6 @@ quarkusPlatformGroupId=io.quarkus.platform
quarkusPlatformArtifactId=quarkus-bom
quarkusPlatformVersion=3.35.3
systemProp.quarkus.analytics.disabled=true

# Workaround for Windows: avoid forked process where -D args with {{ }} get mangled by cmd.exe
systemProp.gradle.quarkus.gradle-worker.no-process=true

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This property is not related to the feature and it changes the build for everyone, including CI.

Quarkus reads gradle.quarkus.gradle-worker.no-process and then runs its build steps inside the Gradle daemon with classloader isolation instead of a separate process (QuarkusTask.java:58 in Quarkus 3.35.3).

Put it in your own ~/.gradle/gradle.properties, and open an issue that describes the problem with Windows instead (I am using a MacBook due to work), then we will fix it.

Suggested change
# Workaround for Windows: avoid forked process where -D args with {{ }} get mangled by cmd.exe
systemProp.gradle.quarkus.gradle-worker.no-process=true

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ThoSap I will open a new issue then, thanks.

Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
package it.aboutbits.postgresql.core;

import com.fasterxml.jackson.databind.ObjectMapper;
import io.fabric8.kubernetes.client.KubernetesClient;
import it.aboutbits.postgresql.crd.clusterconnection.ClusterConnection;
import it.aboutbits.postgresql.crd.clusterconnection.ClusterConnectionSpec;
import jakarta.inject.Singleton;
import org.jspecify.annotations.NullMarked;

import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Base64;

@Singleton
Expand All @@ -19,11 +24,51 @@ public Credentials getSecretRefCredentials(
KubernetesClient kubernetesClient,
ClusterConnection clusterConnection
) {
return getSecretRefCredentials(
kubernetesClient,
clusterConnection.getSpec().getAdminSecretRef(),
clusterConnection.getMetadata().getNamespace()
);
ClusterConnectionSpec spec = clusterConnection.getSpec();

@ThoSap ThoSap Sep 3, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We try to use var throughout this project, I will enable https://errorprone.info/bugpattern/Varifier in a follow-up PR.

Suggested change
ClusterConnectionSpec spec = clusterConnection.getSpec();
var spec = clusterConnection.getSpec();

if (spec.getAdminSecretRef() != null) {
Comment thread
ThoSap marked this conversation as resolved.
return getSecretRefCredentials(
kubernetesClient,
clusterConnection.getSpec().getAdminSecretRef(),
clusterConnection.getMetadata().getNamespace()
);

} else if (spec.getAdminSecretFileRef() != null) {
Comment thread
ThoSap marked this conversation as resolved.
Outdated
return getSecretFileRefCredentials(spec.getAdminSecretFileRef());
}

throw new IllegalStateException("Exactly one of 'adminSecretRef' or 'adminSecretFileRef' must be provided");

}
Comment thread
ThoSap marked this conversation as resolved.

public Credentials getSecretFileRefCredentials(ResourceFileRef fileRef) {
var path = Path.of(fileRef.getPath());

if (!Files.exists(path)) {
throw new IllegalStateException("AWS Secrets Manager file not found [path=%s]".formatted(path));
Comment thread
ThoSap marked this conversation as resolved.
Outdated
}

try {
var content = Files.readString(path);
var objectMapper = new ObjectMapper();
var json = objectMapper.readTree(content);

var usernameNode = json.get(SECRET_DATA_BASIC_AUTH_USERNAME_KEY);
var username = usernameNode != null && !usernameNode.isNull()
? usernameNode.asText()
: null;
Comment thread
ThoSap marked this conversation as resolved.
Outdated

var passwordNode = json.get(SECRET_DATA_BASIC_AUTH_PASSWORD_KEY);
if (passwordNode == null || passwordNode.isNull()) {
throw new IllegalStateException("AWS Secrets Manager file is missing required field '%s' [path=%s]".formatted(
Comment thread
ThoSap marked this conversation as resolved.
Outdated
SECRET_DATA_BASIC_AUTH_PASSWORD_KEY,
path
));
}

return new Credentials(username, passwordNode.asText());
} catch (IOException e) {
throw new IllegalStateException("Failed to read AWS Secrets Manager file [path=%s]".formatted(path), e);
Comment thread
ThoSap marked this conversation as resolved.
Outdated
}
}

public Credentials getSecretRefCredentials(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package it.aboutbits.postgresql.core;

import io.fabric8.generator.annotation.Required;
import io.fabric8.generator.annotation.ValidationRule;
import lombok.Getter;
import lombok.Setter;
import org.jspecify.annotations.NullMarked;

/// A reference to a file inside an AWS Secrets Manager secret.
Comment thread
ThoSap marked this conversation as resolved.
Outdated
///
/// This class is used wherever a CRD spec needs to point to a specific file
/// within an AWS secret. The [#path] field identifies the file location
/// inside the secret.
Comment thread
ThoSap marked this conversation as resolved.
Outdated
///
/// ### Example usage in a CR manifest
///
/// ```yaml
/// spec:
/// adminSecretFileRef:
/// path: "/mnt/db-password"
Comment thread
ThoSap marked this conversation as resolved.
Outdated
/// ```
@Getter
@Setter
@NullMarked
public class ResourceFileRef {
Comment thread
ThoSap marked this conversation as resolved.
Outdated
/// The path to the file inside the AWS Secrets Manager secret.
Comment thread
ThoSap marked this conversation as resolved.
Outdated
/// Must not be blank.
@Required
@ValidationRule(
value = "self.trim().size() > 0",
message = "The path must not be empty."
)
private String path = "";
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import io.fabric8.generator.annotation.Min;
import io.fabric8.generator.annotation.Required;
import io.fabric8.generator.annotation.ValidationRule;
import it.aboutbits.postgresql.core.ResourceFileRef;
import it.aboutbits.postgresql.core.ResourceRef;
import it.aboutbits.postgresql.core.schema_customizer.HostCustomizer;
import lombok.Getter;
Expand All @@ -18,6 +19,10 @@
@Setter
@SchemaCustomizer(value = HostCustomizer.class, input = "host")
@NullMarked
@ValidationRule(
Comment thread
ThoSap marked this conversation as resolved.
value = "(has(self.adminSecretRef) ? 1 : 0) + (has(self.adminSecretFileRef) ? 1 : 0) == 1",
message = "Exactly one of 'adminSecretRef' or 'adminSecretFileRef' must be provided"
)
public class ClusterConnectionSpec {
@Required
@ValidationRule(
Expand All @@ -38,8 +43,11 @@ public class ClusterConnectionSpec {
)
private String database = "postgres";

@Required
private ResourceRef adminSecretRef = new ResourceRef();
@io.fabric8.generator.annotation.Nullable
private ResourceRef adminSecretRef;

@io.fabric8.generator.annotation.Nullable
private ResourceFileRef adminSecretFileRef;
Comment thread
ThoSap marked this conversation as resolved.
Outdated

@io.fabric8.generator.annotation.Nullable
private Map<String, String> parameters = new HashMap<>();
Expand Down
Loading
Loading