Skip to content

Commit 3f50c53

Browse files
author
Fred Campos
committed
Addressed PR review comments: require username in adminSecretFileRef, rename to FileRef, use fabric8 mock server, remove AWS references, updated unit tests and docs
1 parent 263b294 commit 3f50c53

11 files changed

Lines changed: 243 additions & 374 deletions

File tree

docs/cluster-connection.md

Lines changed: 50 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,8 @@ The referenced secret must be of type `kubernetes.io/basic-auth` and contain the
3333
|--------|----------|----------------------------------------------------------------|----------|
3434
| `path` | `string` | The path to the file containing the admin credentials. | Yes |
3535

36-
Use this option when credentials are mounted as a file (e.g. via AWS Secrets Manager) instead of a Kubernetes Secret.
36+
Use this option when the credentials are mounted as a file instead of a Kubernetes Secret. The file must be a JSON object with the keys `username` and `password`, for example `{"username": "postgres", "password": "password"}`.
37+
3738

3839
### Examples
3940

@@ -68,17 +69,62 @@ spec:
6869
#connectTimeout: "10" # Timeout in seconds for connection attempts
6970
```
7071

71-
#### Using a file reference (`adminSecretFileRef`)
72+
### Using a file reference (`adminSecretFileRef`)
73+
74+
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.
75+
76+
#### File format
77+
78+
The file must contain JSON with the following fields:
79+
80+
```json
81+
{
82+
"username": "root",
83+
"password": "password"
84+
}
85+
```
86+
87+
- `password` **required**
88+
- `username` **required**
89+
90+
#### Mount the credentials file
91+
92+
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:
93+
94+
```yaml
95+
apiVersion: apps/v1
96+
kind: Deployment
97+
metadata:
98+
name: postgresql-operator
99+
spec:
100+
template:
101+
spec:
102+
containers:
103+
- name: operator
104+
volumeMounts:
105+
- name: db-credentials
106+
mountPath: /mnt/secrets
107+
readOnly: true
108+
volumes:
109+
- name: db-credentials
110+
secret:
111+
secretName: db-credentials-secret
112+
```
113+
114+
> **Note:** The volume source can be any type that provides a file.
115+
116+
> **Note:** The Helm chart does not support extra volumes yet.
72117
73118
```yaml
74119
apiVersion: postgresql.aboutbits.it/v1
75120
kind: ClusterConnection
76121
metadata:
77-
name: my-postgres-connection
122+
name: quarkus-postgres-connection
78123
spec:
79124
adminSecretFileRef:
80-
path: "/mnt/db-password"
125+
path: "/mnt/secrets/db-credentials.json"
81126
host: localhost
82127
port: 5432
83128
database: postgres
84129
```
130+

docs/docker-environment.md

Lines changed: 2 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,8 @@ For the `postgresql` Dev Service, you can generate the necessary Custom Resource
4343

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

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).
46+
- **`adminSecretRef`** references a Kubernetes `basic-auth` Secret (username + password).
47+
- **`adminSecretFileRef`** references a JSON file mounted into the operator pod.
4848

4949
Exactly one of these must be specified.
5050

@@ -88,65 +88,6 @@ spec:
8888
database: postgres
8989
```
9090

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-
15091
![Established Cluster Connection](images/established-cluster-connection.png)
15192

15293
## 3. Create a Role

operator/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ dependencies {
6262
*/
6363
testImplementation("io.quarkus:quarkus-junit")
6464
testImplementation("io.quarkus:quarkus-junit-mockito")
65+
testImplementation("io.fabric8:kubernetes-server-mock")
6566
testImplementation("org.awaitility:awaitility")
6667
testImplementation(libs.assertj)
6768
testImplementation(libs.datafaker)

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,14 @@
1010
///
1111
/// This class is used wherever a CRD spec needs to point to a specific file
1212
/// The [#path] field identifies the file location
13-
/// inside the secret.
13+
/// within the container.
1414
///
1515
/// ### Example usage in a CR manifest
1616
///
1717
/// ```yaml
1818
/// spec:
1919
/// adminSecretFileRef:
20-
/// path: "/mnt/db-password"
20+
/// path: "/mnt/secrets/db-credentials.json"
2121
/// ```
2222
@Getter
2323
@Setter

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

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -16,58 +16,63 @@
1616
@Singleton
1717
@NullMarked
1818
public final class KubernetesService {
19+
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
20+
1921
public static final String SECRET_TYPE_BASIC_AUTH = "kubernetes.io/basic-auth";
2022
public static final String SECRET_DATA_BASIC_AUTH_USERNAME_KEY = "username";
2123
public static final String SECRET_DATA_BASIC_AUTH_PASSWORD_KEY = "password";
2224

23-
public Credentials getSecretRefCredentials(
25+
public Credentials getAdminCredentials(
2426
KubernetesClient kubernetesClient,
2527
ClusterConnection clusterConnection
2628
) {
27-
ClusterConnectionSpec spec = clusterConnection.getSpec();
29+
var spec = clusterConnection.getSpec();
2830
if (spec.getAdminSecretRef() != null) {
2931
return getSecretRefCredentials(
3032
kubernetesClient,
31-
clusterConnection.getSpec().getAdminSecretRef(),
33+
spec.getAdminSecretRef(),
3234
clusterConnection.getMetadata().getNamespace()
3335
);
34-
3536
} else if (spec.getAdminSecretFileRef() != null) {
3637
return getSecretFileRefCredentials(spec.getAdminSecretFileRef());
3738
}
3839

3940
throw new IllegalStateException("Exactly one of 'adminSecretRef' or 'adminSecretFileRef' must be provided");
40-
4141
}
4242

43-
public Credentials getSecretFileRefCredentials(ResourceFileRef fileRef) {
43+
public Credentials getSecretFileRefCredentials(FileRef fileRef) {
4444
var path = Path.of(fileRef.getPath());
4545

4646
if (!Files.exists(path)) {
47-
throw new IllegalStateException("AWS Secrets Manager file not found [path=%s]".formatted(path));
47+
throw new IllegalStateException("Credential file not found [path=%s]".formatted(path));
4848
}
4949

5050
try {
5151
var content = Files.readString(path);
52-
var objectMapper = new ObjectMapper();
53-
var json = objectMapper.readTree(content);
52+
var json = OBJECT_MAPPER.readTree(content);
5453

5554
var usernameNode = json.get(SECRET_DATA_BASIC_AUTH_USERNAME_KEY);
5655
var username = usernameNode != null && !usernameNode.isNull()
5756
? usernameNode.asText()
5857
: null;
58+
if (username == null) {
59+
throw new IllegalStateException("Credential file is missing required field '%s' [path=%s]".formatted(
60+
SECRET_DATA_BASIC_AUTH_USERNAME_KEY,
61+
path
62+
));
63+
}
5964

6065
var passwordNode = json.get(SECRET_DATA_BASIC_AUTH_PASSWORD_KEY);
6166
if (passwordNode == null || passwordNode.isNull()) {
62-
throw new IllegalStateException("AWS Secrets Manager file is missing required field '%s' [path=%s]".formatted(
67+
throw new IllegalStateException("Credential file is missing required field '%s' [path=%s]".formatted(
6368
SECRET_DATA_BASIC_AUTH_PASSWORD_KEY,
6469
path
6570
));
6671
}
6772

6873
return new Credentials(username, passwordNode.asText());
6974
} catch (IOException e) {
70-
throw new IllegalStateException("Failed to read AWS Secrets Manager file [path=%s]".formatted(path), e);
75+
throw new IllegalStateException("Failed to read Credential file [path=%s]".formatted(path), e);
7176
}
7277
}
7378

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ public CloseableDSLContext getDSLContext(
3434
ClusterConnection clusterConnection,
3535
String database
3636
) throws DataAccessException {
37-
var credentials = kubernetesService.getSecretRefCredentials(
37+
var credentials = kubernetesService.getAdminCredentials(
3838
kubernetesClient,
3939
clusterConnection
4040
);

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

Lines changed: 0 additions & 34 deletions
This file was deleted.

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

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,13 @@
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;
8+
import it.aboutbits.postgresql.core.FileRef;
99
import it.aboutbits.postgresql.core.ResourceRef;
1010
import it.aboutbits.postgresql.core.schema_customizer.HostCustomizer;
1111
import lombok.Getter;
1212
import lombok.Setter;
1313
import org.jspecify.annotations.NullMarked;
14+
import org.jspecify.annotations.Nullable;
1415

1516
import java.util.HashMap;
1617
import java.util.Map;
@@ -44,10 +45,10 @@ public class ClusterConnectionSpec {
4445
private String database = "postgres";
4546

4647
@io.fabric8.generator.annotation.Nullable
47-
private ResourceRef adminSecretRef;
48+
private @Nullable ResourceRef adminSecretRef;
4849

4950
@io.fabric8.generator.annotation.Nullable
50-
private ResourceFileRef adminSecretFileRef;
51+
private @Nullable FileRef adminSecretFileRef;
5152

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

operator/src/test/java/it/aboutbits/postgresql/_support/testdata/persisted/creator/ClusterConnectionCreate.java

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import io.fabric8.kubernetes.client.KubernetesClient;
55
import it.aboutbits.postgresql._support.testdata.base.TestDataCreator;
66
import it.aboutbits.postgresql._support.testdata.persisted.Given;
7+
import it.aboutbits.postgresql.core.FileRef;
78
import it.aboutbits.postgresql.core.ResourceRef;
89
import it.aboutbits.postgresql.crd.clusterconnection.ClusterConnection;
910
import it.aboutbits.postgresql.crd.clusterconnection.ClusterConnectionSpec;
@@ -41,6 +42,11 @@ public class ClusterConnectionCreate extends TestDataCreator<ClusterConnection>
4142

4243
private @Nullable ResourceRef withAdminSecretRef;
4344

45+
private @Nullable FileRef withAdminSecretFileRef;
46+
47+
@Setter(AccessLevel.NONE)
48+
private boolean withoutAdminSecret = false;
49+
4450
private @Nullable String withApplicationName;
4551

4652
public ClusterConnectionCreate(
@@ -61,6 +67,16 @@ public ClusterConnectionCreate withoutNamespace() {
6167
return this;
6268
}
6369

70+
public ClusterConnectionCreate withAdminSecretFileRef(FileRef fileRef) {
71+
this.withAdminSecretFileRef = fileRef;
72+
return this;
73+
}
74+
75+
public ClusterConnectionCreate withoutAdminSecret() {
76+
this.withoutAdminSecret = true;
77+
return this;
78+
}
79+
6480
@Override
6581
protected ClusterConnection create(int index) {
6682
// given
@@ -81,6 +97,9 @@ protected ClusterConnection create(int index) {
8197
spec.setPort(getPort());
8298
spec.setDatabase(getDatabase());
8399
spec.setAdminSecretRef(getAdminSecretRef());
100+
if (withAdminSecretFileRef != null) {
101+
spec.setAdminSecretFileRef(withAdminSecretFileRef);
102+
}
84103
spec.setParameters(getParameters());
85104

86105
item.setSpec(spec);
@@ -116,7 +135,7 @@ protected ClusterConnection create(int index) {
116135
}
117136

118137
private String getName() {
119-
if (withName != null) {
138+
if (withName != null) {
120139
return withName;
121140
}
122141

@@ -154,7 +173,11 @@ private String getDatabase() {
154173
return withDatabase;
155174
}
156175

157-
private ResourceRef getAdminSecretRef() {
176+
private @Nullable ResourceRef getAdminSecretRef() {
177+
if (withoutAdminSecret) {
178+
return null;
179+
}
180+
158181
if (withAdminSecretRef != null) {
159182
return withAdminSecretRef;
160183
}

0 commit comments

Comments
 (0)