Skip to content

Commit 0161244

Browse files
author
Fred Campos
committed
Add adminSecretFileRef as alternative to adminSecretRef for file-based credentials in ClusterConnection
1 parent 6248b1e commit 0161244

9 files changed

Lines changed: 731 additions & 17 deletions

File tree

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,3 +60,6 @@ gradle-app.setting
6060

6161
# Quinoa
6262
.quinoa/
63+
64+
generated/out/
65+
operator/out/

docs/cluster-connection.md

Lines changed: 36 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,16 @@ Other Custom Resources (like `Database`, `Role`, `Schema`, `Grant`, `DefaultPriv
77

88
## Spec
99

10-
| Field | Type | Description | Required | Mutable |
11-
|------------------|---------------------|-----------------------------------------------------------------------|----------|---------|
12-
| `host` | `string` | The hostname of the PostgreSQL instance. | Yes | Yes |
13-
| `port` | `integer` | The port of the PostgreSQL instance (1-65535). | Yes | Yes |
14-
| `database` | `string` | The database to connect to (usually `postgres` for admin operations). | Yes | Yes |
15-
| `adminSecretRef` | `ResourceRef` | Reference to the Kubernetes Secret containing the admin credentials. | Yes | Yes |
16-
| `parameters` | `map[string]string` | Additional connection parameters. | No | Yes |
10+
| Field | Type | Description | Required | Mutable |
11+
|---------------------|---------------------|-----------------------------------------------------------------------|----------|---------|
12+
| `host` | `string` | The hostname of the PostgreSQL instance. | Yes | Yes |
13+
| `port` | `integer` | The port of the PostgreSQL instance (1-65535). | Yes | Yes |
14+
| `database` | `string` | The database to connect to (usually `postgres` for admin operations). | Yes | Yes |
15+
| `adminSecretRef` | `ResourceRef` | Reference to the Kubernetes Secret containing the admin credentials. | No | Yes |
16+
| `adminSecretFileRef`| `ResourceFileRef` | Reference to a file containing the admin credentials. | No | Yes |
17+
| `parameters` | `map[string]string` | Additional connection parameters. | No | Yes |
18+
19+
> **Note:** Exactly one of `adminSecretRef` or `adminSecretFileRef` must be provided.
1720
1821
### ResourceRef (`adminSecretRef`)
1922

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

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

27-
### Example
30+
### ResourceFileRef (`adminSecretFileRef`)
31+
32+
| Field | Type | Description | Required |
33+
|--------|----------|----------------------------------------------------------------|----------|
34+
| `path` | `string` | The path to the file containing the admin credentials. | Yes |
35+
36+
Use this option when credentials are mounted as a file (e.g. via AWS Secrets Manager) instead of a Kubernetes Secret.
37+
38+
### Examples
39+
40+
#### Using a Kubernetes Secret (`adminSecretRef`)
2841

2942
```yaml
3043
apiVersion: v1
@@ -54,3 +67,18 @@ spec:
5467
#sslmode: "require" # Enforce SSL encryption
5568
#connectTimeout: "10" # Timeout in seconds for connection attempts
5669
```
70+
71+
#### Using a file reference (`adminSecretFileRef`)
72+
73+
```yaml
74+
apiVersion: postgresql.aboutbits.it/v1
75+
kind: ClusterConnection
76+
metadata:
77+
name: my-postgres-connection
78+
spec:
79+
adminSecretFileRef:
80+
path: "/mnt/db-password"
81+
host: localhost
82+
port: 5432
83+
database: postgres
84+
```

docs/docker-environment.md

Lines changed: 69 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,16 @@ users:
3939

4040
## 2. Create PostgreSQL Connection and Secret
4141

42-
For the `postgresql` Dev Service, you can generate the necessary Custom Resources to test the Operator:
42+
For the `postgresql` Dev Service, you can generate the necessary Custom Resources to test the Operator.
43+
44+
A `ClusterConnection` requires admin credentials, which can be provided in one of two ways:
45+
46+
- **`adminSecretRef`** — references a Kubernetes `basic-auth` Secret (username + password).
47+
- **`adminSecretFileRef`** — references a JSON file mounted into the operator pod (e.g. from AWS Secrets Manager).
48+
49+
Exactly one of these must be specified.
50+
51+
### Using a Kubernetes Secret (`adminSecretRef`)
4352

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

91+
### Using a file reference (`adminSecretFileRef`)
92+
93+
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).
94+
95+
#### File format
96+
97+
The file must contain JSON with the following fields:
98+
99+
```json
100+
{
101+
"username": "root",
102+
"password": "password"
103+
}
104+
```
105+
106+
- `password` — **required**
107+
- `username` — optional (can be omitted)
108+
109+
#### Mount the credentials file
110+
111+
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:
112+
113+
```yaml
114+
apiVersion: apps/v1
115+
kind: Deployment
116+
metadata:
117+
name: postgresql-operator
118+
spec:
119+
template:
120+
spec:
121+
containers:
122+
- name: operator
123+
volumeMounts:
124+
- name: db-credentials
125+
mountPath: /mnt/secrets
126+
readOnly: true
127+
volumes:
128+
- name: db-credentials
129+
secret:
130+
secretName: db-credentials-secret
131+
```
132+
133+
> **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).
134+
135+
#### Example ClusterConnection
136+
137+
```yaml
138+
apiVersion: postgresql.aboutbits.it/v1
139+
kind: ClusterConnection
140+
metadata:
141+
name: quarkus-postgres-connection
142+
spec:
143+
adminSecretFileRef:
144+
path: "/mnt/secrets/db-credentials.json"
145+
host: localhost
146+
port: 5432
147+
database: postgres
148+
```
149+
82150
![Established Cluster Connection](images/established-cluster-connection.png)
83151

84152
## 3. Create a Role

docs/terraform.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ Every optional field of every Custom Resource is affected, in particular:
9595

9696
| Custom Resource | Optional fields |
9797
|---------------------|------------------------------------------------------------------------------------------------|
98-
| `ClusterConnection` | `parameters`, `adminSecretRef.namespace` |
98+
| `ClusterConnection` | `parameters`, `adminSecretRef`, `adminSecretRef.namespace`, `adminSecretFileRef` |
9999
| `Database` | `owner`, `reclaimPolicy`, `clusterRef.namespace` |
100100
| `Schema` | `owner`, `reclaimPolicy`, `clusterRef.namespace` |
101101
| `Role` | `comment`, `passwordSecretRef`, `flags` (including `flags.validUntil`), `clusterRef.namespace` |

gradle.properties

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,6 @@ quarkusPlatformGroupId=io.quarkus.platform
1313
quarkusPlatformArtifactId=quarkus-bom
1414
quarkusPlatformVersion=3.35.3
1515
systemProp.quarkus.analytics.disabled=true
16+
17+
# Workaround for Windows: avoid forked process where -D args with {{ }} get mangled by cmd.exe
18+
systemProp.gradle.quarkus.gradle-worker.no-process=true

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

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

3+
import com.fasterxml.jackson.databind.ObjectMapper;
34
import io.fabric8.kubernetes.client.KubernetesClient;
45
import it.aboutbits.postgresql.crd.clusterconnection.ClusterConnection;
6+
import it.aboutbits.postgresql.crd.clusterconnection.ClusterConnectionSpec;
57
import jakarta.inject.Singleton;
68
import org.jspecify.annotations.NullMarked;
79

10+
import java.io.IOException;
811
import java.nio.charset.Charset;
12+
import java.nio.file.Files;
13+
import java.nio.file.Path;
914
import java.util.Base64;
1015

1116
@Singleton
@@ -19,11 +24,51 @@ public Credentials getSecretRefCredentials(
1924
KubernetesClient kubernetesClient,
2025
ClusterConnection clusterConnection
2126
) {
22-
return getSecretRefCredentials(
23-
kubernetesClient,
24-
clusterConnection.getSpec().getAdminSecretRef(),
25-
clusterConnection.getMetadata().getNamespace()
26-
);
27+
ClusterConnectionSpec spec = clusterConnection.getSpec();
28+
if (spec.getAdminSecretRef() != null) {
29+
return getSecretRefCredentials(
30+
kubernetesClient,
31+
clusterConnection.getSpec().getAdminSecretRef(),
32+
clusterConnection.getMetadata().getNamespace()
33+
);
34+
35+
} else if (spec.getAdminSecretFileRef() != null) {
36+
return getSecretFileRefCredentials(spec.getAdminSecretFileRef());
37+
}
38+
39+
throw new IllegalStateException("Exactly one of 'adminSecretRef' or 'adminSecretFileRef' must be provided");
40+
41+
}
42+
43+
public Credentials getSecretFileRefCredentials(ResourceFileRef fileRef) {
44+
var path = Path.of(fileRef.getPath());
45+
46+
if (!Files.exists(path)) {
47+
throw new IllegalStateException("AWS Secrets Manager file not found [path=%s]".formatted(path));
48+
}
49+
50+
try {
51+
var content = Files.readString(path);
52+
var objectMapper = new ObjectMapper();
53+
var json = objectMapper.readTree(content);
54+
55+
var usernameNode = json.get(SECRET_DATA_BASIC_AUTH_USERNAME_KEY);
56+
var username = usernameNode != null && !usernameNode.isNull()
57+
? usernameNode.asText()
58+
: null;
59+
60+
var passwordNode = json.get(SECRET_DATA_BASIC_AUTH_PASSWORD_KEY);
61+
if (passwordNode == null || passwordNode.isNull()) {
62+
throw new IllegalStateException("AWS Secrets Manager file is missing required field '%s' [path=%s]".formatted(
63+
SECRET_DATA_BASIC_AUTH_PASSWORD_KEY,
64+
path
65+
));
66+
}
67+
68+
return new Credentials(username, passwordNode.asText());
69+
} catch (IOException e) {
70+
throw new IllegalStateException("Failed to read AWS Secrets Manager file [path=%s]".formatted(path), e);
71+
}
2772
}
2873

2974
public Credentials getSecretRefCredentials(
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package it.aboutbits.postgresql.core;
2+
3+
import io.fabric8.generator.annotation.Required;
4+
import io.fabric8.generator.annotation.ValidationRule;
5+
import lombok.Getter;
6+
import lombok.Setter;
7+
import org.jspecify.annotations.NullMarked;
8+
9+
/// A reference to a file inside an AWS Secrets Manager secret.
10+
///
11+
/// This class is used wherever a CRD spec needs to point to a specific file
12+
/// within an AWS secret. The [#path] field identifies the file location
13+
/// inside the secret.
14+
///
15+
/// ### Example usage in a CR manifest
16+
///
17+
/// ```yaml
18+
/// spec:
19+
/// adminSecretFileRef:
20+
/// path: "/mnt/db-password"
21+
/// ```
22+
@Getter
23+
@Setter
24+
@NullMarked
25+
public class ResourceFileRef {
26+
/// The path to the file inside the AWS Secrets Manager secret.
27+
/// Must not be blank.
28+
@Required
29+
@ValidationRule(
30+
value = "self.trim().size() > 0",
31+
message = "The path must not be empty."
32+
)
33+
private String path = "";
34+
}

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

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import io.fabric8.generator.annotation.Min;
66
import io.fabric8.generator.annotation.Required;
77
import io.fabric8.generator.annotation.ValidationRule;
8+
import it.aboutbits.postgresql.core.ResourceFileRef;
89
import it.aboutbits.postgresql.core.ResourceRef;
910
import it.aboutbits.postgresql.core.schema_customizer.HostCustomizer;
1011
import lombok.Getter;
@@ -18,6 +19,10 @@
1819
@Setter
1920
@SchemaCustomizer(value = HostCustomizer.class, input = "host")
2021
@NullMarked
22+
@ValidationRule(
23+
value = "(has(self.adminSecretRef) ? 1 : 0) + (has(self.adminSecretFileRef) ? 1 : 0) == 1",
24+
message = "Exactly one of 'adminSecretRef' or 'adminSecretFileRef' must be provided"
25+
)
2126
public class ClusterConnectionSpec {
2227
@Required
2328
@ValidationRule(
@@ -38,8 +43,11 @@ public class ClusterConnectionSpec {
3843
)
3944
private String database = "postgres";
4045

41-
@Required
42-
private ResourceRef adminSecretRef = new ResourceRef();
46+
@io.fabric8.generator.annotation.Nullable
47+
private ResourceRef adminSecretRef;
48+
49+
@io.fabric8.generator.annotation.Nullable
50+
private ResourceFileRef adminSecretFileRef;
4351

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

0 commit comments

Comments
 (0)