Skip to content

Commit c3c525f

Browse files
authored
Merge pull request #855 from MetaCell/feature/postgres_operator_params
feat: Add postgres operator parameters
2 parents ffbdacd + d6f416c commit c3c525f

9 files changed

Lines changed: 117 additions & 4 deletions

File tree

deployment-configuration/helm/templates/auto-database-postgres-operator.yaml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,14 @@ metadata:
2424
spec:
2525
instances: {{ .app.harness.database.postgres.instances | default 1 }}
2626

27+
{{- with .app.harness.database.postgres.parameters }}
28+
postgresql:
29+
parameters:
30+
{{- range $key, $value := . }}
31+
{{ $key }}: {{ $value | quote }}
32+
{{- end }}
33+
{{- end }}
34+
2735
inheritedMetadata:
2836
labels:
2937
app: {{ .app.harness.database.name | quote }}

deployment-configuration/value-template.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,8 @@ harness:
115115
# -- CIDR(s) allowed for CNPG pods to reach the Kubernetes API server (port 443).
116116
# -- Resolved automatically at deploy time via cluster lookup. Set explicitly only as a fallback for helm-template or air-gapped use.
117117
apiServerCidr: []
118+
# -- PostgreSQL configuration parameters for CloudNative-PG clusters (operator: true). Values must be strings.
119+
parameters: {}
118120
ports:
119121
- name: http
120122
port: 5432

docs/applications/databases.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ harness
8787
operator: false
8888
instances: 1
8989
apiServerCidr: []
90+
parameters: {}
9091
ports:
9192
- name: http
9293
port: 5432
@@ -109,6 +110,8 @@ helm install cnpg cloudnative-pg/cloudnative-pg
109110

110111
`apiServerCidr`: List of CIDRs allowed for CNPG database pods to reach the Kubernetes API server on port 443. **Resolved automatically at deploy time** by looking up the `kubernetes` Service and Endpoints in the `default` namespace. The explicit list is only used as a fallback when lookup returns nothing (e.g. `helm template` dry-run). Leave empty (`[]`) for auto-detection; set explicitly only for air-gapped or restricted environments.
111112

113+
`parameters`: Optional map of PostgreSQL configuration parameters rendered to CloudNative-PG as `spec.postgresql.parameters` when `operator: true`. Values must be strings, for example `max_connections: "200"`. CloudNative-PG rejects parameters that are fixed or managed by the operator.
114+
112115

113116
#### Neo4j
114117

@@ -209,4 +212,3 @@ Further reading: [MongoDB archiving & compression](https://www.mongodb.com/blog/
209212
Further reading: [pg_dumpall docs](https://www.postgresql.org/docs/10/app-pg-dumpall.html)
210213

211214

212-

docs/model/DatabaseConfig.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ Name | Type | Description | Notes
1212
**operator** | **bool** | Use the CloudNative-PG operator instead of a plain Deployment (postgres only) | [optional]
1313
**instances** | **int** | Number of PostgreSQL instances managed by the CNPG operator (only used when operator is true) | [optional]
1414
**api_server_cidr** | **List[str]** | CIDR(s) allowed for CNPG pods to reach the Kubernetes API server (port 443). Override with your cluster API-server or service CIDR. | [optional]
15+
**parameters** | **Dict[str, str]** | PostgreSQL configuration parameters passed to CloudNative-PG as spec.postgresql.parameters (postgres operator only). Values must be strings. | [optional]
1516
**initialdb** | **str** | Initial database name (postgres only) | [optional]
1617
**args** | **List[str]** | Additional command-line arguments passed to the database server process (postgres only) | [optional]
1718

libraries/models/api/openapi.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -962,6 +962,11 @@ components:
962962
type: array
963963
items:
964964
type: string
965+
parameters:
966+
description: 'PostgreSQL configuration parameters passed to CloudNative-PG as spec.postgresql.parameters (postgres operator only). Values must be strings.'
967+
type: object
968+
additionalProperties:
969+
type: string
965970
initialdb:
966971
description: 'Initial database name (postgres only)'
967972
type: string

libraries/models/cloudharness_model/models/database_config.py

Lines changed: 3 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

libraries/models/docs/DatabaseConfig.md

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

libraries/models/test/test_deserialize.py

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
from os.path import join, dirname as dn, realpath
33
import oyaml as yaml
44

5-
from cloudharness_model import HarnessMainConfig, ApplicationConfig, User, ApplicationHarnessConfig, CDCEvent, ApplicationTestConfig
5+
from cloudharness_model import HarnessMainConfig, ApplicationConfig, User, ApplicationHarnessConfig, CDCEvent, ApplicationTestConfig, DatabaseConfig
66

77
HERE = dn(realpath(__file__))
88

@@ -37,6 +37,27 @@ def test_camelcase():
3737
assert u.last_name == "a"
3838
assert u["lastName"] == "a"
3939

40+
41+
def test_database_config_parameters_round_trip():
42+
config = DatabaseConfig.from_dict({
43+
"image": "postgres:17",
44+
"operator": True,
45+
"parameters": {
46+
"max_connections": "200",
47+
"shared_buffers": "1GB",
48+
},
49+
})
50+
51+
assert config.parameters == {
52+
"max_connections": "200",
53+
"shared_buffers": "1GB",
54+
}
55+
assert config.to_dict()["parameters"] == {
56+
"max_connections": "200",
57+
"shared_buffers": "1GB",
58+
}
59+
60+
4061
def test_robustness():
4162
d = {'aliases': [], 'database': {'auto': True, 'mongo': {'image': 'mongo:5', 'ports': [{'name': 'http', 'port': 27017}]}, 'name': 'keycloak-postgres', 'neo4j': {'dbms_security_auth_enabled': 'false', 'image': 'neo4j:4.1.9', 'memory': {'heap': {'initial': '64M', 'max': '128M'}, 'pagecache': {'size': '64M'}, 'size': '256M'}, 'ports': [{'name': 'http', 'port': 7474}, {'name': 'bolt', 'port': 7687}]}, 'pass': 'password', 'postgres': {'image': 'postgres:10.4', 'initialdb': 'auth_db', 'ports': [{'name': 'http', 'port': 5432}]}, 'resources': {'limits': {'cpu': '1000m', 'memory': '2Gi'}, 'requests': {'cpu': '100m', 'memory': '512Mi'}}, 'size': '2Gi', 'type': 'postgres', 'user': 'user'}, 'dependencies': {'build': [], 'hard': [], 'soft': []}, 'deployment': {'auto': True, 'image': 'osb/accounts:3e02a15477b4696ed554e08cedf4109c67908cbe6b03331072b5b73e83b4fc2b', 'name': 'accounts', 'port': 8080, 'replicas': 1, 'resources': {'limits': {'cpu': '500m', 'memory': '1024Mi'}, 'requests': {'cpu': '10m', 'memory': '512Mi'}}}, 'domain': None, 'env': [{'name': 'KEYCLOAK_IMPORT', 'value': '/tmp/realm.json'},
4263
{'name': 'KEYCLOAK_USER', 'value': 'admin'}, {'name': 'KEYCLOAK_PASSWORD', 'value': 'metacell'}, {'name': 'PROXY_ADDRESS_FORWARDING', 'value': 'true'}, {'name': 'DB_VENDOR', 'value': 'POSTGRES'}, {'name': 'DB_ADDR', 'value': 'keycloak-postgres'}, {'name': 'DB_DATABASE', 'value': 'auth_db'}, {'name': 'DB_USER', 'value': 'user'}, {'name': 'DB_PASSWORD', 'value': 'password'}, {'name': 'JAVA_OPTS', 'value': '-server -Xms64m -Xmx896m -XX:MetaspaceSize=96M -XX:MaxMetaspaceSize=256m -Djava.net.preferIPv4Stack=true -Djboss.modules.system.pkgs=org.jboss.byteman -Djava.awt.headless=true --add-exports=java.base/sun.nio.ch=ALL-UNNAMED --add-exports=jdk.unsupported/sun.misc=ALL-UNNAMED --add-exports=jdk.unsupported/sun.reflect=ALL-UNNAMED'}], 'name': 'accounts', 'readinessProbe': {'path': '/realms/master'}, 'resources': [{'dst': '/tmp/realm.json', 'name': 'realm-config', 'src': 'realm.json'}], 'secrets': {}, 'secured': False, 'service': {'auto': True, 'name': 'accounts', 'port': 8080}, 'subdomain': 'accounts', 'uri_role_mapping': [{'roles': ['administrator'], 'uri': '/*'}], 'use_services': []}
@@ -161,4 +182,4 @@ def internal_data(self, value):
161182

162183

163184
if __name__ == "__main__":
164-
test_property_access()
185+
test_property_access()

tools/deployment-cli-tools/tests/test_helm.py

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
from ch_cli_tools.configurationgenerator import *
33
from ch_cli_tools.preprocessing import preprocess_build_overrides, generate_hash_based_image_tags
44
import pytest
5+
import shutil
6+
import subprocess
57

68
HERE = os.path.dirname(os.path.realpath(__file__))
79
RESOURCES = os.path.join(HERE, 'resources')
@@ -12,6 +14,24 @@ def exists(path):
1214
return path.exists()
1315

1416

17+
def render_helm_chart(chart_path):
18+
completed = subprocess.run(
19+
["helm", "template", str(chart_path)],
20+
check=True,
21+
stdout=subprocess.PIPE,
22+
stderr=subprocess.PIPE,
23+
text=True,
24+
)
25+
return [manifest for manifest in yaml.safe_load_all(completed.stdout) if manifest]
26+
27+
28+
def find_manifest(manifests, kind, name):
29+
for manifest in manifests:
30+
if manifest.get("kind") == kind and manifest.get("metadata", {}).get("name") == name:
31+
return manifest
32+
raise AssertionError(f"Could not find {kind}/{name}")
33+
34+
1535
def test_collect_helm_values(tmp_path):
1636
out_folder = tmp_path / 'test_collect_helm_values'
1737
values = create_helm_chart([CLOUDHARNESS_ROOT, RESOURCES], output_path=out_folder, include=['samples', 'myapp'],
@@ -294,6 +314,57 @@ def test_clear_unused_dbconfig(tmp_path):
294314
assert db_config['postgres'] is None
295315

296316

317+
def test_cnpg_postgres_parameters_render_only_when_set(tmp_path):
318+
out_folder = tmp_path / 'test_cnpg_postgres_parameters_render_only_when_set'
319+
create_helm_chart([CLOUDHARNESS_ROOT, RESOURCES], output_path=out_folder, domain="my.local",
320+
env='withpostgres', local=False, include=["myapp"], exclude=["legacy"])
321+
322+
helm_path = out_folder / HELM_CHART_PATH
323+
shutil.rmtree(helm_path / 'charts')
324+
values_path = helm_path / 'values.yaml'
325+
with open(values_path, 'r') as values_file:
326+
values = yaml.safe_load(values_file)
327+
postgres = values['apps']['myapp']['harness']['database']['postgres']
328+
postgres['operator'] = True
329+
postgres['parameters'] = {
330+
# Simulate generated YAML values where on/off can be parsed as booleans before Helm renders the chart.
331+
'autovacuum': True,
332+
'max_connections': '200',
333+
'shared_buffers': '1GB',
334+
'synchronous_commit': True,
335+
'track_io_timing': False,
336+
}
337+
with open(values_path, 'w') as values_file:
338+
yaml.safe_dump(values, values_file)
339+
340+
manifests = render_helm_chart(helm_path)
341+
db_name = values['apps']['myapp']['harness']['database']['name']
342+
cluster = find_manifest(manifests, 'Cluster', db_name)
343+
assert cluster['spec']['postgresql']['parameters'] == {
344+
'autovacuum': 'true',
345+
'max_connections': '200',
346+
'shared_buffers': '1GB',
347+
'synchronous_commit': 'true',
348+
'track_io_timing': 'false',
349+
}
350+
351+
postgres['parameters'] = {}
352+
with open(values_path, 'w') as values_file:
353+
yaml.safe_dump(values, values_file)
354+
355+
manifests = render_helm_chart(helm_path)
356+
cluster = find_manifest(manifests, 'Cluster', db_name)
357+
assert 'postgresql' not in cluster['spec']
358+
359+
postgres.pop('parameters')
360+
with open(values_path, 'w') as values_file:
361+
yaml.safe_dump(values, values_file)
362+
363+
manifests = render_helm_chart(helm_path)
364+
cluster = find_manifest(manifests, 'Cluster', db_name)
365+
assert 'postgresql' not in cluster['spec']
366+
367+
297368
def test_clear_all_dbconfig_if_nodb(tmp_path):
298369
out_folder = tmp_path / 'test_clear_all_dbconfig_if_nodb'
299370

0 commit comments

Comments
 (0)