-
Notifications
You must be signed in to change notification settings - Fork 3
Add adminSecretFileRef as alternative to adminSecretRef for file-based credentials in ClusterConnection
#60
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 1 commit
0161244
263b294
3f50c53
6110958
b335a78
544360c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -60,3 +60,6 @@ gradle-app.setting | |
|
|
||
| # Quinoa | ||
| .quinoa/ | ||
|
|
||
| generated/out/ | ||
| operator/out/ | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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). | ||
|
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. | ||
|
|
@@ -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) | ||
|
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: | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 Fix: We will add
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 | ||
| ``` | ||
|
|
||
|  | ||
|
|
||
| ## 3. Create a Role | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||
|---|---|---|---|---|---|---|---|---|
|
|
@@ -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 | ||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 Put it in your own
Suggested change
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||||||
|
|
@@ -19,11 +24,51 @@ public Credentials getSecretRefCredentials( | |||||
| KubernetesClient kubernetesClient, | ||||||
| ClusterConnection clusterConnection | ||||||
| ) { | ||||||
| return getSecretRefCredentials( | ||||||
| kubernetesClient, | ||||||
| clusterConnection.getSpec().getAdminSecretRef(), | ||||||
| clusterConnection.getMetadata().getNamespace() | ||||||
| ); | ||||||
| ClusterConnectionSpec spec = clusterConnection.getSpec(); | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
|
||||||
| if (spec.getAdminSecretRef() != null) { | ||||||
|
ThoSap marked this conversation as resolved.
|
||||||
| return getSecretRefCredentials( | ||||||
| kubernetesClient, | ||||||
| clusterConnection.getSpec().getAdminSecretRef(), | ||||||
| clusterConnection.getMetadata().getNamespace() | ||||||
| ); | ||||||
|
|
||||||
| } else if (spec.getAdminSecretFileRef() != null) { | ||||||
|
ThoSap marked this conversation as resolved.
Outdated
|
||||||
| return getSecretFileRefCredentials(spec.getAdminSecretFileRef()); | ||||||
| } | ||||||
|
|
||||||
| throw new IllegalStateException("Exactly one of 'adminSecretRef' or 'adminSecretFileRef' must be provided"); | ||||||
|
|
||||||
| } | ||||||
|
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)); | ||||||
|
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; | ||||||
|
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( | ||||||
|
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); | ||||||
|
ThoSap marked this conversation as resolved.
Outdated
|
||||||
| } | ||||||
| } | ||||||
|
|
||||||
| public Credentials getSecretRefCredentials( | ||||||
|
|
||||||
| 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. | ||
|
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. | ||
|
ThoSap marked this conversation as resolved.
Outdated
|
||
| /// | ||
| /// ### Example usage in a CR manifest | ||
| /// | ||
| /// ```yaml | ||
| /// spec: | ||
| /// adminSecretFileRef: | ||
| /// path: "/mnt/db-password" | ||
|
ThoSap marked this conversation as resolved.
Outdated
|
||
| /// ``` | ||
| @Getter | ||
| @Setter | ||
| @NullMarked | ||
| public class ResourceFileRef { | ||
|
ThoSap marked this conversation as resolved.
Outdated
|
||
| /// The path to the file inside the AWS Secrets Manager secret. | ||
|
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 = ""; | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.