qontract-reconcile 0.10.1rc402__py3-none-any.whl → 0.10.1rc403__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: qontract-reconcile
3
- Version: 0.10.1rc402
3
+ Version: 0.10.1rc403
4
4
  Summary: Collection of tools to reconcile services with their desired state as defined in the app-interface DB.
5
5
  Home-page: https://github.com/app-sre/qontract-reconcile
6
6
  Author: Red Hat App-SRE Team
@@ -7,7 +7,7 @@ reconcile/aws_iam_password_reset.py,sha256=NwErtrqgBiXr7eGCAHdtGGOx0S7-4JnSc29Ie
7
7
  reconcile/aws_support_cases_sos.py,sha256=i6bSWnlH9fh14P14PjVhFLwNl-q3fD733_rXKM_O51c,2992
8
8
  reconcile/blackbox_exporter_endpoint_monitoring.py,sha256=W_VJagnsJR1v5oqjlI3RJJE0_nhtJ0m81RS8zWA5u5c,3538
9
9
  reconcile/checkpoint.py,sha256=figtZRuWUvdpdSnkhAqeGvO5dI02TT6J3heyeFhlwqM,5016
10
- reconcile/cli.py,sha256=AYkCf2D4jENSkiJmpNzc6cVv_mlcIUc7k7Mli3vgtZA,79614
10
+ reconcile/cli.py,sha256=QEKbka04dATCDawHz2_Xs5tkGWHQ7Hs-YZ-E-JzOQ84,79955
11
11
  reconcile/closedbox_endpoint_monitoring_base.py,sha256=GmEdDSp9yBnwpzzrla6VJfhOZd_qxYh-xtIN5bXjOBo,4909
12
12
  reconcile/cluster_deployment_mapper.py,sha256=2Ah-nu-Mdig0pjuiZl_XLrmVAjYzFjORR3dMlCgkmw0,2352
13
13
  reconcile/dashdotdb_base.py,sha256=13s9B8iIqSwu-rS-95QbIMPue_Gli3YMNBct5QjmrVo,4525
@@ -15,6 +15,7 @@ reconcile/dashdotdb_cso.py,sha256=FoXrWGpOwXG5jf0eklN84tjJVUAYzKat7rtq_28JMlQ,36
15
15
  reconcile/dashdotdb_dora.py,sha256=FINH-8dU3_r8EgUOMiF_fbxD9fKs6LFDxe6rufQ9XcM,17214
16
16
  reconcile/dashdotdb_dvo.py,sha256=YXqpI6fBQAql-ybGI0grj9gWMzmKiAvPE__pNju6obk,8996
17
17
  reconcile/dashdotdb_slo.py,sha256=oiWMehzU5wTk2AkYmOzD34vCvIFKCzuy0WDLrA3Kjzc,5823
18
+ reconcile/database_access_manager.py,sha256=2I__xR2VzvbF5VdRXCDlFtKZZK7f_-Zmm2BHXaRxc4E,20706
18
19
  reconcile/dynatrace_token_provider.py,sha256=HWItJ_LavPcUJlpYz5fmfnfOpP2-0Qjkar0EAzBJ5Cw,16602
19
20
  reconcile/email_sender.py,sha256=-5L-Ag_jaEYSzYRoMr52KQBRXz1E8yx9GqLbg2X4XFU,3533
20
21
  reconcile/gabi_authorized_users.py,sha256=rCosZv8Iu9jhWG88YiwK-gftX475aJ1R-PYIJYp_svY,4342
@@ -277,6 +278,7 @@ reconcile/gql_definitions/terraform_cloudflare_users/terraform_cloudflare_roles.
277
278
  reconcile/gql_definitions/terraform_repo/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
278
279
  reconcile/gql_definitions/terraform_repo/terraform_repo.py,sha256=H2jQhte5Db3orab8RsTUBV-Zi9KgESxGOB835CocqSY,3108
279
280
  reconcile/gql_definitions/terraform_resources/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
281
+ reconcile/gql_definitions/terraform_resources/database_access_manager.py,sha256=rw-6tQ05lzeE30hnLEuvLJR6sdTWZJapgmMZKixVgzE,4881
280
282
  reconcile/gql_definitions/terraform_resources/terraform_resources_namespaces.py,sha256=edR1d8taPIlQH-4OSc9uaGyluDi-3KTsLXpO28REoDU,41349
281
283
  reconcile/gql_definitions/terraform_tgw_attachments/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
282
284
  reconcile/gql_definitions/terraform_tgw_attachments/aws_accounts.py,sha256=bxW6Wg0uE1lL9KWz1Kw9BR5QV658Tq_zbhHLDDumlgU,2606
@@ -357,6 +359,7 @@ reconcile/test/test_checkpoint.py,sha256=sbDtqTbfw5yMZ_mCltMXxkyyGueVLGUjTDtcWhP
357
359
  reconcile/test/test_cli.py,sha256=qx_iBwh4Z-YkK3sbjK1wEziPTgn060EN-baf9DNvR3k,1096
358
360
  reconcile/test/test_closedbox_endpoint_monitoring.py,sha256=isMHYwRWMFARU2nbJgbl69kD6H0eA86noCM4MPVI1fo,7151
359
361
  reconcile/test/test_dashdotdb_dora.py,sha256=XDMdnLDvup8sSqQEynxdPhXQZAafYbd2IUo9flSRJ8I,7917
362
+ reconcile/test/test_database_access_manager.py,sha256=9P-IKPlqirpHu_vyRedZ69zR63AvJZdLDuM30qtg4F4,11384
360
363
  reconcile/test/test_gabi_authorized_users.py,sha256=6XnV5Q9inxP81ktGMVKyWucjBTUj8Imy2L0HG3YHyUE,2496
361
364
  reconcile/test/test_github_org.py,sha256=j3KeB4OnSln1gm2hidce49xdMru-j75NS3cM-AEgzZc,4511
362
365
  reconcile/test/test_github_repo_invites.py,sha256=QJ0VFk5B59rx4XtHoT6XOGWw9xRIZMen_cgtviN_Vi8,3419
@@ -619,8 +622,8 @@ tools/test/test_app_interface_metrics_exporter.py,sha256=dmEcNwZltP1rd_4DbxIYakO
619
622
  tools/test/test_qontract_cli.py,sha256=awwTHEc2DWlykuqGIYM0WOBoSL0KRnOraCLk3C7izis,1401
620
623
  tools/test/test_sd_app_sre_alert_report.py,sha256=JeLhgzpKCPgLvptwg_4ZvJHLVWKNG1T5845HXTkMBxA,1826
621
624
  tools/test/test_sre_checkpoints.py,sha256=SKqPPTl9ua0RFdSSofnoQX-JZE6dFLO3LRhfQzqtfh8,2607
622
- qontract_reconcile-0.10.1rc402.dist-info/METADATA,sha256=DU5UiE_qV8lPDI4Eir53vzO8F3AH0kFhHZ-U0cN7akQ,2347
623
- qontract_reconcile-0.10.1rc402.dist-info/WHEEL,sha256=yQN5g4mg4AybRjkgi-9yy4iQEFibGQmlz78Pik5Or-A,92
624
- qontract_reconcile-0.10.1rc402.dist-info/entry_points.txt,sha256=rTjAv28I_CHLM8ID3OPqMI_suoQ9s7tFbim4aYjn9kk,376
625
- qontract_reconcile-0.10.1rc402.dist-info/top_level.txt,sha256=l5ISPoXzt0SdR4jVdkfa7RPSKNc8zAHYWAnR-Dw8Ey8,24
626
- qontract_reconcile-0.10.1rc402.dist-info/RECORD,,
625
+ qontract_reconcile-0.10.1rc403.dist-info/METADATA,sha256=cIo20x-baQGEabysu8S0LhYBh9G8h8NeNGF0KOyJhlU,2347
626
+ qontract_reconcile-0.10.1rc403.dist-info/WHEEL,sha256=yQN5g4mg4AybRjkgi-9yy4iQEFibGQmlz78Pik5Or-A,92
627
+ qontract_reconcile-0.10.1rc403.dist-info/entry_points.txt,sha256=rTjAv28I_CHLM8ID3OPqMI_suoQ9s7tFbim4aYjn9kk,376
628
+ qontract_reconcile-0.10.1rc403.dist-info/top_level.txt,sha256=l5ISPoXzt0SdR4jVdkfa7RPSKNc8zAHYWAnR-Dw8Ey8,24
629
+ qontract_reconcile-0.10.1rc403.dist-info/RECORD,,
reconcile/cli.py CHANGED
@@ -2128,6 +2128,17 @@ def advanced_upgrade_scheduler(
2128
2128
  )
2129
2129
 
2130
2130
 
2131
+ @integration.command(short_help="Manage Databases and Database Users.")
2132
+ @click.pass_context
2133
+ def database_access_manager(ctx):
2134
+ from reconcile.database_access_manager import DatabaseAccessManagerIntegration
2135
+
2136
+ run_class_integration(
2137
+ integration=DatabaseAccessManagerIntegration(PydanticRunParams()),
2138
+ ctx=ctx.obj,
2139
+ )
2140
+
2141
+
2131
2142
  @integration.command(
2132
2143
  short_help="Export Product and Application informnation to Status Board."
2133
2144
  )
@@ -0,0 +1,621 @@
1
+ import base64
2
+ import logging
3
+ from random import choices
4
+ from string import (
5
+ ascii_letters,
6
+ digits,
7
+ )
8
+ from typing import (
9
+ Any,
10
+ Callable,
11
+ Optional,
12
+ )
13
+
14
+ from pydantic import BaseModel
15
+
16
+ from reconcile import openshift_base
17
+ from reconcile import openshift_resources_base as orb
18
+ from reconcile import queries
19
+ from reconcile.gql_definitions.terraform_resources.database_access_manager import (
20
+ DatabaseAccessV1,
21
+ NamespaceTerraformProviderResourceAWSV1,
22
+ NamespaceTerraformResourceRDSV1,
23
+ NamespaceV1,
24
+ query,
25
+ )
26
+ from reconcile.utils import gql
27
+ from reconcile.utils.oc import (
28
+ OC_Map,
29
+ OCClient,
30
+ )
31
+ from reconcile.utils.openshift_resource import OpenshiftResource
32
+ from reconcile.utils.runtime.integration import QontractReconcileIntegration
33
+ from reconcile.utils.semver_helper import make_semver
34
+ from reconcile.utils.state import (
35
+ State,
36
+ init_state,
37
+ )
38
+
39
+ QONTRACT_INTEGRATION = "database-access-manager"
40
+ QONTRACT_INTEGRATION_VERSION = make_semver(0, 1, 0)
41
+
42
+ SUPPORTED_ENGINES = ["postgres"]
43
+
44
+
45
+ def get_database_access_namespaces(
46
+ query_func: Optional[Callable] = None,
47
+ ) -> list[NamespaceV1]:
48
+ if not query_func:
49
+ query_func = gql.get_api().query
50
+ return query(query_func).namespaces_v1 or []
51
+
52
+
53
+ class DatabaseConnectionParameters(BaseModel):
54
+ host: str
55
+ port: str
56
+ user: str
57
+ password: str
58
+ database: str
59
+
60
+
61
+ class PSQLScriptGenerator(BaseModel):
62
+ db_access: DatabaseAccessV1
63
+ connection_parameter: DatabaseConnectionParameters
64
+ engine: str
65
+
66
+ def _get_db(self) -> str:
67
+ return self.db_access.database
68
+
69
+ def _get_user(self) -> str:
70
+ return self.db_access.username
71
+
72
+ def _generate_create_user(self) -> str:
73
+ return f"""
74
+ \\set ON_ERROR_STOP on
75
+
76
+ SELECT 'CREATE DATABASE "{self._get_db()}"'
77
+ WHERE NOT EXISTS (SELECT FROM pg_database WHERE datname = '{self._get_db()}');\\gexec
78
+ /* revoke only required for databases lower version 15
79
+ this makes the behaviour compliant with postgres 15, writes are only allowed
80
+ in the schema created for the role/user */
81
+ REVOKE ALL ON DATABASE "{self._get_db()}" FROM public;
82
+
83
+ \\c "{self._get_db()}"
84
+
85
+ select 'CREATE ROLE "{self._get_user()}" WITH LOGIN PASSWORD ''{self.connection_parameter.password}'' VALID UNTIL ''infinity'''
86
+ WHERE NOT EXISTS (SELECT FROM pg_catalog.pg_roles WHERE rolname = '{self._get_db()}');\\gexec
87
+
88
+ -- rds specific, grant role to admin or create schema fails
89
+ grant "{self._get_user()}" to postgres;
90
+ CREATE SCHEMA IF NOT EXISTS "{self._get_user()}" AUTHORIZATION "{self._get_user()}";"""
91
+
92
+ def _generate_db_access(self) -> str:
93
+ statements: list[str] = ["\n"]
94
+ for access in self.db_access.access or []:
95
+ statement = f"GRANT {','.join(access.grants)} ON ALL TABLES IN SCHEMA \"{access.target.dbschema}\" TO \"{self._get_user()}\";\n"
96
+ statements.append(statement)
97
+ return "".join(statements)
98
+
99
+ def generate_script(self) -> str:
100
+ x = self._generate_create_user() + "\n" + self._generate_db_access()
101
+ return x
102
+
103
+
104
+ def secret_head(name: str) -> dict[str, Any]:
105
+ return {
106
+ "apiVersion": "v1",
107
+ "kind": "Secret",
108
+ "type": "Opaque",
109
+ "metadata": {
110
+ "name": name,
111
+ },
112
+ }
113
+
114
+
115
+ def generate_user_secret_spec(
116
+ name: str, db_connection: DatabaseConnectionParameters
117
+ ) -> OpenshiftResource:
118
+ secret = secret_head(name)
119
+ secret["data"] = {
120
+ "db.host": base64.b64encode(db_connection.host.encode("utf-8")).decode("utf-8"),
121
+ "db.name": base64.b64encode(db_connection.database.encode("utf-8")).decode(
122
+ "utf-8"
123
+ ),
124
+ "db.password": base64.b64encode(db_connection.password.encode("utf-8")).decode(
125
+ "utf-8"
126
+ ),
127
+ "db.port": base64.b64encode(db_connection.port.encode("utf-8")).decode("utf-8"),
128
+ "db.user": base64.b64encode(db_connection.user.encode("utf-8")).decode("utf-8"),
129
+ }
130
+ return OpenshiftResource(
131
+ body=secret,
132
+ integration=QONTRACT_INTEGRATION,
133
+ integration_version=QONTRACT_INTEGRATION_VERSION,
134
+ )
135
+
136
+
137
+ def generate_script_secret_spec(name: str, script: str) -> OpenshiftResource:
138
+ secret = secret_head(name)
139
+ secret["data"] = {
140
+ "script.sql": base64.b64encode(script.encode("utf-8")).decode("utf-8"),
141
+ }
142
+ return OpenshiftResource(
143
+ body=secret,
144
+ integration=QONTRACT_INTEGRATION,
145
+ integration_version=QONTRACT_INTEGRATION_VERSION,
146
+ )
147
+
148
+
149
+ def get_db_engine(resource: NamespaceTerraformResourceRDSV1) -> str:
150
+ defaults = gql.get_resource(resource.defaults)
151
+ engine = "postgres"
152
+ for line in defaults["content"].split("\n"):
153
+ if line and line.startswith("engine:"):
154
+ engine = line.split(":")[1].strip()
155
+ if engine not in SUPPORTED_ENGINES:
156
+ raise Exception(f"Unsupported engine: {engine}")
157
+ return engine
158
+
159
+
160
+ class JobData(BaseModel):
161
+ engine: str
162
+ engine_version: str
163
+ name_suffix: str
164
+ image_repository: str
165
+ service_account_name: str
166
+ rds_admin_secret_name: str
167
+ script_secret_name: str
168
+ pull_secret: str
169
+
170
+
171
+ def get_job_spec(job_data: JobData) -> OpenshiftResource:
172
+ job_name = f"dbam-{job_data.name_suffix}"
173
+
174
+ if job_data.engine == "postgres":
175
+ command = "/usr/local/bin/psql"
176
+
177
+ job = {
178
+ "apiVersion": "batch/v1",
179
+ "kind": "Job",
180
+ "metadata": {
181
+ "name": job_name,
182
+ "labels": {
183
+ "app": "qontract-reconcile",
184
+ "integration": QONTRACT_INTEGRATION,
185
+ },
186
+ },
187
+ "spec": {
188
+ "backoffLimit": 1,
189
+ "template": {
190
+ "metadata": {
191
+ "name": job_name,
192
+ },
193
+ "spec": {
194
+ "imagePullSecrets": [{"name": job_data.pull_secret}],
195
+ "restartPolicy": "Never",
196
+ "serviceAccountName": job_data.service_account_name,
197
+ "containers": [
198
+ {
199
+ "name": job_name,
200
+ "image": f"{job_data.image_repository}/{job_data.engine}:{job_data.engine_version}",
201
+ "command": [
202
+ command,
203
+ ],
204
+ "args": [
205
+ "-ae",
206
+ "--host=$(db.host)",
207
+ "--port=$(db.port)",
208
+ "--username=$(db.user)",
209
+ "--dbname=$(db.name)",
210
+ "--file=/tmp/scripts/script.sql",
211
+ ],
212
+ "env": [
213
+ {
214
+ "name": "db.host",
215
+ "valueFrom": {
216
+ "secretKeyRef": {
217
+ "name": job_data.rds_admin_secret_name,
218
+ "key": "db.host",
219
+ },
220
+ },
221
+ },
222
+ {
223
+ "name": "db.name",
224
+ "valueFrom": {
225
+ "secretKeyRef": {
226
+ "name": job_data.rds_admin_secret_name,
227
+ "key": "db.name",
228
+ },
229
+ },
230
+ },
231
+ {
232
+ "name": "PGPASSWORD",
233
+ "valueFrom": {
234
+ "secretKeyRef": {
235
+ "name": job_data.rds_admin_secret_name,
236
+ "key": "db.password",
237
+ },
238
+ },
239
+ },
240
+ {
241
+ "name": "db.port",
242
+ "valueFrom": {
243
+ "secretKeyRef": {
244
+ "name": job_data.rds_admin_secret_name,
245
+ "key": "db.port",
246
+ },
247
+ },
248
+ },
249
+ {
250
+ "name": "db.user",
251
+ "valueFrom": {
252
+ "secretKeyRef": {
253
+ "name": job_data.rds_admin_secret_name,
254
+ "key": "db.user",
255
+ },
256
+ },
257
+ },
258
+ ],
259
+ "resources": {
260
+ "requests": {
261
+ "cpu": "100m",
262
+ "memory": "128Mi",
263
+ },
264
+ },
265
+ "volumeMounts": [
266
+ {
267
+ "name": "configs",
268
+ "mountPath": "/tmp/scripts/",
269
+ "readOnly": True,
270
+ },
271
+ ],
272
+ },
273
+ ],
274
+ "volumes": [
275
+ {
276
+ "name": "configs",
277
+ "projected": {
278
+ "sources": [
279
+ {
280
+ "secret": {
281
+ "name": job_data.script_secret_name,
282
+ },
283
+ },
284
+ ],
285
+ },
286
+ },
287
+ ],
288
+ },
289
+ },
290
+ },
291
+ }
292
+ return OpenshiftResource(
293
+ body=job,
294
+ integration=QONTRACT_INTEGRATION,
295
+ integration_version=QONTRACT_INTEGRATION_VERSION,
296
+ )
297
+
298
+
299
+ def get_service_account_spec(name: str) -> OpenshiftResource:
300
+ return OpenshiftResource(
301
+ body={
302
+ "apiVersion": "v1",
303
+ "kind": "ServiceAccount",
304
+ "metadata": {
305
+ "name": name,
306
+ "labels": {
307
+ "app": "qontract-reconcile",
308
+ "integration": QONTRACT_INTEGRATION,
309
+ },
310
+ },
311
+ "automountServiceAccountToken": False,
312
+ },
313
+ integration=QONTRACT_INTEGRATION,
314
+ integration_version=QONTRACT_INTEGRATION_VERSION,
315
+ )
316
+
317
+
318
+ class DBAMResource(BaseModel):
319
+ resource: OpenshiftResource
320
+ clean_up: bool
321
+
322
+ class Config:
323
+ arbitrary_types_allowed = True
324
+
325
+
326
+ class JobStatusCondition(BaseModel):
327
+ type: str
328
+
329
+
330
+ class JobStatus(BaseModel):
331
+ conditions: list[JobStatusCondition]
332
+
333
+ def is_complete(self) -> bool:
334
+ return True if self.conditions else False
335
+
336
+ def has_errors(self) -> bool:
337
+ for condition in self.conditions:
338
+ if condition.type == "Failed":
339
+ return True
340
+ return False
341
+
342
+
343
+ def _populate_resources(
344
+ db_access: DatabaseAccessV1,
345
+ engine: str,
346
+ image_repository: str,
347
+ pull_secret: dict[Any, Any],
348
+ admin_secret_name: str,
349
+ resource_prefix: str,
350
+ settings: dict[Any, Any],
351
+ database_connection: DatabaseConnectionParameters,
352
+ ) -> list[DBAMResource]:
353
+ managed_resources: list[DBAMResource] = []
354
+ # create service account
355
+ managed_resources.append(
356
+ DBAMResource(
357
+ resource=get_service_account_spec(resource_prefix),
358
+ clean_up=True,
359
+ )
360
+ )
361
+
362
+ # create script secret
363
+ generator = PSQLScriptGenerator(
364
+ db_access=db_access,
365
+ connection_parameter=database_connection,
366
+ engine=engine,
367
+ )
368
+ script_secret_name = f"{resource_prefix}-script"
369
+ managed_resources.append(
370
+ DBAMResource(
371
+ resource=generate_script_secret_spec(
372
+ script_secret_name,
373
+ generator.generate_script(),
374
+ ),
375
+ clean_up=True,
376
+ )
377
+ )
378
+ # create user secret
379
+ managed_resources.append(
380
+ DBAMResource(
381
+ resource=generate_user_secret_spec(resource_prefix, database_connection),
382
+ clean_up=False,
383
+ )
384
+ )
385
+ # create pull secret
386
+ labels = pull_secret["labels"] or {}
387
+ pull_secret_resources = orb.fetch_provider_vault_secret(
388
+ path=pull_secret["path"],
389
+ version=pull_secret["version"],
390
+ name=f"{resource_prefix}-pull-secret",
391
+ labels=labels,
392
+ annotations=pull_secret["annotations"] or {},
393
+ type=pull_secret["type"],
394
+ integration=QONTRACT_INTEGRATION,
395
+ integration_version=QONTRACT_INTEGRATION_VERSION,
396
+ settings=settings,
397
+ )
398
+ managed_resources.append(
399
+ DBAMResource(resource=pull_secret_resources, clean_up=True)
400
+ )
401
+ # create job
402
+ managed_resources.append(
403
+ DBAMResource(
404
+ resource=get_job_spec(
405
+ JobData(
406
+ engine=engine,
407
+ engine_version="15.4-alpine",
408
+ name_suffix=db_access.name,
409
+ image_repository=image_repository,
410
+ service_account_name=resource_prefix,
411
+ rds_admin_secret_name=admin_secret_name,
412
+ script_secret_name=script_secret_name,
413
+ pull_secret=f"{resource_prefix}-pull-secret",
414
+ )
415
+ ),
416
+ clean_up=True,
417
+ )
418
+ )
419
+
420
+ return managed_resources
421
+
422
+
423
+ def _generate_password() -> str:
424
+ return "".join(choices(ascii_letters + digits, k=32))
425
+
426
+
427
+ def _create_database_connection_parameter(
428
+ db_access: DatabaseAccessV1,
429
+ namespace_name: str,
430
+ oc: OCClient,
431
+ admin_secret_name: str,
432
+ user_secret_name: str,
433
+ ) -> DatabaseConnectionParameters:
434
+ def _decode_secret_value(value: str) -> str:
435
+ return base64.b64decode(value).decode("utf-8")
436
+
437
+ user_secret = oc.get(
438
+ namespace_name,
439
+ "Secret",
440
+ user_secret_name,
441
+ allow_not_found=True,
442
+ )
443
+ if user_secret:
444
+ password = _decode_secret_value(user_secret["data"]["db.password"])
445
+ host = _decode_secret_value(user_secret["data"]["db.host"])
446
+ user = _decode_secret_value(user_secret["data"]["db.user"])
447
+ port = _decode_secret_value(user_secret["data"]["db.port"])
448
+ database = _decode_secret_value(user_secret["data"]["db.name"])
449
+ else:
450
+ admin_secret = oc.get(
451
+ namespace_name,
452
+ "Secret",
453
+ admin_secret_name,
454
+ allow_not_found=False,
455
+ )
456
+ host = _decode_secret_value(admin_secret["data"]["db.host"])
457
+ port = _decode_secret_value(admin_secret["data"]["db.port"])
458
+ user = db_access.username
459
+ password = _generate_password()
460
+ database = db_access.database
461
+ database_connection = DatabaseConnectionParameters(
462
+ host=host,
463
+ port=port,
464
+ user=user,
465
+ password=password,
466
+ database=database,
467
+ )
468
+ return database_connection
469
+
470
+
471
+ class JobFailedError(Exception):
472
+ pass
473
+
474
+
475
+ def _process_db_access(
476
+ dry_run: bool,
477
+ state: State,
478
+ db_access: DatabaseAccessV1,
479
+ oc_map: OC_Map,
480
+ cluster_name: str,
481
+ namespace_name: str,
482
+ admin_secret_name: str,
483
+ engine: str,
484
+ settings: dict[Any, Any],
485
+ ) -> None:
486
+ if state.exists(db_access.name):
487
+ current_state = state.get(db_access.name)
488
+ if current_state == db_access.dict(by_alias=True):
489
+ return
490
+
491
+ resource_prefix = f"dbam-{db_access.name}"
492
+ oc = oc_map.get_cluster(cluster_name, False)
493
+
494
+ database_connection = _create_database_connection_parameter(
495
+ db_access,
496
+ namespace_name,
497
+ oc,
498
+ admin_secret_name,
499
+ resource_prefix,
500
+ )
501
+
502
+ managed_resources = _populate_resources(
503
+ db_access,
504
+ engine,
505
+ settings["imageRepository"],
506
+ settings["pullSecret"],
507
+ admin_secret_name,
508
+ resource_prefix,
509
+ settings,
510
+ database_connection,
511
+ )
512
+
513
+ # create job, delete old, failed job first
514
+ job = oc.get(
515
+ namespace_name,
516
+ "Job",
517
+ f"dbam-{db_access.name}",
518
+ allow_not_found=True,
519
+ )
520
+ if not job:
521
+ for r in managed_resources:
522
+ openshift_base.apply(
523
+ dry_run=dry_run,
524
+ oc_map=oc_map,
525
+ cluster=cluster_name,
526
+ namespace=namespace_name,
527
+ resource_type=r.resource.kind,
528
+ resource=r.resource,
529
+ wait_for_namespace=False,
530
+ )
531
+ return
532
+ job_status = JobStatus(
533
+ conditions=[
534
+ JobStatusCondition(type=c["type"])
535
+ for c in job["status"].get("conditions", [])
536
+ ]
537
+ )
538
+ if job_status.is_complete():
539
+ if job_status.has_errors():
540
+ raise JobFailedError(f"Job dbam-{db_access.name} failed, please check logs")
541
+ state.add(
542
+ db_access.name,
543
+ value=db_access.dict(by_alias=True),
544
+ force=True,
545
+ )
546
+ logging.debug("job completed, cleaning up")
547
+ for r in managed_resources:
548
+ if r.clean_up:
549
+ openshift_base.delete(
550
+ dry_run=dry_run,
551
+ oc_map=oc_map,
552
+ cluster=cluster_name,
553
+ namespace=namespace_name,
554
+ resource_type=r.resource.kind,
555
+ name=r.resource.name,
556
+ enable_deletion=True,
557
+ )
558
+ else:
559
+ logging.info(f"Job dbam-{db_access.name} appears to be still running")
560
+
561
+
562
+ class DatabaseAccessManagerIntegration(QontractReconcileIntegration):
563
+ @property
564
+ def name(self) -> str:
565
+ return QONTRACT_INTEGRATION
566
+
567
+ def run(self, dry_run: bool) -> None:
568
+ settings = queries.get_app_interface_settings()
569
+
570
+ sql_query_settings = settings.get("sqlQuery")
571
+ if not sql_query_settings:
572
+ raise KeyError("sqlQuery settings are required")
573
+
574
+ state = init_state(
575
+ integration=QONTRACT_INTEGRATION, secret_reader=self.secret_reader
576
+ )
577
+
578
+ encounteredErrors = False
579
+
580
+ namespaces = get_database_access_namespaces()
581
+ with OC_Map(
582
+ namespaces=[n.dict(by_alias=True) for n in namespaces],
583
+ integration=QONTRACT_INTEGRATION,
584
+ settings=settings,
585
+ ) as oc_map:
586
+ for namespace in namespaces:
587
+ for external_resource in [
588
+ er
589
+ for er in namespace.external_resources or []
590
+ if isinstance(er, NamespaceTerraformProviderResourceAWSV1)
591
+ ]:
592
+ for resource in [
593
+ r
594
+ for r in external_resource.resources or []
595
+ if isinstance(r, NamespaceTerraformResourceRDSV1)
596
+ ]:
597
+ admin_secret_name = resource.output_resource_name
598
+ if admin_secret_name is None:
599
+ logging.error(
600
+ f"{resource.identifier}-{resource.provider} is missing output_resource_name"
601
+ )
602
+ encounteredErrors = True
603
+ else:
604
+ for db_access in resource.database_access or []:
605
+ try:
606
+ _process_db_access(
607
+ dry_run,
608
+ state,
609
+ db_access,
610
+ oc_map,
611
+ namespace.cluster.name,
612
+ namespace.name,
613
+ admin_secret_name,
614
+ get_db_engine(resource),
615
+ sql_query_settings,
616
+ )
617
+ except JobFailedError:
618
+ encounteredErrors = True
619
+
620
+ if encounteredErrors:
621
+ raise JobFailedError("One or more jobs failed to complete")
@@ -0,0 +1,166 @@
1
+ """
2
+ Generated by qenerate plugin=pydantic_v1. DO NOT MODIFY MANUALLY!
3
+ """
4
+ from collections.abc import Callable # noqa: F401 # pylint: disable=W0611
5
+ from datetime import datetime # noqa: F401 # pylint: disable=W0611
6
+ from enum import Enum # noqa: F401 # pylint: disable=W0611
7
+ from typing import ( # noqa: F401 # pylint: disable=W0611
8
+ Any,
9
+ Optional,
10
+ Union,
11
+ )
12
+
13
+ from pydantic import ( # noqa: F401 # pylint: disable=W0611
14
+ BaseModel,
15
+ Extra,
16
+ Field,
17
+ Json,
18
+ )
19
+
20
+
21
+ DEFINITION = """
22
+ query DatabaseAccessManager {
23
+ namespaces_v1 {
24
+ name
25
+ cluster {
26
+ name
27
+ serverUrl
28
+ automationToken {
29
+ path
30
+ field
31
+ version
32
+ format
33
+ }
34
+ internal
35
+ }
36
+ externalResources {
37
+ provider
38
+ provisioner {
39
+ name
40
+ }
41
+ ... on NamespaceTerraformProviderResourceAWS_v1 {
42
+ resources {
43
+ provider
44
+ ... on NamespaceTerraformResourceRDS_v1 {
45
+ region
46
+ identifier
47
+ defaults
48
+ output_resource_name
49
+ database_access {
50
+ username
51
+ name
52
+ database
53
+ delete
54
+ access {
55
+ grants
56
+ target {
57
+ dbschema
58
+ }
59
+ }
60
+ }
61
+ }
62
+ }
63
+ }
64
+ }
65
+ }
66
+ }
67
+ """
68
+
69
+
70
+ class ConfiguredBaseModel(BaseModel):
71
+ class Config:
72
+ smart_union = True
73
+ extra = Extra.forbid
74
+
75
+
76
+ class VaultSecretV1(ConfiguredBaseModel):
77
+ path: str = Field(..., alias="path")
78
+ field: str = Field(..., alias="field")
79
+ version: Optional[int] = Field(..., alias="version")
80
+ q_format: Optional[str] = Field(..., alias="format")
81
+
82
+
83
+ class ClusterV1(ConfiguredBaseModel):
84
+ name: str = Field(..., alias="name")
85
+ server_url: str = Field(..., alias="serverUrl")
86
+ automation_token: Optional[VaultSecretV1] = Field(..., alias="automationToken")
87
+ internal: Optional[bool] = Field(..., alias="internal")
88
+
89
+
90
+ class ExternalResourcesProvisionerV1(ConfiguredBaseModel):
91
+ name: str = Field(..., alias="name")
92
+
93
+
94
+ class NamespaceExternalResourceV1(ConfiguredBaseModel):
95
+ provider: str = Field(..., alias="provider")
96
+ provisioner: ExternalResourcesProvisionerV1 = Field(..., alias="provisioner")
97
+
98
+
99
+ class NamespaceTerraformResourceAWSV1(ConfiguredBaseModel):
100
+ provider: str = Field(..., alias="provider")
101
+
102
+
103
+ class DatabaseAccessAccessGranteeV1(ConfiguredBaseModel):
104
+ dbschema: str = Field(..., alias="dbschema")
105
+
106
+
107
+ class DatabaseAccessAccessV1(ConfiguredBaseModel):
108
+ grants: list[str] = Field(..., alias="grants")
109
+ target: DatabaseAccessAccessGranteeV1 = Field(..., alias="target")
110
+
111
+
112
+ class DatabaseAccessV1(ConfiguredBaseModel):
113
+ username: str = Field(..., alias="username")
114
+ name: str = Field(..., alias="name")
115
+ database: str = Field(..., alias="database")
116
+ delete: Optional[bool] = Field(..., alias="delete")
117
+ access: Optional[list[DatabaseAccessAccessV1]] = Field(..., alias="access")
118
+
119
+
120
+ class NamespaceTerraformResourceRDSV1(NamespaceTerraformResourceAWSV1):
121
+ region: Optional[str] = Field(..., alias="region")
122
+ identifier: str = Field(..., alias="identifier")
123
+ defaults: str = Field(..., alias="defaults")
124
+ output_resource_name: Optional[str] = Field(..., alias="output_resource_name")
125
+ database_access: Optional[list[DatabaseAccessV1]] = Field(
126
+ ..., alias="database_access"
127
+ )
128
+
129
+
130
+ class NamespaceTerraformProviderResourceAWSV1(NamespaceExternalResourceV1):
131
+ resources: list[
132
+ Union[NamespaceTerraformResourceRDSV1, NamespaceTerraformResourceAWSV1]
133
+ ] = Field(..., alias="resources")
134
+
135
+
136
+ class NamespaceV1(ConfiguredBaseModel):
137
+ name: str = Field(..., alias="name")
138
+ cluster: ClusterV1 = Field(..., alias="cluster")
139
+ external_resources: Optional[
140
+ list[
141
+ Union[NamespaceTerraformProviderResourceAWSV1, NamespaceExternalResourceV1]
142
+ ]
143
+ ] = Field(..., alias="externalResources")
144
+
145
+
146
+ class DatabaseAccessManagerQueryData(ConfiguredBaseModel):
147
+ namespaces_v1: Optional[list[NamespaceV1]] = Field(..., alias="namespaces_v1")
148
+
149
+
150
+ def query(query_func: Callable, **kwargs: Any) -> DatabaseAccessManagerQueryData:
151
+ """
152
+ This is a convenience function which queries and parses the data into
153
+ concrete types. It should be compatible with most GQL clients.
154
+ You do not have to use it to consume the generated data classes.
155
+ Alternatively, you can also mime and alternate the behavior
156
+ of this function in the caller.
157
+
158
+ Parameters:
159
+ query_func (Callable): Function which queries your GQL Server
160
+ kwargs: optional arguments that will be passed to the query function
161
+
162
+ Returns:
163
+ DatabaseAccessManagerQueryData: queried data parsed into generated classes
164
+ """
165
+ raw_data: dict[Any, Any] = query_func(DEFINITION, **kwargs)
166
+ return DatabaseAccessManagerQueryData(**raw_data)
@@ -0,0 +1,405 @@
1
+ from collections import defaultdict
2
+ from typing import Callable
3
+ from unittest.mock import MagicMock
4
+
5
+ import pytest
6
+ from pytest_mock import MockerFixture
7
+
8
+ from reconcile.database_access_manager import (
9
+ DatabaseConnectionParameters,
10
+ DBAMResource,
11
+ JobFailedError,
12
+ JobStatus,
13
+ JobStatusCondition,
14
+ PSQLScriptGenerator,
15
+ _create_database_connection_parameter,
16
+ _generate_password,
17
+ _populate_resources,
18
+ _process_db_access,
19
+ )
20
+ from reconcile.gql_definitions.terraform_resources.database_access_manager import (
21
+ DatabaseAccessAccessV1,
22
+ DatabaseAccessV1,
23
+ )
24
+ from reconcile.utils.oc import OC_Map
25
+ from reconcile.utils.openshift_resource import OpenshiftResource
26
+
27
+
28
+ @pytest.fixture
29
+ def db_access(gql_class_factory: Callable[..., DatabaseAccessV1]) -> DatabaseAccessV1:
30
+ return gql_class_factory(
31
+ DatabaseAccessV1,
32
+ {
33
+ "username": "test",
34
+ "name": "test",
35
+ "database": "test",
36
+ },
37
+ )
38
+
39
+
40
+ @pytest.fixture
41
+ def db_access_access(
42
+ gql_class_factory: Callable[..., DatabaseAccessAccessV1]
43
+ ) -> DatabaseAccessAccessV1:
44
+ return gql_class_factory(
45
+ DatabaseAccessAccessV1,
46
+ {
47
+ "grants": ["insert", "select"],
48
+ "target": {
49
+ "dbschema": "foo",
50
+ },
51
+ },
52
+ )
53
+
54
+
55
+ @pytest.fixture
56
+ def db_access_complete(
57
+ db_access: DatabaseAccessV1, db_access_access: DatabaseAccessAccessV1
58
+ ) -> DatabaseAccessV1:
59
+ db_access.access = [db_access_access]
60
+ return db_access
61
+
62
+
63
+ @pytest.fixture
64
+ def db_connection_parameter():
65
+ return DatabaseConnectionParameters(
66
+ host="localhost",
67
+ port="5432",
68
+ user="test",
69
+ password="postgres", # notsecret
70
+ database="test",
71
+ )
72
+
73
+
74
+ @pytest.fixture
75
+ def db_secret_dict() -> dict[str, dict[str, str]]:
76
+ return {
77
+ "data": {
78
+ "db.password": "aGR1aHNkZnVoc2Rm", # notsecret
79
+ "db.host": "bG9jYWxob3N0",
80
+ "db.port": "NTQzMg==",
81
+ "db.user": "dGVzdA==",
82
+ "db.name": "dGVzdA==",
83
+ }
84
+ }
85
+
86
+
87
+ @pytest.fixture
88
+ def openshift_resource_secet() -> OpenshiftResource:
89
+ return OpenshiftResource(
90
+ body={
91
+ "metadata": {"name": "test"},
92
+ "kind": "secret",
93
+ "data": {"password": "postgres"},
94
+ },
95
+ integration="TEST",
96
+ integration_version="0.0.1",
97
+ )
98
+
99
+
100
+ def _assert_create_script(script: str) -> None:
101
+ assert 'CREATE DATABASE "test"' in script
102
+ assert "REVOKE ALL ON DATABASE" in script
103
+ assert 'CREATE ROLE "test" WITH LOGIN PASSWORD' in script
104
+ assert "CREATE SCHEMA IF NOT EXISTS" in script
105
+
106
+
107
+ def _assert_grant_access(script: str) -> None:
108
+ assert 'GRANT insert,select ON ALL TABLES IN SCHEMA "foo" TO "test"' in script
109
+
110
+
111
+ def test_generate_create_user(
112
+ db_access: DatabaseAccessV1, db_connection_parameter: DatabaseConnectionParameters
113
+ ) -> None:
114
+ s = PSQLScriptGenerator(
115
+ db_access=db_access,
116
+ connection_parameter=db_connection_parameter,
117
+ engine="postgres",
118
+ )
119
+ script = s._generate_create_user()
120
+ _assert_create_script(script)
121
+
122
+
123
+ def test_generate_access(
124
+ db_access: DatabaseAccessV1,
125
+ db_access_access: DatabaseAccessAccessV1,
126
+ db_connection_parameter: DatabaseConnectionParameters,
127
+ ):
128
+ db_access.access = [db_access_access]
129
+
130
+ s = PSQLScriptGenerator(
131
+ db_access=db_access,
132
+ connection_parameter=db_connection_parameter,
133
+ engine="postgres",
134
+ )
135
+ script = s._generate_db_access()
136
+ _assert_grant_access(script)
137
+
138
+
139
+ def test_generate_complete(
140
+ db_access_complete: DatabaseAccessV1,
141
+ db_connection_parameter: DatabaseConnectionParameters,
142
+ ):
143
+ s = PSQLScriptGenerator(
144
+ db_access=db_access_complete,
145
+ connection_parameter=db_connection_parameter,
146
+ engine="postgres",
147
+ )
148
+ script = s.generate_script()
149
+ _assert_create_script(script)
150
+ _assert_grant_access(script)
151
+
152
+
153
+ def test_job_completion():
154
+ s = JobStatus(conditions=[])
155
+ assert s.is_complete() is False
156
+
157
+ s = JobStatus(conditions=[JobStatusCondition(type="Complete")])
158
+ assert s.is_complete()
159
+ assert s.has_errors() is False
160
+
161
+
162
+ def test_has_errors():
163
+ s = JobStatus(conditions=[JobStatusCondition(type="Failed")])
164
+ assert s.is_complete()
165
+ assert s.has_errors()
166
+
167
+
168
+ def test_populate_resources(
169
+ mocker: MockerFixture,
170
+ db_access: DatabaseAccessV1,
171
+ db_connection_parameter: DatabaseConnectionParameters,
172
+ openshift_resource_secet: OpenshiftResource,
173
+ ):
174
+ mocker.patch(
175
+ "reconcile.database_access_manager.orb.fetch_provider_vault_secret",
176
+ return_value=openshift_resource_secet,
177
+ )
178
+ reources = _populate_resources(
179
+ db_access=db_access,
180
+ engine="postgres",
181
+ image_repository="foo",
182
+ pull_secret={
183
+ "version": 1,
184
+ "annotations": [],
185
+ "type": "a",
186
+ "labels": [],
187
+ "path": "/foo/bar",
188
+ },
189
+ admin_secret_name="db-secret",
190
+ resource_prefix="dbam-foo",
191
+ settings={"foo": "bar"},
192
+ database_connection=db_connection_parameter,
193
+ )
194
+
195
+ r_kinds = [r.resource.kind for r in reources]
196
+ assert sorted(r_kinds) == ["Job", "Secret", "Secret", "ServiceAccount", "secret"]
197
+
198
+
199
+ def test__create_database_connection_parameter_user_exists(
200
+ db_access: DatabaseAccessV1,
201
+ db_secret_dict: dict[str, dict[str, str]],
202
+ mocker: MockerFixture,
203
+ ):
204
+ oc = mocker.patch("reconcile.utils.oc.OCNative", autospec=True)
205
+ oc.get.return_value = db_secret_dict
206
+ p = _create_database_connection_parameter(
207
+ db_access=db_access,
208
+ namespace_name="foo",
209
+ oc=oc,
210
+ admin_secret_name="db-secret",
211
+ user_secret_name="db-user-secret",
212
+ )
213
+ assert p == DatabaseConnectionParameters(
214
+ host="localhost",
215
+ port="5432",
216
+ user="test",
217
+ password="hduhsdfuhsdf",
218
+ database="test",
219
+ )
220
+
221
+
222
+ def test__create_database_connection_parameter_user_missing(
223
+ db_access: DatabaseAccessV1,
224
+ db_secret_dict: dict[str, dict[str, str]],
225
+ mocker: MockerFixture,
226
+ ):
227
+ pw_generated = "1N5j7oksB45l8w0RJD8qR0ENJP1yOAOs" # notsecret
228
+ oc = mocker.patch("reconcile.utils.oc.OCNative", autospec=True)
229
+ oc.get.side_effect = [None, db_secret_dict]
230
+ mocker.patch(
231
+ "reconcile.database_access_manager._generate_password",
232
+ return_value=pw_generated,
233
+ )
234
+ p = _create_database_connection_parameter(
235
+ db_access=db_access,
236
+ namespace_name="foo",
237
+ oc=oc,
238
+ admin_secret_name="db-secret",
239
+ user_secret_name="db-user-secret",
240
+ )
241
+ assert p == DatabaseConnectionParameters(
242
+ host="localhost",
243
+ port="5432",
244
+ user="test",
245
+ password=pw_generated,
246
+ database="test",
247
+ )
248
+
249
+
250
+ def test_generate_password():
251
+ assert len(_generate_password()) == 32
252
+ assert _generate_password() != _generate_password()
253
+
254
+
255
+ @pytest.fixture
256
+ def dbam_state(mocker: MockerFixture) -> MockerFixture:
257
+ return mocker.patch("reconcile.database_access_manager.State", autospec=True)
258
+
259
+
260
+ @pytest.fixture
261
+ def dbam_oc_map(mocker: MockerFixture) -> MockerFixture:
262
+ return mocker.patch("reconcile.database_access_manager.OC_Map", autospec=True)
263
+
264
+
265
+ @pytest.fixture
266
+ def dbam_process_mocks(
267
+ openshift_resource_secet: OpenshiftResource, mocker: MockerFixture
268
+ ) -> DBAMResource:
269
+ expected_resource = DBAMResource(resource=openshift_resource_secet, clean_up=True)
270
+ mocker.patch(
271
+ "reconcile.database_access_manager._create_database_connection_parameter",
272
+ return_value=db_connection_parameter,
273
+ )
274
+ mocker.patch(
275
+ "reconcile.database_access_manager._populate_resources",
276
+ return_value=[expected_resource],
277
+ )
278
+ return expected_resource
279
+
280
+
281
+ def test__process_db_access_job_pass(
282
+ db_access: DatabaseAccessV1,
283
+ dbam_state: MagicMock,
284
+ dbam_oc_map: MagicMock,
285
+ dbam_process_mocks: DBAMResource,
286
+ mocker: MockerFixture,
287
+ ):
288
+ dbam_state.exists.return_value = False
289
+ oc = mocker.patch("reconcile.utils.oc.OCNative", autospec=True)
290
+ oc.get.return_value = {"status": {"conditions": [{"type": "Complete"}]}}
291
+ dbam_oc_map.get_cluster.return_value = oc
292
+
293
+ ob_delete = mocker.patch(
294
+ "reconcile.database_access_manager.openshift_base.delete", autospec=True
295
+ )
296
+
297
+ _process_db_access(
298
+ False,
299
+ dbam_state,
300
+ db_access,
301
+ dbam_oc_map,
302
+ "test-cluster",
303
+ namespace_name="test-namepsace",
304
+ admin_secret_name="db-secret",
305
+ engine="postgres",
306
+ settings=defaultdict(str),
307
+ )
308
+
309
+ assert ob_delete.call_count == 1
310
+ ob_delete.assert_called_once_with(
311
+ dry_run=False,
312
+ oc_map=dbam_oc_map,
313
+ cluster="test-cluster",
314
+ namespace="test-namepsace",
315
+ resource_type="secret",
316
+ name=dbam_process_mocks.resource.name,
317
+ enable_deletion=True,
318
+ )
319
+
320
+
321
+ def test__process_db_access_job_error(
322
+ db_access: DatabaseAccessV1,
323
+ dbam_state: MagicMock,
324
+ dbam_oc_map: MagicMock,
325
+ dbam_process_mocks: DBAMResource,
326
+ mocker: MockerFixture,
327
+ ):
328
+ dbam_state.exists.return_value = False
329
+ oc = mocker.patch("reconcile.utils.oc.OCNative", autospec=True)
330
+ oc.get.return_value = {"status": {"conditions": [{"type": "Failed"}]}}
331
+ dbam_oc_map.get_cluster.return_value = oc
332
+
333
+ with pytest.raises(JobFailedError):
334
+ _process_db_access(
335
+ False,
336
+ dbam_state,
337
+ db_access,
338
+ dbam_oc_map,
339
+ "test-cluster",
340
+ namespace_name="test-namepsace",
341
+ admin_secret_name="db-secret",
342
+ engine="postgres",
343
+ settings=defaultdict(str),
344
+ )
345
+
346
+
347
+ def test__process_db_access_state_diff(
348
+ db_access: DatabaseAccessV1,
349
+ dbam_state: MagicMock,
350
+ dbam_oc_map: MagicMock,
351
+ dbam_process_mocks: DBAMResource,
352
+ mocker: MockerFixture,
353
+ ):
354
+ dbam_state.exists.return_value = True
355
+ dbam_state.get.return_value = {}
356
+ oc = mocker.patch("reconcile.utils.oc.OCNative", autospec=True)
357
+ oc.get.return_value = False
358
+ dbam_oc_map.get_cluster.return_value = oc
359
+
360
+ ob_apply = mocker.patch(
361
+ "reconcile.database_access_manager.openshift_base.apply", autospec=True
362
+ )
363
+ _process_db_access(
364
+ False,
365
+ dbam_state,
366
+ db_access,
367
+ dbam_oc_map,
368
+ "test-cluster",
369
+ namespace_name="test-namepsace",
370
+ admin_secret_name="db-secret",
371
+ engine="postgres",
372
+ settings=defaultdict(str),
373
+ )
374
+
375
+ assert ob_apply.call_count == 1
376
+ ob_apply.assert_called_once_with(
377
+ dry_run=False,
378
+ oc_map=dbam_oc_map,
379
+ cluster="test-cluster",
380
+ namespace="test-namepsace",
381
+ resource_type="secret",
382
+ resource=dbam_process_mocks.resource,
383
+ wait_for_namespace=False,
384
+ )
385
+
386
+
387
+ def test__process_db_access_state_exists_matched(
388
+ db_access: DatabaseAccessV1,
389
+ dbam_state: MagicMock,
390
+ dbam_oc_map: OC_Map,
391
+ ):
392
+ dbam_state.exists.return_value = True
393
+ dbam_state.get.return_value = db_access.dict(by_alias=True)
394
+ # missing mocks would cause this to fail if not exit early
395
+ _process_db_access(
396
+ False,
397
+ dbam_state,
398
+ db_access,
399
+ dbam_oc_map,
400
+ "test-cluster",
401
+ namespace_name="test-namepsace",
402
+ admin_secret_name="db-secret",
403
+ engine="postgres",
404
+ settings=defaultdict(str),
405
+ )