qontract-reconcile 0.10.1rc401__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.
- {qontract_reconcile-0.10.1rc401.dist-info → qontract_reconcile-0.10.1rc403.dist-info}/METADATA +1 -1
- {qontract_reconcile-0.10.1rc401.dist-info → qontract_reconcile-0.10.1rc403.dist-info}/RECORD +9 -6
- reconcile/cli.py +11 -0
- reconcile/database_access_manager.py +621 -0
- reconcile/gql_definitions/terraform_resources/database_access_manager.py +166 -0
- reconcile/test/test_database_access_manager.py +405 -0
- {qontract_reconcile-0.10.1rc401.dist-info → qontract_reconcile-0.10.1rc403.dist-info}/WHEEL +0 -0
- {qontract_reconcile-0.10.1rc401.dist-info → qontract_reconcile-0.10.1rc403.dist-info}/entry_points.txt +0 -0
- {qontract_reconcile-0.10.1rc401.dist-info → qontract_reconcile-0.10.1rc403.dist-info}/top_level.txt +0 -0
{qontract_reconcile-0.10.1rc401.dist-info → qontract_reconcile-0.10.1rc403.dist-info}/METADATA
RENAMED
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: qontract-reconcile
|
3
|
-
Version: 0.10.
|
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
|
{qontract_reconcile-0.10.1rc401.dist-info → qontract_reconcile-0.10.1rc403.dist-info}/RECORD
RENAMED
@@ -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=
|
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.
|
623
|
-
qontract_reconcile-0.10.
|
624
|
-
qontract_reconcile-0.10.
|
625
|
-
qontract_reconcile-0.10.
|
626
|
-
qontract_reconcile-0.10.
|
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
|
+
)
|
File without changes
|
File without changes
|
{qontract_reconcile-0.10.1rc401.dist-info → qontract_reconcile-0.10.1rc403.dist-info}/top_level.txt
RENAMED
File without changes
|