qontract-reconcile 0.10.1rc734__py3-none-any.whl → 0.10.1rc736__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.1rc734.dist-info → qontract_reconcile-0.10.1rc736.dist-info}/METADATA +1 -1
- {qontract_reconcile-0.10.1rc734.dist-info → qontract_reconcile-0.10.1rc736.dist-info}/RECORD +20 -15
- reconcile/acs_notifiers.py +156 -0
- reconcile/cli.py +11 -0
- reconcile/gql_definitions/acs/acs_policies.py +82 -0
- reconcile/gql_definitions/jira/__init__.py +0 -0
- reconcile/gql_definitions/jira/jira_servers.py +76 -0
- reconcile/gql_definitions/templating/template_collection.py +6 -0
- reconcile/gql_definitions/templating/templates.py +2 -0
- reconcile/templating/lib/merge_request_manager.py +17 -3
- reconcile/templating/lib/model.py +4 -3
- reconcile/templating/renderer.py +38 -29
- reconcile/templating/validator.py +5 -1
- reconcile/test/test_acs_notifiers.py +399 -0
- reconcile/test/test_acs_policies.py +69 -0
- reconcile/utils/acs/base.py +5 -0
- reconcile/utils/acs/notifiers.py +111 -0
- {qontract_reconcile-0.10.1rc734.dist-info → qontract_reconcile-0.10.1rc736.dist-info}/WHEEL +0 -0
- {qontract_reconcile-0.10.1rc734.dist-info → qontract_reconcile-0.10.1rc736.dist-info}/entry_points.txt +0 -0
- {qontract_reconcile-0.10.1rc734.dist-info → qontract_reconcile-0.10.1rc736.dist-info}/top_level.txt +0 -0
{qontract_reconcile-0.10.1rc734.dist-info → qontract_reconcile-0.10.1rc736.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.1rc736
|
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.1rc734.dist-info → qontract_reconcile-0.10.1rc736.dist-info}/RECORD
RENAMED
@@ -1,4 +1,5 @@
|
|
1
1
|
reconcile/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
2
|
+
reconcile/acs_notifiers.py,sha256=1UBRTGsLrMUxpgm3WGceNyCmY9MZwUht4t26eIMWOLU,5589
|
2
3
|
reconcile/acs_policies.py,sha256=1iRYmMdz0YtqyQgA9O0uGQdmMKUCCe-ApRa6LIEAdps,8769
|
3
4
|
reconcile/acs_rbac.py,sha256=JEDevU4AdhTjMW-fAnNG3iw6Od5tYxGuYSirmu9KurI,22657
|
4
5
|
reconcile/aws_ami_share.py,sha256=eeu0TI3M5yyUaozyAq_aW3tir-9be4YFguOXvIvKHSo,3757
|
@@ -9,7 +10,7 @@ reconcile/aws_iam_password_reset.py,sha256=NwErtrqgBiXr7eGCAHdtGGOx0S7-4JnSc29Ie
|
|
9
10
|
reconcile/aws_support_cases_sos.py,sha256=Jk6_XjDeJSYxgRGqcEAOcynt9qJF2r5HPIPcSKmoBv8,2974
|
10
11
|
reconcile/blackbox_exporter_endpoint_monitoring.py,sha256=W_VJagnsJR1v5oqjlI3RJJE0_nhtJ0m81RS8zWA5u5c,3538
|
11
12
|
reconcile/checkpoint.py,sha256=R2WFXUXLTB4sWMi4GeA4eegsuf_1-Q4vH8M0Toh3Ij4,5036
|
12
|
-
reconcile/cli.py,sha256=
|
13
|
+
reconcile/cli.py,sha256=XBUVgDXU9vIUWu4F3qkNN39FlMH7XN8kaGjzfShPWGM,97162
|
13
14
|
reconcile/closedbox_endpoint_monitoring_base.py,sha256=SMhkcQqprWvThrIJa3U_3uh5w1h-alleW1QnCJFY4Qw,4909
|
14
15
|
reconcile/cluster_deployment_mapper.py,sha256=2Ah-nu-Mdig0pjuiZl_XLrmVAjYzFjORR3dMlCgkmw0,2352
|
15
16
|
reconcile/dashdotdb_base.py,sha256=a5aPLVxyqPSbjdB0Ty-uliOtxwvEbbEljHJKxdK3-Zk,4813
|
@@ -185,7 +186,7 @@ reconcile/glitchtip_project_dsn/integration.py,sha256=qt2a33FOUUlCyonSHm3nUb7pNX
|
|
185
186
|
reconcile/gql_definitions/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
186
187
|
reconcile/gql_definitions/acs/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
187
188
|
reconcile/gql_definitions/acs/acs_instances.py,sha256=L91WW9LbhJbBSrECqShQpFtjoBOsmNIYLRpMbx1io5o,2181
|
188
|
-
reconcile/gql_definitions/acs/acs_policies.py,sha256=
|
189
|
+
reconcile/gql_definitions/acs/acs_policies.py,sha256=negfb87RDVH7WT1qiouD8ywfgH4Iumsv9Q2bf9rbjcA,7089
|
189
190
|
reconcile/gql_definitions/acs/acs_rbac.py,sha256=cZsIlCWliPQdQHgmBsIMx54fJNOtkdRXLzmOKZmJNHk,3009
|
190
191
|
reconcile/gql_definitions/advanced_upgrade_service/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
191
192
|
reconcile/gql_definitions/advanced_upgrade_service/aus_clusters.py,sha256=zrZCHaxkffdX2bvFUt1Hd_czViI3v3FPhZz5vJQ73jI,4301
|
@@ -285,6 +286,8 @@ reconcile/gql_definitions/integrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JC
|
|
285
286
|
reconcile/gql_definitions/integrations/integrations.py,sha256=LfpgVbCCCk20ohwP5pDea5fwxMFGrcgE6J_WHBuGqek,11595
|
286
287
|
reconcile/gql_definitions/jenkins_configs/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
287
288
|
reconcile/gql_definitions/jenkins_configs/jenkins_configs.py,sha256=0nMkH0G-AjQwu53fqHykth6X6jjbHdW2hBp5n7N-r24,2766
|
289
|
+
reconcile/gql_definitions/jira/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
290
|
+
reconcile/gql_definitions/jira/jira_servers.py,sha256=i8_P30m-r5Ek6TMgZpqN9fSChDcOf9nLkWD966CXdNw,2102
|
288
291
|
reconcile/gql_definitions/jira_permissions_validator/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
289
292
|
reconcile/gql_definitions/jira_permissions_validator/jira_boards_for_permissions_validator.py,sha256=7p-GA-dGeuouUcAvOIMBbgGJJtIXPG5Jx4n024to97M,3856
|
290
293
|
reconcile/gql_definitions/jumphosts/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
@@ -328,8 +331,8 @@ reconcile/gql_definitions/status_board/status_board.py,sha256=vHEzncabujkqbjJ-ib
|
|
328
331
|
reconcile/gql_definitions/statuspage/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
329
332
|
reconcile/gql_definitions/statuspage/statuspages.py,sha256=gxDb42H93nwtBg7oFRb6Gk9pbAZpsWk_y4Y0s3_g3nE,3520
|
330
333
|
reconcile/gql_definitions/templating/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
331
|
-
reconcile/gql_definitions/templating/template_collection.py,sha256=
|
332
|
-
reconcile/gql_definitions/templating/templates.py,sha256=
|
334
|
+
reconcile/gql_definitions/templating/template_collection.py,sha256=lS0vzEKV2ZrzOqOEriqpy0yBgKjb2Ftrzgx6PIH46_4,3310
|
335
|
+
reconcile/gql_definitions/templating/templates.py,sha256=ejAvQ13zfNMQTz3FWtRUic6dSvio3aAgBKEqt600hbk,2821
|
333
336
|
reconcile/gql_definitions/terraform_cloudflare_dns/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
334
337
|
reconcile/gql_definitions/terraform_cloudflare_dns/app_interface_cloudflare_dns_settings.py,sha256=eyGX9HcTF6MZbOYZ6Kl6Mg3k6nJTUtwqs9gDxBP_8Dk,1920
|
335
338
|
reconcile/gql_definitions/terraform_cloudflare_dns/terraform_cloudflare_zones.py,sha256=uVZYu5EUcvdAQYBK5YKD0mjoMKDb5inSuCJrrOD5KpE,5704
|
@@ -416,16 +419,17 @@ reconcile/templates/jira-checkpoint-missinginfo.j2,sha256=c_Vvg-lEENsB3tgxm9B6Y9
|
|
416
419
|
reconcile/templates/rosa-classic-cluster-creation.sh.j2,sha256=0UHfYtXRVJqP07VJQx456cRI6EbZNBgamtP_8nb4WPY,2353
|
417
420
|
reconcile/templates/rosa-hcp-cluster-creation.sh.j2,sha256=O7Bf3WQIJhsZoEqaYA0wRktUO4yXXCb4BQkuvvp-C80,2385
|
418
421
|
reconcile/templating/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
419
|
-
reconcile/templating/renderer.py,sha256=
|
420
|
-
reconcile/templating/validator.py,sha256=
|
422
|
+
reconcile/templating/renderer.py,sha256=2FWbnefT2siozQpXXkuvVKUo6cePMqLY4BMYpqXg6xM,10652
|
423
|
+
reconcile/templating/validator.py,sha256=pvDEc6veznEZzjypkoRJUGMMFLWosU-zd7i3j7JeNjE,4670
|
421
424
|
reconcile/templating/lib/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
422
|
-
reconcile/templating/lib/merge_request_manager.py,sha256=
|
423
|
-
reconcile/templating/lib/model.py,sha256=
|
425
|
+
reconcile/templating/lib/merge_request_manager.py,sha256=JUkfF3smaQ8onzKF5F7UpmA7MWaQpftANy6dDo1FCug,5464
|
426
|
+
reconcile/templating/lib/model.py,sha256=fb6FYYLQjmoh2DjVKO7TEWCuDPf1Q34xmOx0M9Z07ek,324
|
424
427
|
reconcile/templating/lib/rendering.py,sha256=_BVQ2gqip8K1AgLYfaTWh8NKJFTW6VjUZ6rBI_GH30E,5061
|
425
428
|
reconcile/test/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
426
429
|
reconcile/test/conftest.py,sha256=rQousYrxUz-EwAIbsYO6bIwR1B4CrOz9y_zaUVo2lfI,4466
|
427
430
|
reconcile/test/fixtures.py,sha256=9SDWAUlSd1rCx7z3GhULHcpr-I6FyCsXxaFAZIqYQsQ,591
|
428
|
-
reconcile/test/
|
431
|
+
reconcile/test/test_acs_notifiers.py,sha256=LIVXu7EBlfKh5CWgR5jcA0vJ4IhUtxkZM6-1im204rc,12865
|
432
|
+
reconcile/test/test_acs_policies.py,sha256=9TPGbF4mS2B18S7ldibZrugztaTQvcrBFvzs5eXbN-E,19211
|
429
433
|
reconcile/test/test_acs_rbac.py,sha256=lvNd8GY0-GHzcOdOn13QWdrqbBXXKzNT7EEDHNH7cjM,28272
|
430
434
|
reconcile/test/test_aggregated_list.py,sha256=iiWitQuNYC58aimWaiBoE4NROHjr1NCgQ91MnHEG_Ro,6412
|
431
435
|
reconcile/test/test_amtool.py,sha256=vxRhGieeydMBOb9UI2ziMHjJa8puMeGNsUhGhy-yMnk,1032
|
@@ -655,7 +659,8 @@ reconcile/utils/vault.py,sha256=S0eHqvZ9N3fya1E8YDaUffEvLk_fdtpzL4rvWn6f828,1499
|
|
655
659
|
reconcile/utils/vaultsecretref.py,sha256=3Ed2uBy36TzSvL0B-l4FoWQqB2SbBKDKEuUPIO608Bo,931
|
656
660
|
reconcile/utils/vcs.py,sha256=o1r0n_IrU2El75CED_6sjR2GZGM-exuWsj5F7jONaMU,6779
|
657
661
|
reconcile/utils/acs/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
658
|
-
reconcile/utils/acs/base.py,sha256=
|
662
|
+
reconcile/utils/acs/base.py,sha256=kjcxLGIMe8oLNFOxZ_bDcClFqGCL7Auwug5is0yNGbw,2555
|
663
|
+
reconcile/utils/acs/notifiers.py,sha256=LfWw9LGq7hA90A69n8Ie9f-NozvCGdYwXEXRLIc17rY,3735
|
659
664
|
reconcile/utils/acs/policies.py,sha256=_jAz6cv8KRYtDsXjGoJgNbD8_9PUa5LSwwVlpK4A_cQ,5505
|
660
665
|
reconcile/utils/acs/rbac.py,sha256=ugsLM9Pb7FbUbdq85E3VzXGMaB9ZovXob7tdWCxwqZ8,8808
|
661
666
|
reconcile/utils/aws_api_typed/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
@@ -765,8 +770,8 @@ tools/test/test_app_interface_metrics_exporter.py,sha256=SX7qL3D1SIRKFo95FoQztvf
|
|
765
770
|
tools/test/test_qontract_cli.py,sha256=w2l4BHB09k1d-BGJ1jBUNCqDv7zkqYrMHojQXg-21kQ,4155
|
766
771
|
tools/test/test_sd_app_sre_alert_report.py,sha256=v363r9zM7__0kR5K6mvJoGFcM9BvE33fWAayrqkpojA,2116
|
767
772
|
tools/test/test_sre_checkpoints.py,sha256=SKqPPTl9ua0RFdSSofnoQX-JZE6dFLO3LRhfQzqtfh8,2607
|
768
|
-
qontract_reconcile-0.10.
|
769
|
-
qontract_reconcile-0.10.
|
770
|
-
qontract_reconcile-0.10.
|
771
|
-
qontract_reconcile-0.10.
|
772
|
-
qontract_reconcile-0.10.
|
773
|
+
qontract_reconcile-0.10.1rc736.dist-info/METADATA,sha256=HGDJxGb77YYojenQNeOYhLfp3EOLLqkOwx8Tat8ONWA,2382
|
774
|
+
qontract_reconcile-0.10.1rc736.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
|
775
|
+
qontract_reconcile-0.10.1rc736.dist-info/entry_points.txt,sha256=rIxI5zWtHNlfpDeq1a7pZXAPoqf7HG32KMTN3MeWK_8,429
|
776
|
+
qontract_reconcile-0.10.1rc736.dist-info/top_level.txt,sha256=l5ISPoXzt0SdR4jVdkfa7RPSKNc8zAHYWAnR-Dw8Ey8,24
|
777
|
+
qontract_reconcile-0.10.1rc736.dist-info/RECORD,,
|
@@ -0,0 +1,156 @@
|
|
1
|
+
import logging
|
2
|
+
from typing import Any
|
3
|
+
|
4
|
+
import reconcile.gql_definitions.acs.acs_policies as gql_acs_policies
|
5
|
+
from reconcile.gql_definitions.jira.jira_servers import (
|
6
|
+
JiraServerV1,
|
7
|
+
)
|
8
|
+
from reconcile.gql_definitions.jira.jira_servers import (
|
9
|
+
query as query_jira_servers,
|
10
|
+
)
|
11
|
+
from reconcile.utils import gql
|
12
|
+
from reconcile.utils.acs.notifiers import (
|
13
|
+
AcsNotifiersApi,
|
14
|
+
JiraCredentials,
|
15
|
+
JiraNotifier,
|
16
|
+
SeverityPriorityMapping,
|
17
|
+
)
|
18
|
+
from reconcile.utils.differ import diff_iterables
|
19
|
+
from reconcile.utils.disabled_integrations import integration_is_enabled
|
20
|
+
from reconcile.utils.runtime.integration import (
|
21
|
+
NoParams,
|
22
|
+
QontractReconcileIntegration,
|
23
|
+
)
|
24
|
+
from reconcile.utils.semver_helper import make_semver
|
25
|
+
|
26
|
+
|
27
|
+
class AcsNotifiersIntegration(QontractReconcileIntegration[NoParams]):
|
28
|
+
def __init__(self) -> None:
|
29
|
+
super().__init__(NoParams())
|
30
|
+
self.qontract_integration = "acs-notifiers"
|
31
|
+
self.qontract_integration_version = make_semver(0, 1, 0)
|
32
|
+
|
33
|
+
@property
|
34
|
+
def name(self) -> str:
|
35
|
+
return self.qontract_integration
|
36
|
+
|
37
|
+
def _get_escalation_policies(
|
38
|
+
self, acs_policies: list[gql_acs_policies.AcsPolicyV1]
|
39
|
+
) -> list[gql_acs_policies.AppEscalationPolicyV1]:
|
40
|
+
return list(
|
41
|
+
{
|
42
|
+
p.integrations.notifiers.jira.escalation_policy.name: p.integrations.notifiers.jira.escalation_policy
|
43
|
+
for p in acs_policies
|
44
|
+
if p.integrations
|
45
|
+
and p.integrations.notifiers
|
46
|
+
and p.integrations.notifiers.jira
|
47
|
+
and integration_is_enabled(
|
48
|
+
self.qontract_integration,
|
49
|
+
p.integrations.notifiers.jira.escalation_policy.channels.jira_board[
|
50
|
+
0
|
51
|
+
],
|
52
|
+
)
|
53
|
+
}.values()
|
54
|
+
)
|
55
|
+
|
56
|
+
def _build_jira_notifier(
|
57
|
+
self, escalation_policy: gql_acs_policies.AppEscalationPolicyV1
|
58
|
+
) -> JiraNotifier:
|
59
|
+
jira_board = escalation_policy.channels.jira_board[0]
|
60
|
+
|
61
|
+
custom_fields: dict[str, Any] = {}
|
62
|
+
if jira_board.issue_security_id:
|
63
|
+
custom_fields["security"] = {"id": jira_board.issue_security_id}
|
64
|
+
if escalation_policy.channels.jira_component:
|
65
|
+
custom_fields["components"] = [
|
66
|
+
{"name": escalation_policy.channels.jira_component}
|
67
|
+
]
|
68
|
+
if escalation_policy.channels.jira_labels:
|
69
|
+
custom_fields["labels"] = escalation_policy.channels.jira_labels
|
70
|
+
|
71
|
+
item = JiraNotifier(
|
72
|
+
name=f"jira-{escalation_policy.name}",
|
73
|
+
board=jira_board.name,
|
74
|
+
url=jira_board.server.server_url,
|
75
|
+
issue_type=jira_board.issue_type or "Task",
|
76
|
+
severity_priority_mappings=sorted(
|
77
|
+
[
|
78
|
+
SeverityPriorityMapping(**vars(sp))
|
79
|
+
for sp in jira_board.severity_priority_mappings.mappings
|
80
|
+
],
|
81
|
+
key=lambda m: m.severity,
|
82
|
+
),
|
83
|
+
custom_fields=custom_fields,
|
84
|
+
)
|
85
|
+
|
86
|
+
return item
|
87
|
+
|
88
|
+
def get_desired_state(
|
89
|
+
self, acs_policies: list[gql_acs_policies.AcsPolicyV1]
|
90
|
+
) -> list[JiraNotifier]:
|
91
|
+
return [
|
92
|
+
self._build_jira_notifier(ep)
|
93
|
+
for ep in self._get_escalation_policies(acs_policies)
|
94
|
+
]
|
95
|
+
|
96
|
+
def get_jira_credentials(
|
97
|
+
self, jira_servers: list[JiraServerV1]
|
98
|
+
) -> dict[str, JiraCredentials]:
|
99
|
+
return {
|
100
|
+
server.server_url: JiraCredentials(
|
101
|
+
url=server.server_url,
|
102
|
+
username=server.username,
|
103
|
+
token=self.secret_reader.read_secret(server.token),
|
104
|
+
)
|
105
|
+
for server in jira_servers
|
106
|
+
}
|
107
|
+
|
108
|
+
def reconcile(
|
109
|
+
self,
|
110
|
+
current_state: list[JiraNotifier],
|
111
|
+
desired_state: list[JiraNotifier],
|
112
|
+
acs_api: AcsNotifiersApi,
|
113
|
+
jira_credentials: dict[str, JiraCredentials],
|
114
|
+
dry_run: bool,
|
115
|
+
) -> None:
|
116
|
+
diff = diff_iterables(
|
117
|
+
current=current_state, desired=desired_state, key=lambda x: x.name
|
118
|
+
)
|
119
|
+
for a in diff.add.values():
|
120
|
+
logging.info(f"Create Jira notifier: {a.name}")
|
121
|
+
if not dry_run:
|
122
|
+
acs_api.create_jira_notifier(
|
123
|
+
a,
|
124
|
+
jira_credentials=jira_credentials[a.url],
|
125
|
+
)
|
126
|
+
for c in diff.change.values():
|
127
|
+
logging.info(f"Update Jira notifier: {c.desired.name}")
|
128
|
+
if not dry_run:
|
129
|
+
acs_api.update_jira_notifier(
|
130
|
+
c.desired,
|
131
|
+
jira_credentials=jira_credentials[c.desired.url],
|
132
|
+
)
|
133
|
+
for d in diff.delete.values():
|
134
|
+
logging.info(f"Delete Jira notifier: {d.name}")
|
135
|
+
if not dry_run:
|
136
|
+
acs_api.delete_jira_notifier(d)
|
137
|
+
|
138
|
+
def run(
|
139
|
+
self,
|
140
|
+
dry_run: bool,
|
141
|
+
) -> None:
|
142
|
+
gql_api_query = gql.get_api().query
|
143
|
+
jira_credentials = self.get_jira_credentials(
|
144
|
+
query_jira_servers(query_func=gql_api_query).jira_servers or []
|
145
|
+
)
|
146
|
+
desired_state = self.get_desired_state(
|
147
|
+
gql_acs_policies.query(query_func=gql_api_query).acs_policies or []
|
148
|
+
)
|
149
|
+
instance = AcsNotifiersApi.get_acs_instance(query_func=gql_api_query)
|
150
|
+
with AcsNotifiersApi(
|
151
|
+
url=instance.url, token=self.secret_reader.read_secret(instance.credentials)
|
152
|
+
) as acs_api:
|
153
|
+
current_state = acs_api.get_jira_notifiers()
|
154
|
+
self.reconcile(
|
155
|
+
desired_state, current_state, acs_api, jira_credentials, dry_run
|
156
|
+
)
|
reconcile/cli.py
CHANGED
@@ -3441,6 +3441,17 @@ def acs_policies(ctx):
|
|
3441
3441
|
)
|
3442
3442
|
|
3443
3443
|
|
3444
|
+
@integration.command(short_help="Manages RHACS notifier configurations")
|
3445
|
+
@click.pass_context
|
3446
|
+
def acs_notifiers(ctx):
|
3447
|
+
from reconcile import acs_notifiers
|
3448
|
+
|
3449
|
+
run_class_integration(
|
3450
|
+
integration=acs_notifiers.AcsNotifiersIntegration(),
|
3451
|
+
ctx=ctx.obj,
|
3452
|
+
)
|
3453
|
+
|
3454
|
+
|
3444
3455
|
@integration.command(short_help="Automate Deadmanssnitch Creation/Deletion")
|
3445
3456
|
@click.pass_context
|
3446
3457
|
def deadmanssnitch(ctx):
|
@@ -25,6 +25,37 @@ query AcsPolicy {
|
|
25
25
|
description
|
26
26
|
severity
|
27
27
|
notifiers
|
28
|
+
integrations {
|
29
|
+
notifiers {
|
30
|
+
jira {
|
31
|
+
escalationPolicy {
|
32
|
+
name
|
33
|
+
channels {
|
34
|
+
jiraBoard {
|
35
|
+
name
|
36
|
+
server {
|
37
|
+
serverUrl
|
38
|
+
}
|
39
|
+
severityPriorityMappings {
|
40
|
+
name
|
41
|
+
mappings {
|
42
|
+
severity
|
43
|
+
priority
|
44
|
+
}
|
45
|
+
}
|
46
|
+
issueType
|
47
|
+
issueSecurityId
|
48
|
+
disable {
|
49
|
+
integrations
|
50
|
+
}
|
51
|
+
}
|
52
|
+
jiraComponent
|
53
|
+
jiraLabels
|
54
|
+
}
|
55
|
+
}
|
56
|
+
}
|
57
|
+
}
|
58
|
+
}
|
28
59
|
categories
|
29
60
|
scope {
|
30
61
|
level
|
@@ -74,6 +105,56 @@ class ConfiguredBaseModel(BaseModel):
|
|
74
105
|
extra=Extra.forbid
|
75
106
|
|
76
107
|
|
108
|
+
class JiraServerV1(ConfiguredBaseModel):
|
109
|
+
server_url: str = Field(..., alias="serverUrl")
|
110
|
+
|
111
|
+
|
112
|
+
class SeverityPriorityMappingV1(ConfiguredBaseModel):
|
113
|
+
severity: str = Field(..., alias="severity")
|
114
|
+
priority: str = Field(..., alias="priority")
|
115
|
+
|
116
|
+
|
117
|
+
class JiraSeverityPriorityMappingsV1(ConfiguredBaseModel):
|
118
|
+
name: str = Field(..., alias="name")
|
119
|
+
mappings: list[SeverityPriorityMappingV1] = Field(..., alias="mappings")
|
120
|
+
|
121
|
+
|
122
|
+
class DisableJiraBoardAutomationsV1(ConfiguredBaseModel):
|
123
|
+
integrations: Optional[list[str]] = Field(..., alias="integrations")
|
124
|
+
|
125
|
+
|
126
|
+
class JiraBoardV1(ConfiguredBaseModel):
|
127
|
+
name: str = Field(..., alias="name")
|
128
|
+
server: JiraServerV1 = Field(..., alias="server")
|
129
|
+
severity_priority_mappings: JiraSeverityPriorityMappingsV1 = Field(..., alias="severityPriorityMappings")
|
130
|
+
issue_type: Optional[str] = Field(..., alias="issueType")
|
131
|
+
issue_security_id: Optional[str] = Field(..., alias="issueSecurityId")
|
132
|
+
disable: Optional[DisableJiraBoardAutomationsV1] = Field(..., alias="disable")
|
133
|
+
|
134
|
+
|
135
|
+
class AppEscalationPolicyChannelsV1(ConfiguredBaseModel):
|
136
|
+
jira_board: list[JiraBoardV1] = Field(..., alias="jiraBoard")
|
137
|
+
jira_component: Optional[str] = Field(..., alias="jiraComponent")
|
138
|
+
jira_labels: Optional[list[str]] = Field(..., alias="jiraLabels")
|
139
|
+
|
140
|
+
|
141
|
+
class AppEscalationPolicyV1(ConfiguredBaseModel):
|
142
|
+
name: str = Field(..., alias="name")
|
143
|
+
channels: AppEscalationPolicyChannelsV1 = Field(..., alias="channels")
|
144
|
+
|
145
|
+
|
146
|
+
class AcsPolicyIntegrationNotifierJiraV1(ConfiguredBaseModel):
|
147
|
+
escalation_policy: AppEscalationPolicyV1 = Field(..., alias="escalationPolicy")
|
148
|
+
|
149
|
+
|
150
|
+
class AcsPolicyIntegrationNotifiersV1(ConfiguredBaseModel):
|
151
|
+
jira: Optional[AcsPolicyIntegrationNotifierJiraV1] = Field(..., alias="jira")
|
152
|
+
|
153
|
+
|
154
|
+
class AcsPolicyIntegrationsV1(ConfiguredBaseModel):
|
155
|
+
notifiers: Optional[AcsPolicyIntegrationNotifiersV1] = Field(..., alias="notifiers")
|
156
|
+
|
157
|
+
|
77
158
|
class AcsPolicyScopeV1(ConfiguredBaseModel):
|
78
159
|
level: str = Field(..., alias="level")
|
79
160
|
|
@@ -131,6 +212,7 @@ class AcsPolicyV1(ConfiguredBaseModel):
|
|
131
212
|
description: Optional[str] = Field(..., alias="description")
|
132
213
|
severity: str = Field(..., alias="severity")
|
133
214
|
notifiers: Optional[list[str]] = Field(..., alias="notifiers")
|
215
|
+
integrations: Optional[AcsPolicyIntegrationsV1] = Field(..., alias="integrations")
|
134
216
|
categories: list[str] = Field(..., alias="categories")
|
135
217
|
scope: Union[AcsPolicyScopeClusterV1, AcsPolicyScopeNamespaceV1, AcsPolicyScopeV1] = Field(..., alias="scope")
|
136
218
|
conditions: list[Union[AcsPolicyConditionsCvssV1, AcsPolicyConditionsSeverityV1, AcsPolicyConditionsImageTagV1, AcsPolicyConditionsCveV1, AcsPolicyConditionsImageAgeV1, AcsPolicyConditionsV1]] = Field(..., alias="conditions")
|
File without changes
|
@@ -0,0 +1,76 @@
|
|
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 JiraServers {
|
23
|
+
jira_servers: jira_servers_v1 {
|
24
|
+
serverUrl
|
25
|
+
username
|
26
|
+
token {
|
27
|
+
path
|
28
|
+
version
|
29
|
+
field
|
30
|
+
format
|
31
|
+
}
|
32
|
+
}
|
33
|
+
}
|
34
|
+
"""
|
35
|
+
|
36
|
+
|
37
|
+
class ConfiguredBaseModel(BaseModel):
|
38
|
+
class Config:
|
39
|
+
smart_union=True
|
40
|
+
extra=Extra.forbid
|
41
|
+
|
42
|
+
|
43
|
+
class VaultSecretV1(ConfiguredBaseModel):
|
44
|
+
path: str = Field(..., alias="path")
|
45
|
+
version: Optional[int] = Field(..., alias="version")
|
46
|
+
field: str = Field(..., alias="field")
|
47
|
+
q_format: Optional[str] = Field(..., alias="format")
|
48
|
+
|
49
|
+
|
50
|
+
class JiraServerV1(ConfiguredBaseModel):
|
51
|
+
server_url: str = Field(..., alias="serverUrl")
|
52
|
+
username: str = Field(..., alias="username")
|
53
|
+
token: VaultSecretV1 = Field(..., alias="token")
|
54
|
+
|
55
|
+
|
56
|
+
class JiraServersQueryData(ConfiguredBaseModel):
|
57
|
+
jira_servers: Optional[list[JiraServerV1]] = Field(..., alias="jira_servers")
|
58
|
+
|
59
|
+
|
60
|
+
def query(query_func: Callable, **kwargs: Any) -> JiraServersQueryData:
|
61
|
+
"""
|
62
|
+
This is a convenience function which queries and parses the data into
|
63
|
+
concrete types. It should be compatible with most GQL clients.
|
64
|
+
You do not have to use it to consume the generated data classes.
|
65
|
+
Alternatively, you can also mime and alternate the behavior
|
66
|
+
of this function in the caller.
|
67
|
+
|
68
|
+
Parameters:
|
69
|
+
query_func (Callable): Function which queries your GQL Server
|
70
|
+
kwargs: optional arguments that will be passed to the query function
|
71
|
+
|
72
|
+
Returns:
|
73
|
+
JiraServersQueryData: queried data parsed into generated classes
|
74
|
+
"""
|
75
|
+
raw_data: dict[Any, Any] = query_func(DEFINITION, **kwargs)
|
76
|
+
return JiraServersQueryData(**raw_data)
|
@@ -22,7 +22,9 @@ DEFINITION = """
|
|
22
22
|
query TemplateCollection_v1 {
|
23
23
|
template_collection_v1 {
|
24
24
|
name
|
25
|
+
additionalMrLabels
|
25
26
|
description
|
27
|
+
enableAutoApproval
|
26
28
|
variables {
|
27
29
|
static
|
28
30
|
dynamic {
|
@@ -32,6 +34,7 @@ query TemplateCollection_v1 {
|
|
32
34
|
}
|
33
35
|
templates {
|
34
36
|
name
|
37
|
+
autoApproved
|
35
38
|
condition
|
36
39
|
targetPath
|
37
40
|
patch {
|
@@ -68,6 +71,7 @@ class TemplatePatchV1(ConfiguredBaseModel):
|
|
68
71
|
|
69
72
|
class TemplateV1(ConfiguredBaseModel):
|
70
73
|
name: str = Field(..., alias="name")
|
74
|
+
auto_approved: Optional[bool] = Field(..., alias="autoApproved")
|
71
75
|
condition: Optional[str] = Field(..., alias="condition")
|
72
76
|
target_path: str = Field(..., alias="targetPath")
|
73
77
|
patch: Optional[TemplatePatchV1] = Field(..., alias="patch")
|
@@ -76,7 +80,9 @@ class TemplateV1(ConfiguredBaseModel):
|
|
76
80
|
|
77
81
|
class TemplateCollectionV1(ConfiguredBaseModel):
|
78
82
|
name: str = Field(..., alias="name")
|
83
|
+
additional_mr_labels: Optional[list[str]] = Field(..., alias="additionalMrLabels")
|
79
84
|
description: str = Field(..., alias="description")
|
85
|
+
enable_auto_approval: Optional[bool] = Field(..., alias="enableAutoApproval")
|
80
86
|
variables: Optional[TemplateCollectionVariablesV1] = Field(..., alias="variables")
|
81
87
|
templates: list[TemplateV1] = Field(..., alias="templates")
|
82
88
|
|
@@ -22,6 +22,7 @@ DEFINITION = """
|
|
22
22
|
query Templatev1 {
|
23
23
|
template_v1 {
|
24
24
|
name
|
25
|
+
autoApproved
|
25
26
|
condition
|
26
27
|
patch {
|
27
28
|
path
|
@@ -64,6 +65,7 @@ class TemplateTestV1(ConfiguredBaseModel):
|
|
64
65
|
|
65
66
|
class TemplateV1(ConfiguredBaseModel):
|
66
67
|
name: str = Field(..., alias="name")
|
68
|
+
auto_approved: Optional[bool] = Field(..., alias="autoApproved")
|
67
69
|
condition: Optional[str] = Field(..., alias="condition")
|
68
70
|
patch: Optional[TemplatePatchV1] = Field(..., alias="patch")
|
69
71
|
target_path: str = Field(..., alias="targetPath")
|
@@ -13,6 +13,7 @@ from reconcile.utils.merge_request_manager.parser import (
|
|
13
13
|
Parser,
|
14
14
|
)
|
15
15
|
from reconcile.utils.mr import MergeRequestBase
|
16
|
+
from reconcile.utils.mr.labels import AUTO_MERGE
|
16
17
|
from reconcile.utils.vcs import VCS
|
17
18
|
|
18
19
|
DATA_SEPARATOR = (
|
@@ -120,16 +121,23 @@ class TemplateRenderingMR(MergeRequestBase):
|
|
120
121
|
)
|
121
122
|
|
122
123
|
|
124
|
+
class MrData(BaseModel):
|
125
|
+
data: list[TemplateOutput]
|
126
|
+
auto_approved: bool
|
127
|
+
|
128
|
+
|
123
129
|
class MergeRequestManager(MergeRequestManagerBase[TemplateInfo]):
|
124
130
|
def __init__(self, vcs: VCS, parser: Parser):
|
125
131
|
super().__init__(vcs, parser, TR_LABEL)
|
126
132
|
|
127
|
-
def create_merge_request(self,
|
133
|
+
def create_merge_request(self, data: MrData) -> None:
|
128
134
|
if not self._housekeeping_ran:
|
129
135
|
self.housekeeping()
|
130
136
|
|
131
|
-
|
132
|
-
|
137
|
+
output = data.data
|
138
|
+
collections = {o.input.collection for o in output}
|
139
|
+
collection_hashes = {o.input.collection_hash for o in output}
|
140
|
+
additional_labels = {label for o in output for label in o.input.labels}
|
133
141
|
# From the way the code is written, we can assert that there is only one collection and one template hash
|
134
142
|
assert len(collections) == 1
|
135
143
|
assert len(collection_hashes) == 1
|
@@ -158,6 +166,12 @@ class MergeRequestManager(MergeRequestManagerBase[TemplateInfo]):
|
|
158
166
|
logging.info("Opening MR for %s with hash (%s)", collection, collection_hash)
|
159
167
|
mr_labels = [TR_LABEL]
|
160
168
|
|
169
|
+
if data.auto_approved:
|
170
|
+
mr_labels.append(AUTO_MERGE)
|
171
|
+
|
172
|
+
if additional_labels:
|
173
|
+
mr_labels.extend(additional_labels)
|
174
|
+
|
161
175
|
self._vcs.open_app_interface_merge_request(
|
162
176
|
mr=TemplateRenderingMR(
|
163
177
|
title=title,
|
@@ -1,15 +1,16 @@
|
|
1
|
-
from typing import Optional
|
2
|
-
|
3
1
|
from pydantic import BaseModel
|
4
2
|
|
5
3
|
|
6
4
|
class TemplateInput(BaseModel):
|
7
5
|
collection: str
|
8
6
|
collection_hash: str
|
7
|
+
enable_auto_approval: bool = False
|
8
|
+
labels: list[str] = []
|
9
9
|
|
10
10
|
|
11
11
|
class TemplateOutput(BaseModel):
|
12
|
-
input:
|
12
|
+
input: TemplateInput
|
13
13
|
is_new: bool = False
|
14
14
|
path: str
|
15
15
|
content: str
|
16
|
+
auto_approved: bool = False
|
reconcile/templating/renderer.py
CHANGED
@@ -17,6 +17,7 @@ from reconcile.gql_definitions.templating.template_collection import (
|
|
17
17
|
)
|
18
18
|
from reconcile.templating.lib.merge_request_manager import (
|
19
19
|
MergeRequestManager,
|
20
|
+
MrData,
|
20
21
|
create_parser,
|
21
22
|
)
|
22
23
|
from reconcile.templating.lib.model import TemplateInput, TemplateOutput
|
@@ -59,6 +60,19 @@ class FilePersistence(ABC):
|
|
59
60
|
def read(self, path: str) -> Optional[str]:
|
60
61
|
pass
|
61
62
|
|
63
|
+
@staticmethod
|
64
|
+
def _read_local_file(path: str) -> Optional[str]:
|
65
|
+
try:
|
66
|
+
with open(
|
67
|
+
path,
|
68
|
+
"r",
|
69
|
+
encoding="utf-8",
|
70
|
+
) as f:
|
71
|
+
return f.read()
|
72
|
+
except FileNotFoundError:
|
73
|
+
logging.debug(f"File not found: {path}, need to create it")
|
74
|
+
return None
|
75
|
+
|
62
76
|
|
63
77
|
class LocalFilePersistence(FilePersistence):
|
64
78
|
"""
|
@@ -80,16 +94,7 @@ class LocalFilePersistence(FilePersistence):
|
|
80
94
|
f.write(output.content)
|
81
95
|
|
82
96
|
def read(self, path: str) -> Optional[str]:
|
83
|
-
|
84
|
-
with open(
|
85
|
-
f"{join_path(self.app_interface_data_path, path)}",
|
86
|
-
"r",
|
87
|
-
encoding="utf-8",
|
88
|
-
) as f:
|
89
|
-
return f.read()
|
90
|
-
except FileNotFoundError:
|
91
|
-
logging.debug(f"File not found: {path}, need to create it")
|
92
|
-
return None
|
97
|
+
return self._read_local_file(join_path(self.app_interface_data_path, path))
|
93
98
|
|
94
99
|
|
95
100
|
class PersistenceTransaction(FilePersistence):
|
@@ -127,6 +132,8 @@ class ClonedRepoGitlabPersistence(FilePersistence):
|
|
127
132
|
"""
|
128
133
|
This class is used to persist the rendered templates in a cloned gitlab repo
|
129
134
|
Reads are from the local filesystem, writes are done via utils.VCS abstraction
|
135
|
+
|
136
|
+
Only one MR is created per run. Auto-approval MRs are prefered.
|
130
137
|
"""
|
131
138
|
|
132
139
|
def __init__(self, local_path: str, vcs: VCS, mr_manager: MergeRequestManager):
|
@@ -136,19 +143,19 @@ class ClonedRepoGitlabPersistence(FilePersistence):
|
|
136
143
|
|
137
144
|
def write(self, outputs: list[TemplateOutput]) -> None:
|
138
145
|
self.mr_manager.housekeeping()
|
139
|
-
|
146
|
+
|
147
|
+
if any([o.input.enable_auto_approval for o in outputs]):
|
148
|
+
auto_approved = [o for o in outputs if o.auto_approved]
|
149
|
+
if auto_approved:
|
150
|
+
self.mr_manager.create_merge_request(
|
151
|
+
MrData(data=auto_approved, auto_approved=True)
|
152
|
+
)
|
153
|
+
return
|
154
|
+
|
155
|
+
self.mr_manager.create_merge_request(MrData(data=outputs, auto_approved=False))
|
140
156
|
|
141
157
|
def read(self, path: str) -> Optional[str]:
|
142
|
-
|
143
|
-
with open(
|
144
|
-
f"{join_path(self.local_path, path)}",
|
145
|
-
"r",
|
146
|
-
encoding="utf-8",
|
147
|
-
) as f:
|
148
|
-
return f.read()
|
149
|
-
except FileNotFoundError:
|
150
|
-
logging.debug(f"File not found: {path}, need to create it")
|
151
|
-
return None
|
158
|
+
return self._read_local_file(join_path(self.local_path, path))
|
152
159
|
|
153
160
|
|
154
161
|
def unpack_static_variables(
|
@@ -187,6 +194,7 @@ class TemplateRendererIntegration(QontractReconcileIntegration):
|
|
187
194
|
variables: dict,
|
188
195
|
persistence: FilePersistence,
|
189
196
|
ruaml_instance: yaml.YAML,
|
197
|
+
template_input: TemplateInput,
|
190
198
|
) -> Optional[TemplateOutput]:
|
191
199
|
r = create_renderer(
|
192
200
|
template,
|
@@ -217,6 +225,8 @@ class TemplateRendererIntegration(QontractReconcileIntegration):
|
|
217
225
|
path=target_path,
|
218
226
|
content=output,
|
219
227
|
is_new=current_str is None,
|
228
|
+
auto_approved=template.auto_approved or False,
|
229
|
+
input=template_input,
|
220
230
|
)
|
221
231
|
return None
|
222
232
|
|
@@ -244,18 +254,17 @@ class TemplateRendererIntegration(QontractReconcileIntegration):
|
|
244
254
|
).hexdigest()
|
245
255
|
|
246
256
|
with PersistenceTransaction(persistence, dry_run) as p:
|
257
|
+
input = TemplateInput(
|
258
|
+
collection=c.name,
|
259
|
+
collection_hash=template_hash,
|
260
|
+
enable_auto_approval=c.enable_auto_approval or False,
|
261
|
+
labels=c.additional_mr_labels or [],
|
262
|
+
)
|
247
263
|
for template in c.templates:
|
248
264
|
output = self.process_template(
|
249
|
-
template,
|
250
|
-
variables,
|
251
|
-
p,
|
252
|
-
ruamel_instance,
|
265
|
+
template, variables, p, ruamel_instance, input
|
253
266
|
)
|
254
267
|
if output:
|
255
|
-
output.input = TemplateInput(
|
256
|
-
collection=c.name,
|
257
|
-
collection_hash=template_hash,
|
258
|
-
)
|
259
268
|
outputs.append(output)
|
260
269
|
|
261
270
|
if not dry_run:
|
@@ -127,7 +127,11 @@ class TemplateValidatorIntegration(QontractReconcileIntegration):
|
|
127
127
|
if diffs:
|
128
128
|
for diff in diffs:
|
129
129
|
logging.error(f"template: {diff.template}, test: {diff.test}")
|
130
|
-
|
130
|
+
# This log should never be added except for local debugging.
|
131
|
+
# Credentials could be leaked, i.e. creating an MR with a diff,
|
132
|
+
# using a template, that uses the vault function.
|
133
|
+
# Use template-validator CLI instead.
|
134
|
+
# logging.debug(diff.diff)
|
131
135
|
raise ValueError("Template validation failed")
|
132
136
|
|
133
137
|
@property
|
@@ -0,0 +1,399 @@
|
|
1
|
+
import copy
|
2
|
+
from typing import Any
|
3
|
+
|
4
|
+
import pytest
|
5
|
+
from pytest_mock import MockerFixture
|
6
|
+
|
7
|
+
from reconcile.acs_notifiers import AcsNotifiersIntegration
|
8
|
+
from reconcile.gql_definitions.acs.acs_policies import (
|
9
|
+
AcsPolicyIntegrationNotifierJiraV1,
|
10
|
+
AcsPolicyIntegrationNotifiersV1,
|
11
|
+
AcsPolicyIntegrationsV1,
|
12
|
+
AcsPolicyScopeClusterV1,
|
13
|
+
AcsPolicyScopeNamespaceV1,
|
14
|
+
AcsPolicyV1,
|
15
|
+
AppEscalationPolicyChannelsV1,
|
16
|
+
AppEscalationPolicyV1,
|
17
|
+
DisableJiraBoardAutomationsV1,
|
18
|
+
JiraBoardV1,
|
19
|
+
JiraServerV1,
|
20
|
+
JiraSeverityPriorityMappingsV1,
|
21
|
+
SeverityPriorityMappingV1,
|
22
|
+
)
|
23
|
+
from reconcile.utils.acs.notifiers import (
|
24
|
+
AcsNotifiersApi,
|
25
|
+
JiraCredentials,
|
26
|
+
JiraNotifier,
|
27
|
+
SeverityPriorityMapping,
|
28
|
+
)
|
29
|
+
|
30
|
+
|
31
|
+
@pytest.fixture
|
32
|
+
def severity_priority_mapping() -> SeverityPriorityMapping:
|
33
|
+
return SeverityPriorityMapping(severity="critical", priority="Critical")
|
34
|
+
|
35
|
+
|
36
|
+
@pytest.fixture
|
37
|
+
def severity_priority_mapping_api_payload() -> dict[str, str]:
|
38
|
+
return {
|
39
|
+
"severity": "CRITICAL_SEVERITY",
|
40
|
+
"priorityName": "Critical",
|
41
|
+
}
|
42
|
+
|
43
|
+
|
44
|
+
def test_severity_priority_mappings_to_api(
|
45
|
+
severity_priority_mapping: SeverityPriorityMapping,
|
46
|
+
severity_priority_mapping_api_payload: dict[str, str],
|
47
|
+
) -> None:
|
48
|
+
assert severity_priority_mapping.to_api() == severity_priority_mapping_api_payload
|
49
|
+
|
50
|
+
|
51
|
+
def test_severity_priority_mappings_from_api(
|
52
|
+
severity_priority_mapping: SeverityPriorityMapping,
|
53
|
+
severity_priority_mapping_api_payload: dict[str, str],
|
54
|
+
) -> None:
|
55
|
+
assert (
|
56
|
+
SeverityPriorityMapping.from_api(severity_priority_mapping_api_payload)
|
57
|
+
== severity_priority_mapping
|
58
|
+
)
|
59
|
+
|
60
|
+
|
61
|
+
@pytest.fixture
|
62
|
+
def jira_credentials() -> JiraCredentials:
|
63
|
+
return JiraCredentials(
|
64
|
+
url="https://jira.example.com",
|
65
|
+
username="jirabot",
|
66
|
+
token="topsecret",
|
67
|
+
)
|
68
|
+
|
69
|
+
|
70
|
+
@pytest.fixture
|
71
|
+
def jira_notifier(
|
72
|
+
severity_priority_mapping: SeverityPriorityMapping,
|
73
|
+
jira_credentials: JiraCredentials,
|
74
|
+
) -> JiraNotifier:
|
75
|
+
return JiraNotifier(
|
76
|
+
name="jira-notifier-1",
|
77
|
+
board="JIRAPLAY",
|
78
|
+
url=jira_credentials.url,
|
79
|
+
issue_type="Task",
|
80
|
+
severity_priority_mappings=[severity_priority_mapping],
|
81
|
+
custom_fields={"security": {"id": "0"}},
|
82
|
+
)
|
83
|
+
|
84
|
+
|
85
|
+
@pytest.fixture
|
86
|
+
def jira_notifier_api_payload(
|
87
|
+
severity_priority_mapping: SeverityPriorityMapping,
|
88
|
+
jira_credentials: JiraCredentials,
|
89
|
+
) -> dict[str, Any]:
|
90
|
+
return {
|
91
|
+
"name": "jira-notifier-1",
|
92
|
+
"type": "jira",
|
93
|
+
"uiEndpoint": "https://acs.example.com",
|
94
|
+
"labelDefault": "JIRAPLAY",
|
95
|
+
"jira": {
|
96
|
+
"url": jira_credentials.url,
|
97
|
+
"username": jira_credentials.username,
|
98
|
+
"password": jira_credentials.token,
|
99
|
+
"issueType": "Task",
|
100
|
+
"priorityMappings": [severity_priority_mapping.to_api()],
|
101
|
+
"defaultFieldsJson": '{"security": {"id": "0"}}',
|
102
|
+
},
|
103
|
+
}
|
104
|
+
|
105
|
+
|
106
|
+
def test_jira_notifier_to_api(
|
107
|
+
jira_notifier: JiraNotifier,
|
108
|
+
jira_credentials: JiraCredentials,
|
109
|
+
jira_notifier_api_payload: dict[str, Any],
|
110
|
+
) -> None:
|
111
|
+
assert (
|
112
|
+
jira_notifier.to_api(
|
113
|
+
ui_endpoint="https://acs.example.com", jira_credentials=jira_credentials
|
114
|
+
)
|
115
|
+
== jira_notifier_api_payload
|
116
|
+
)
|
117
|
+
|
118
|
+
|
119
|
+
@pytest.fixture
|
120
|
+
def acs_notifier_api() -> AcsNotifiersApi:
|
121
|
+
return AcsNotifiersApi(
|
122
|
+
url="https://acs.example.com",
|
123
|
+
token="topsecret",
|
124
|
+
)
|
125
|
+
|
126
|
+
|
127
|
+
@pytest.fixture
|
128
|
+
def acs_notifier_api_notifiers_api_payload(
|
129
|
+
jira_notifier: JiraNotifier,
|
130
|
+
jira_credentials: JiraCredentials,
|
131
|
+
) -> list[Any]:
|
132
|
+
return [
|
133
|
+
jira_notifier.to_api(
|
134
|
+
ui_endpoint="https://acs.example.com", jira_credentials=jira_credentials
|
135
|
+
)
|
136
|
+
]
|
137
|
+
|
138
|
+
|
139
|
+
def test_acs_notifier_api_get_notifiers(
|
140
|
+
mocker: MockerFixture,
|
141
|
+
acs_notifier_api_notifiers_api_payload: list[Any],
|
142
|
+
acs_notifier_api: AcsNotifiersApi,
|
143
|
+
) -> None:
|
144
|
+
generic_request_mock = mocker.patch(
|
145
|
+
"reconcile.utils.acs.notifiers.AcsNotifiersApi.generic_request_json"
|
146
|
+
)
|
147
|
+
generic_request_mock.return_value = {
|
148
|
+
"notifiers": acs_notifier_api_notifiers_api_payload
|
149
|
+
}
|
150
|
+
|
151
|
+
assert acs_notifier_api.get_notifiers() == acs_notifier_api_notifiers_api_payload
|
152
|
+
generic_request_mock.assert_called_once_with("/v1/notifiers", "GET")
|
153
|
+
|
154
|
+
|
155
|
+
def test_acs_notifier_api_get_jira_notifiers(
|
156
|
+
mocker: MockerFixture,
|
157
|
+
acs_notifier_api_notifiers_api_payload: list[Any],
|
158
|
+
acs_notifier_api: AcsNotifiersApi,
|
159
|
+
jira_notifier: JiraNotifier,
|
160
|
+
) -> None:
|
161
|
+
get_notifiers_mock = mocker.patch(
|
162
|
+
"reconcile.utils.acs.notifiers.AcsNotifiersApi.get_notifiers"
|
163
|
+
)
|
164
|
+
get_notifiers_mock.return_value = acs_notifier_api_notifiers_api_payload
|
165
|
+
|
166
|
+
assert acs_notifier_api.get_jira_notifiers() == [jira_notifier]
|
167
|
+
|
168
|
+
|
169
|
+
def test_get_notifier_id_by_name(
|
170
|
+
mocker: MockerFixture,
|
171
|
+
acs_notifier_api_notifiers_api_payload: list[Any],
|
172
|
+
acs_notifier_api: AcsNotifiersApi,
|
173
|
+
) -> None:
|
174
|
+
get_notifiers_mock = mocker.patch(
|
175
|
+
"reconcile.utils.acs.notifiers.AcsNotifiersApi.get_notifiers"
|
176
|
+
)
|
177
|
+
get_notifiers_mock.return_value = acs_notifier_api_notifiers_api_payload
|
178
|
+
acs_notifier_api_notifiers_api_payload[0]["id"] = "jira-notifier-id-1"
|
179
|
+
|
180
|
+
assert (
|
181
|
+
acs_notifier_api.get_notifier_id_by_name("jira-notifier-1")
|
182
|
+
== "jira-notifier-id-1"
|
183
|
+
)
|
184
|
+
get_notifiers_mock.assert_called_once()
|
185
|
+
|
186
|
+
|
187
|
+
def test_update_jira_notifier(
|
188
|
+
mocker: MockerFixture,
|
189
|
+
acs_notifier_api: AcsNotifiersApi,
|
190
|
+
jira_notifier: JiraNotifier,
|
191
|
+
jira_credentials: JiraCredentials,
|
192
|
+
) -> None:
|
193
|
+
get_notifier_id_by_name_mock = mocker.patch(
|
194
|
+
"reconcile.utils.acs.notifiers.AcsNotifiersApi.get_notifier_id_by_name"
|
195
|
+
)
|
196
|
+
get_notifier_id_by_name_mock.return_value = "jira-notifier-id-1"
|
197
|
+
generic_request_mock = mocker.patch(
|
198
|
+
"reconcile.utils.acs.notifiers.AcsNotifiersApi.generic_request"
|
199
|
+
)
|
200
|
+
|
201
|
+
acs_notifier_api.update_jira_notifier(jira_notifier, jira_credentials)
|
202
|
+
get_notifier_id_by_name_mock.assert_called_once_with(jira_notifier.name)
|
203
|
+
body = jira_notifier.to_api(acs_notifier_api.url, jira_credentials)
|
204
|
+
generic_request_mock.assert_called_once_with(
|
205
|
+
"/v1/notifiers/jira-notifier-id-1", "PUT", body
|
206
|
+
)
|
207
|
+
|
208
|
+
|
209
|
+
def test_create_jira_notifier(
|
210
|
+
mocker: MockerFixture,
|
211
|
+
acs_notifier_api: AcsNotifiersApi,
|
212
|
+
jira_notifier: JiraNotifier,
|
213
|
+
jira_credentials: JiraCredentials,
|
214
|
+
) -> None:
|
215
|
+
generic_request_mock = mocker.patch(
|
216
|
+
"reconcile.utils.acs.notifiers.AcsNotifiersApi.generic_request"
|
217
|
+
)
|
218
|
+
acs_notifier_api.create_jira_notifier(jira_notifier, jira_credentials)
|
219
|
+
body = jira_notifier.to_api(acs_notifier_api.url, jira_credentials)
|
220
|
+
generic_request_mock.assert_called_once_with("/v1/notifiers", "POST", body)
|
221
|
+
|
222
|
+
|
223
|
+
def test_delete_jira_notifier(
|
224
|
+
mocker: MockerFixture,
|
225
|
+
acs_notifier_api: AcsNotifiersApi,
|
226
|
+
jira_notifier: JiraNotifier,
|
227
|
+
) -> None:
|
228
|
+
get_notifier_id_by_name_mock = mocker.patch(
|
229
|
+
"reconcile.utils.acs.notifiers.AcsNotifiersApi.get_notifier_id_by_name"
|
230
|
+
)
|
231
|
+
get_notifier_id_by_name_mock.return_value = "jira-notifier-id-1"
|
232
|
+
generic_request_mock = mocker.patch(
|
233
|
+
"reconcile.utils.acs.notifiers.AcsNotifiersApi.generic_request"
|
234
|
+
)
|
235
|
+
acs_notifier_api.delete_jira_notifier(jira_notifier)
|
236
|
+
get_notifier_id_by_name_mock.assert_called_once_with(jira_notifier.name)
|
237
|
+
generic_request_mock.assert_called_once_with(
|
238
|
+
"/v1/notifiers/jira-notifier-id-1", "DELETE"
|
239
|
+
)
|
240
|
+
|
241
|
+
|
242
|
+
@pytest.fixture
|
243
|
+
def acs_notifier_integration() -> AcsNotifiersIntegration:
|
244
|
+
return AcsNotifiersIntegration()
|
245
|
+
|
246
|
+
|
247
|
+
@pytest.fixture
|
248
|
+
def escalation_policy() -> AppEscalationPolicyV1:
|
249
|
+
return AppEscalationPolicyV1(
|
250
|
+
name="notifier-1",
|
251
|
+
channels=AppEscalationPolicyChannelsV1(
|
252
|
+
jiraBoard=[
|
253
|
+
JiraBoardV1(
|
254
|
+
name="JIRAPLAY",
|
255
|
+
server=JiraServerV1(
|
256
|
+
serverUrl="https://jira.example.com",
|
257
|
+
),
|
258
|
+
severityPriorityMappings=JiraSeverityPriorityMappingsV1(
|
259
|
+
name="sp",
|
260
|
+
mappings=[
|
261
|
+
SeverityPriorityMappingV1(
|
262
|
+
severity="critical", priority="Critical"
|
263
|
+
)
|
264
|
+
],
|
265
|
+
),
|
266
|
+
issueType="Task",
|
267
|
+
issueSecurityId="0",
|
268
|
+
disable=DisableJiraBoardAutomationsV1(integrations=[]),
|
269
|
+
)
|
270
|
+
],
|
271
|
+
jiraComponent="",
|
272
|
+
jiraLabels=[],
|
273
|
+
),
|
274
|
+
)
|
275
|
+
|
276
|
+
|
277
|
+
@pytest.fixture
|
278
|
+
def acs_policies(
|
279
|
+
escalation_policy: AppEscalationPolicyV1,
|
280
|
+
) -> list[AcsPolicyV1]:
|
281
|
+
return [
|
282
|
+
AcsPolicyV1(
|
283
|
+
name="acs-policy-1",
|
284
|
+
description="CVEs within app-sre clusters with CVSS score gte to 7 and fixable",
|
285
|
+
severity="high",
|
286
|
+
notifiers=[],
|
287
|
+
integrations=AcsPolicyIntegrationsV1(
|
288
|
+
notifiers=AcsPolicyIntegrationNotifiersV1(
|
289
|
+
jira=AcsPolicyIntegrationNotifierJiraV1(
|
290
|
+
escalationPolicy=escalation_policy,
|
291
|
+
),
|
292
|
+
),
|
293
|
+
),
|
294
|
+
categories=["vulnerability-management"],
|
295
|
+
scope=AcsPolicyScopeClusterV1(
|
296
|
+
level="cluster",
|
297
|
+
clusters=[],
|
298
|
+
),
|
299
|
+
conditions=[],
|
300
|
+
),
|
301
|
+
AcsPolicyV1(
|
302
|
+
name="acs-policy-2",
|
303
|
+
description="image security policy violations of critical severity within app-sre namespaces",
|
304
|
+
severity="critical",
|
305
|
+
notifiers=[],
|
306
|
+
integrations=AcsPolicyIntegrationsV1(
|
307
|
+
notifiers=AcsPolicyIntegrationNotifiersV1(
|
308
|
+
jira=AcsPolicyIntegrationNotifierJiraV1(
|
309
|
+
escalationPolicy=escalation_policy,
|
310
|
+
),
|
311
|
+
),
|
312
|
+
),
|
313
|
+
categories=["vulnerability-management", "devops-best-practices"],
|
314
|
+
scope=AcsPolicyScopeNamespaceV1(
|
315
|
+
level="namespace",
|
316
|
+
namespaces=[],
|
317
|
+
),
|
318
|
+
conditions=[],
|
319
|
+
),
|
320
|
+
]
|
321
|
+
|
322
|
+
|
323
|
+
def test_integration_get_escalation_policies(
|
324
|
+
acs_notifier_integration: AcsNotifiersIntegration,
|
325
|
+
acs_policies: list[AcsPolicyV1],
|
326
|
+
escalation_policy: AppEscalationPolicyV1,
|
327
|
+
) -> None:
|
328
|
+
result = acs_notifier_integration._get_escalation_policies(acs_policies)
|
329
|
+
|
330
|
+
assert len(acs_policies) > 1
|
331
|
+
assert len(result) == 1
|
332
|
+
assert result[0] == escalation_policy
|
333
|
+
|
334
|
+
|
335
|
+
def test_build_jira_notifier(
|
336
|
+
acs_notifier_integration: AcsNotifiersIntegration,
|
337
|
+
escalation_policy: AppEscalationPolicyV1,
|
338
|
+
jira_notifier: JiraNotifier,
|
339
|
+
) -> None:
|
340
|
+
assert (
|
341
|
+
acs_notifier_integration._build_jira_notifier(escalation_policy)
|
342
|
+
== jira_notifier
|
343
|
+
)
|
344
|
+
|
345
|
+
|
346
|
+
def test_get_desired_state(
|
347
|
+
acs_notifier_integration: AcsNotifiersIntegration,
|
348
|
+
acs_policies: list[AcsPolicyV1],
|
349
|
+
jira_notifier: JiraNotifier,
|
350
|
+
) -> None:
|
351
|
+
assert acs_notifier_integration.get_desired_state(acs_policies) == [jira_notifier]
|
352
|
+
|
353
|
+
|
354
|
+
def test_reconcile(
|
355
|
+
mocker: MockerFixture,
|
356
|
+
acs_notifier_integration: AcsNotifiersIntegration,
|
357
|
+
acs_notifier_api: AcsNotifiersApi,
|
358
|
+
jira_credentials: JiraCredentials,
|
359
|
+
jira_notifier: JiraNotifier,
|
360
|
+
) -> None:
|
361
|
+
notifier_to_add = copy.deepcopy(jira_notifier)
|
362
|
+
notifier_to_add.name = "jira-notifier-to-add"
|
363
|
+
notifier_to_update_current = copy.deepcopy(jira_notifier)
|
364
|
+
notifier_to_update_current.name = "jira-notifier-to-update"
|
365
|
+
notifier_to_update_current.issue_type = "Task"
|
366
|
+
notifier_to_update_desired = copy.deepcopy(jira_notifier)
|
367
|
+
notifier_to_update_desired.name = "jira-notifier-to-update"
|
368
|
+
notifier_to_update_current.issue_type = "Bug"
|
369
|
+
notifier_to_delete = copy.deepcopy(jira_notifier)
|
370
|
+
notifier_to_delete.name = "jira-notifier-to-delete"
|
371
|
+
|
372
|
+
current_state = [notifier_to_delete, notifier_to_update_current]
|
373
|
+
desired_state = [notifier_to_add, notifier_to_update_desired]
|
374
|
+
|
375
|
+
create_jira_notifier_mock = mocker.patch(
|
376
|
+
"reconcile.utils.acs.notifiers.AcsNotifiersApi.create_jira_notifier"
|
377
|
+
)
|
378
|
+
update_jira_notifier_mock = mocker.patch(
|
379
|
+
"reconcile.utils.acs.notifiers.AcsNotifiersApi.update_jira_notifier"
|
380
|
+
)
|
381
|
+
|
382
|
+
delete_jira_notifier_mock = mocker.patch(
|
383
|
+
"reconcile.utils.acs.notifiers.AcsNotifiersApi.delete_jira_notifier"
|
384
|
+
)
|
385
|
+
acs_notifier_integration.reconcile(
|
386
|
+
current_state,
|
387
|
+
desired_state,
|
388
|
+
acs_notifier_api,
|
389
|
+
{jira_credentials.url: jira_credentials},
|
390
|
+
dry_run=False,
|
391
|
+
)
|
392
|
+
|
393
|
+
create_jira_notifier_mock.assert_called_once_with(
|
394
|
+
notifier_to_add, jira_credentials=jira_credentials
|
395
|
+
)
|
396
|
+
update_jira_notifier_mock.assert_called_once_with(
|
397
|
+
notifier_to_update_desired, jira_credentials=jira_credentials
|
398
|
+
)
|
399
|
+
delete_jira_notifier_mock.assert_called_once_with(notifier_to_delete)
|
@@ -10,11 +10,20 @@ from reconcile.gql_definitions.acs.acs_policies import (
|
|
10
10
|
AcsPolicyConditionsCveV1,
|
11
11
|
AcsPolicyConditionsCvssV1,
|
12
12
|
AcsPolicyConditionsSeverityV1,
|
13
|
+
AcsPolicyIntegrationNotifierJiraV1,
|
14
|
+
AcsPolicyIntegrationNotifiersV1,
|
15
|
+
AcsPolicyIntegrationsV1,
|
13
16
|
AcsPolicyQueryData,
|
14
17
|
AcsPolicyScopeClusterV1,
|
15
18
|
AcsPolicyScopeNamespaceV1,
|
16
19
|
AcsPolicyV1,
|
20
|
+
AppEscalationPolicyChannelsV1,
|
21
|
+
AppEscalationPolicyV1,
|
17
22
|
ClusterV1,
|
23
|
+
DisableJiraBoardAutomationsV1,
|
24
|
+
JiraBoardV1,
|
25
|
+
JiraServerV1,
|
26
|
+
JiraSeverityPriorityMappingsV1,
|
18
27
|
NamespaceV1,
|
19
28
|
NamespaceV1_ClusterV1,
|
20
29
|
)
|
@@ -41,6 +50,36 @@ def query_data_desired_state() -> AcsPolicyQueryData:
|
|
41
50
|
description="CVEs within app-sre clusters with CVSS score gte to 7 and fixable",
|
42
51
|
severity="high",
|
43
52
|
notifiers=[JIRA_NOTIFIER_NAME],
|
53
|
+
integrations=AcsPolicyIntegrationsV1(
|
54
|
+
notifiers=AcsPolicyIntegrationNotifiersV1(
|
55
|
+
jira=AcsPolicyIntegrationNotifierJiraV1(
|
56
|
+
escalationPolicy=AppEscalationPolicyV1(
|
57
|
+
name="test-ep",
|
58
|
+
channels=AppEscalationPolicyChannelsV1(
|
59
|
+
jiraBoard=[
|
60
|
+
JiraBoardV1(
|
61
|
+
name="board",
|
62
|
+
server=JiraServerV1(
|
63
|
+
serverUrl="server",
|
64
|
+
),
|
65
|
+
severityPriorityMappings=JiraSeverityPriorityMappingsV1(
|
66
|
+
name="sp",
|
67
|
+
mappings=[],
|
68
|
+
),
|
69
|
+
issueType="Task",
|
70
|
+
issueSecurityId="0",
|
71
|
+
disable=DisableJiraBoardAutomationsV1(
|
72
|
+
integrations=[]
|
73
|
+
),
|
74
|
+
)
|
75
|
+
],
|
76
|
+
jiraComponent="",
|
77
|
+
jiraLabels=[],
|
78
|
+
),
|
79
|
+
),
|
80
|
+
),
|
81
|
+
),
|
82
|
+
),
|
44
83
|
categories=["vulnerability-management"],
|
45
84
|
scope=AcsPolicyScopeClusterV1(
|
46
85
|
level="cluster",
|
@@ -61,6 +100,36 @@ def query_data_desired_state() -> AcsPolicyQueryData:
|
|
61
100
|
description="image security policy violations of critical severity within app-sre namespaces",
|
62
101
|
severity="critical",
|
63
102
|
notifiers=[],
|
103
|
+
integrations=AcsPolicyIntegrationsV1(
|
104
|
+
notifiers=AcsPolicyIntegrationNotifiersV1(
|
105
|
+
jira=AcsPolicyIntegrationNotifierJiraV1(
|
106
|
+
escalationPolicy=AppEscalationPolicyV1(
|
107
|
+
name="test-ep",
|
108
|
+
channels=AppEscalationPolicyChannelsV1(
|
109
|
+
jiraBoard=[
|
110
|
+
JiraBoardV1(
|
111
|
+
name="board",
|
112
|
+
server=JiraServerV1(
|
113
|
+
serverUrl="server",
|
114
|
+
),
|
115
|
+
severityPriorityMappings=JiraSeverityPriorityMappingsV1(
|
116
|
+
name="sp",
|
117
|
+
mappings=[],
|
118
|
+
),
|
119
|
+
issueType="Task",
|
120
|
+
issueSecurityId="0",
|
121
|
+
disable=DisableJiraBoardAutomationsV1(
|
122
|
+
integrations=[]
|
123
|
+
),
|
124
|
+
)
|
125
|
+
],
|
126
|
+
jiraComponent="",
|
127
|
+
jiraLabels=[],
|
128
|
+
),
|
129
|
+
),
|
130
|
+
),
|
131
|
+
),
|
132
|
+
),
|
64
133
|
categories=["vulnerability-management", "devops-best-practices"],
|
65
134
|
scope=AcsPolicyScopeNamespaceV1(
|
66
135
|
level="namespace",
|
reconcile/utils/acs/base.py
CHANGED
@@ -0,0 +1,111 @@
|
|
1
|
+
import json
|
2
|
+
from typing import Any, Optional
|
3
|
+
|
4
|
+
from pydantic import BaseModel
|
5
|
+
|
6
|
+
from reconcile.utils.acs.base import AcsBaseApi
|
7
|
+
|
8
|
+
|
9
|
+
class JiraCredentials(BaseModel):
|
10
|
+
url: str
|
11
|
+
username: str
|
12
|
+
token: str
|
13
|
+
|
14
|
+
|
15
|
+
class SeverityPriorityMapping(BaseModel):
|
16
|
+
severity: str
|
17
|
+
priority: str
|
18
|
+
|
19
|
+
@staticmethod
|
20
|
+
def from_api(mapping: dict[str, str]) -> "SeverityPriorityMapping":
|
21
|
+
return SeverityPriorityMapping(
|
22
|
+
severity=mapping["severity"].replace("_SEVERITY", "").lower(),
|
23
|
+
priority=mapping["priorityName"],
|
24
|
+
)
|
25
|
+
|
26
|
+
def to_api(self) -> Any:
|
27
|
+
return {
|
28
|
+
"severity": f"{self.severity.upper()}_SEVERITY",
|
29
|
+
"priorityName": self.priority,
|
30
|
+
}
|
31
|
+
|
32
|
+
|
33
|
+
class JiraNotifier(BaseModel):
|
34
|
+
name: str
|
35
|
+
board: str
|
36
|
+
url: str
|
37
|
+
issue_type: Optional[str]
|
38
|
+
severity_priority_mappings: list[SeverityPriorityMapping]
|
39
|
+
custom_fields: Optional[dict[str, Any]]
|
40
|
+
|
41
|
+
@staticmethod
|
42
|
+
def from_api(notifier: dict[str, Any]) -> "JiraNotifier":
|
43
|
+
notifier_jira = notifier["jira"]
|
44
|
+
return JiraNotifier(
|
45
|
+
name=notifier["name"],
|
46
|
+
board=notifier["labelDefault"],
|
47
|
+
url=notifier_jira["url"],
|
48
|
+
issue_type=notifier_jira["issueType"],
|
49
|
+
severity_priority_mappings=sorted(
|
50
|
+
[
|
51
|
+
SeverityPriorityMapping.from_api(mapping)
|
52
|
+
for mapping in notifier_jira["priorityMappings"]
|
53
|
+
],
|
54
|
+
key=lambda m: m.severity,
|
55
|
+
),
|
56
|
+
custom_fields=json.loads(notifier_jira.get("defaultFieldsJson") or "{}"),
|
57
|
+
)
|
58
|
+
|
59
|
+
def to_api(self, ui_endpoint: str, jira_credentials: JiraCredentials) -> Any:
|
60
|
+
return {
|
61
|
+
"name": self.name,
|
62
|
+
"type": "jira",
|
63
|
+
"uiEndpoint": ui_endpoint,
|
64
|
+
"labelDefault": self.board,
|
65
|
+
"jira": {
|
66
|
+
"url": jira_credentials.url,
|
67
|
+
"username": jira_credentials.username,
|
68
|
+
"password": jira_credentials.token,
|
69
|
+
"issueType": self.issue_type or "Task",
|
70
|
+
"priorityMappings": [
|
71
|
+
mapping.to_api() for mapping in self.severity_priority_mappings
|
72
|
+
],
|
73
|
+
"defaultFieldsJson": json.dumps(self.custom_fields or {}),
|
74
|
+
},
|
75
|
+
}
|
76
|
+
|
77
|
+
|
78
|
+
class AcsNotifiersApi(AcsBaseApi):
|
79
|
+
"""
|
80
|
+
Implements methods to support reconcile operations against the ACS NotifiersService api
|
81
|
+
"""
|
82
|
+
|
83
|
+
def get_notifiers(self) -> list[Any]:
|
84
|
+
return self.generic_request_json("/v1/notifiers", "GET")["notifiers"]
|
85
|
+
|
86
|
+
def get_jira_notifiers(self) -> list[JiraNotifier]:
|
87
|
+
return [
|
88
|
+
JiraNotifier.from_api(notifier)
|
89
|
+
for notifier in self.get_notifiers()
|
90
|
+
if notifier["type"] == "jira"
|
91
|
+
]
|
92
|
+
|
93
|
+
def get_notifier_id_by_name(self, name: str) -> str:
|
94
|
+
return [n["id"] for n in self.get_notifiers() if n["name"] == name][0]
|
95
|
+
|
96
|
+
def update_jira_notifier(
|
97
|
+
self, jira_notifier: JiraNotifier, jira_credentials: JiraCredentials
|
98
|
+
) -> None:
|
99
|
+
notifier_id = self.get_notifier_id_by_name(jira_notifier.name)
|
100
|
+
body = jira_notifier.to_api(self.url, jira_credentials)
|
101
|
+
self.generic_request(f"/v1/notifiers/{notifier_id}", "PUT", body)
|
102
|
+
|
103
|
+
def create_jira_notifier(
|
104
|
+
self, jira_notifier: JiraNotifier, jira_credentials: JiraCredentials
|
105
|
+
) -> None:
|
106
|
+
body = jira_notifier.to_api(self.url, jira_credentials)
|
107
|
+
self.generic_request("/v1/notifiers", "POST", body)
|
108
|
+
|
109
|
+
def delete_jira_notifier(self, jira_notifier: JiraNotifier) -> None:
|
110
|
+
notifier_id = self.get_notifier_id_by_name(jira_notifier.name)
|
111
|
+
self.generic_request(f"/v1/notifiers/{notifier_id}", "DELETE")
|
File without changes
|
File without changes
|
{qontract_reconcile-0.10.1rc734.dist-info → qontract_reconcile-0.10.1rc736.dist-info}/top_level.txt
RENAMED
File without changes
|