Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
17 changes: 17 additions & 0 deletions .hazyforge/clusters/anvil-primaris/namespace/chat2db/deploy.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,23 @@ ai:
models:
- gpt-5.4

datasourceBootstrap:
enabled: true
rolloutRevision: "1"
managementKey: hazytrade-production-readonly
alias: Hazy Trade Production (read only)
type: POSTGRESQL
host: 5.161.253.191
port: 5432
database: hazytrade
user: chat2db_hazytrade_ro
authenticationType: "1"
environmentId: 2
sslMode: verify-full
existingSecret: chat2db-hazytrade-datasource
passwordKey: password
caKey: ca.crt

resources:
requests:
cpu: 500m
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
apiVersion: external-secrets.io/v1
kind: ExternalSecret
metadata:
name: chat2db-hazytrade-datasource
namespace: chat2db
spec:
refreshInterval: 1h
secretStoreRef:
kind: ClusterSecretStore
name: azurekv-cluster-secret-store
target:
name: chat2db-hazytrade-datasource
creationPolicy: Owner
deletionPolicy: Delete
data:
- secretKey: password
remoteRef:
key: secret/chat2db-hazytrade-ro-password
- secretKey: ca.crt
remoteRef:
key: secret/hazy-trade-postgres-ca-crt
21 changes: 21 additions & 0 deletions HAZYFORGE.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,27 @@ identity allowlists are not supported by this deployment.
- The dedicated ZITADEL client ID and secret, oauth2-proxy cookie key, and exact
allowed subject are projected from Azure Key Vault by External Secrets.

## Managed PostgreSQL datasource

The chart can reconcile one PostgreSQL datasource at application startup with
`datasourceBootstrap.enabled`. Non-secret connection identity is supplied in
Helm values. The password and CA certificate are read from files in a dedicated
existing Kubernetes Secret; they must never be placed directly in values.

The datasource is owned by an explicit management key stored with its local
record. Reconciliation never adopts an unmarked datasource, never treats an
alias as ownership, and refuses an alias collision rather than changing a
user-created connection. New and changed configuration is connection-tested
before it is persisted, and the password uses the same AES-GCM encryption key
as manually created Community datasources.

The PostgreSQL account is the write-safety boundary. Give it only `CONNECT`,
schema `USAGE`, and the required `SELECT` grants; Chat2DB does not impose a
query-level read-only policy. Because this cluster has no Secret-reload
controller, rotate the mounted credential together with a declarative pod
rollout by bumping `datasourceBootstrap.rolloutRevision`, so startup
reconciliation applies the new password.

## Image

`.github/workflows/hazyforge-image.yml` builds the frontend and backend from
Expand Down
53 changes: 53 additions & 0 deletions charts/chat2db/templates/deployment.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ spec:
metadata:
labels:
{{- include "chat2db.selectorLabels" . | nindent 8 }}
{{- with .Values.datasourceBootstrap.rolloutRevision }}
annotations:
hazyforge.io/datasource-rollout-revision: {{ . | quote }}
{{- end }}
spec:
automountServiceAccountToken: {{ .Values.automountServiceAccountToken }}
securityContext:
Expand All @@ -38,6 +42,34 @@ spec:
value: {{ .Values.encryptionKey.mountPath | quote }}
- name: JAVA_TOOL_OPTIONS
value: "-XX:MaxRAMPercentage=75.0 -XX:+ExitOnOutOfMemoryError"
{{- if .Values.datasourceBootstrap.enabled }}
- name: CHAT2DB_BOOTSTRAP_DATASOURCE_ENABLED
value: "true"
- name: CHAT2DB_BOOTSTRAP_DATASOURCE_MANAGEMENT_KEY
value: {{ required "datasourceBootstrap.managementKey is required when datasource bootstrap is enabled" .Values.datasourceBootstrap.managementKey | quote }}
- name: CHAT2DB_BOOTSTRAP_DATASOURCE_ALIAS
value: {{ required "datasourceBootstrap.alias is required when datasource bootstrap is enabled" .Values.datasourceBootstrap.alias | quote }}
- name: CHAT2DB_BOOTSTRAP_DATASOURCE_TYPE
value: {{ required "datasourceBootstrap.type is required when datasource bootstrap is enabled" .Values.datasourceBootstrap.type | quote }}
- name: CHAT2DB_BOOTSTRAP_DATASOURCE_HOST
value: {{ required "datasourceBootstrap.host is required when datasource bootstrap is enabled" .Values.datasourceBootstrap.host | quote }}
- name: CHAT2DB_BOOTSTRAP_DATASOURCE_PORT
value: {{ .Values.datasourceBootstrap.port | quote }}
- name: CHAT2DB_BOOTSTRAP_DATASOURCE_DATABASE
value: {{ required "datasourceBootstrap.database is required when datasource bootstrap is enabled" .Values.datasourceBootstrap.database | quote }}
- name: CHAT2DB_BOOTSTRAP_DATASOURCE_USER
value: {{ required "datasourceBootstrap.user is required when datasource bootstrap is enabled" .Values.datasourceBootstrap.user | quote }}
- name: CHAT2DB_BOOTSTRAP_DATASOURCE_AUTHENTICATION_TYPE
value: {{ .Values.datasourceBootstrap.authenticationType | quote }}
- name: CHAT2DB_BOOTSTRAP_DATASOURCE_ENVIRONMENT_ID
value: {{ .Values.datasourceBootstrap.environmentId | quote }}
- name: CHAT2DB_BOOTSTRAP_DATASOURCE_SSL_MODE
value: {{ .Values.datasourceBootstrap.sslMode | quote }}
- name: CHAT2DB_BOOTSTRAP_DATASOURCE_PASSWORD_FILE
value: {{ .Values.datasourceBootstrap.passwordMountPath | quote }}
- name: CHAT2DB_BOOTSTRAP_DATASOURCE_CA_FILE
value: {{ .Values.datasourceBootstrap.caMountPath | quote }}
{{- end }}
{{- with .Values.cors.allowedOrigin }}
- name: CHAT2DB_WEB_CORS_ALLOWED_ORIGIN
value: {{ . | quote }}
Expand Down Expand Up @@ -95,6 +127,16 @@ spec:
mountPath: {{ .Values.encryptionKey.mountPath | quote }}
subPath: {{ .Values.encryptionKey.secretKey | quote }}
readOnly: true
{{- if .Values.datasourceBootstrap.enabled }}
- name: datasource-bootstrap
mountPath: {{ .Values.datasourceBootstrap.passwordMountPath | quote }}
subPath: password
readOnly: true
- name: datasource-bootstrap
mountPath: {{ .Values.datasourceBootstrap.caMountPath | quote }}
subPath: ca.crt
readOnly: true
{{- end }}
- name: tmp
mountPath: /tmp
resources:
Expand All @@ -107,6 +149,17 @@ spec:
secret:
secretName: {{ .Values.encryptionKey.existingSecret }}
defaultMode: 0440
{{- if .Values.datasourceBootstrap.enabled }}
- name: datasource-bootstrap
secret:
secretName: {{ required "datasourceBootstrap.existingSecret is required when datasource bootstrap is enabled" .Values.datasourceBootstrap.existingSecret }}
defaultMode: 0440
items:
- key: {{ .Values.datasourceBootstrap.passwordKey | quote }}
path: password
- key: {{ .Values.datasourceBootstrap.caKey | quote }}
path: ca.crt
{{- end }}
- name: tmp
emptyDir:
sizeLimit: 1Gi
Expand Down
39 changes: 39 additions & 0 deletions charts/chat2db/test/render-managed-datasource.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
#!/usr/bin/env bash
set -euo pipefail

chart_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
render_dir="$(mktemp -d)"
trap 'rm -rf "${render_dir}"' EXIT

helm lint "${chart_dir}"
helm template chat2db "${chart_dir}" >"${render_dir}/default.yaml"

if grep -q 'CHAT2DB_BOOTSTRAP_DATASOURCE' "${render_dir}/default.yaml"; then
echo "disabled datasource bootstrap unexpectedly rendered configuration" >&2
exit 1
fi

helm template chat2db "${chart_dir}" \
--set datasourceBootstrap.enabled=true \
--set datasourceBootstrap.rolloutRevision=1 \
--set datasourceBootstrap.managementKey=test-readonly \
--set 'datasourceBootstrap.alias=Test Read Only' \
--set datasourceBootstrap.host=postgres.example.internal \
--set datasourceBootstrap.database=app \
--set datasourceBootstrap.user=reader \
--set datasourceBootstrap.existingSecret=chat2db-database \
>"${render_dir}/enabled.yaml"

grep -q 'name: CHAT2DB_BOOTSTRAP_DATASOURCE_ENABLED' "${render_dir}/enabled.yaml"
grep -q 'hazyforge.io/datasource-rollout-revision: "1"' "${render_dir}/enabled.yaml"
grep -q 'secretName: chat2db-database' "${render_dir}/enabled.yaml"
grep -q 'defaultMode: 0440' "${render_dir}/enabled.yaml"
grep -q 'mountPath: "/run/secrets/chat2db-datasource/password"' "${render_dir}/enabled.yaml"
grep -q 'mountPath: "/run/secrets/chat2db-datasource/ca.crt"' "${render_dir}/enabled.yaml"

if helm template chat2db "${chart_dir}" \
--set datasourceBootstrap.enabled=true \
>"${render_dir}/invalid.yaml" 2>"${render_dir}/invalid.err"; then
echo "enabled datasource bootstrap rendered without required values" >&2
exit 1
fi
23 changes: 23 additions & 0 deletions charts/chat2db/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,29 @@ encryptionKey:
secretKey: encryption.key
mountPath: /run/secrets/chat2db-community-encryption.key

# Optional, controller-owned PostgreSQL datasource reconciled at application
# startup. Credentials and the trust root must come from an existing Secret.
datasourceBootstrap:
enabled: false
# Bump whenever the externally managed password or CA changes so Kubernetes
# replaces the pod and startup reconciliation reads the new secret files.
rolloutRevision: ""
managementKey: ""
alias: ""
type: POSTGRESQL
host: ""
port: 5432
database: ""
user: ""
authenticationType: "1"
environmentId: 2
sslMode: verify-full
existingSecret: ""
passwordKey: password
caKey: ca.crt
passwordMountPath: /run/secrets/chat2db-datasource/password
caMountPath: /run/secrets/chat2db-datasource/ca.crt

resources:
requests:
cpu: 500m
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,4 +92,10 @@ public class DataSource {


private Long spaceId;

/** Identifies the controller that owns this datasource, when any. */
private String managedBy;

/** Stable controller-scoped identity used for idempotent reconciliation. */
private String managedKey;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package ai.chat2db.community.domain.api.model.datasource;

import lombok.AllArgsConstructor;
import lombok.Data;

@Data
@AllArgsConstructor(staticName = "of")
public class ManagedDataSourceReconcileResult {

public enum Action {
CREATED,
UPDATED,
UNCHANGED
}

private Action action;

private Long dataSourceId;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package ai.chat2db.community.domain.api.model.datasource;

import lombok.Data;

/** Desired configuration for one controller-managed datasource. */
@Data
public class ManagedDataSourceSpec {

private String managementKey;

private String alias;

private String type;

private String host;

private String port;

private String database;

private String user;

private String password;

private String authenticationType;

private Long environmentId;

private String url;
}
Original file line number Diff line number Diff line change
Expand Up @@ -71,4 +71,8 @@ public class WorkspaceDataSource {
private String storageType;

private Long spaceId;

private String managedBy;

private String managedKey;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package ai.chat2db.community.domain.api.service.db;

import ai.chat2db.community.domain.api.model.datasource.ManagedDataSourceReconcileResult;
import ai.chat2db.community.domain.api.model.datasource.ManagedDataSourceSpec;

public interface IManagedDataSourceReconciler {

ManagedDataSourceReconcileResult reconcile(ManagedDataSourceSpec spec);
}
Loading
Loading