qontract-reconcile 0.10.1rc733__py3-none-any.whl → 0.10.1rc735__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.1rc733.dist-info → qontract_reconcile-0.10.1rc735.dist-info}/METADATA +1 -1
- {qontract_reconcile-0.10.1rc733.dist-info → qontract_reconcile-0.10.1rc735.dist-info}/RECORD +15 -10
- 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/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
- reconcile/utils/jira_client.py +10 -3
- {qontract_reconcile-0.10.1rc733.dist-info → qontract_reconcile-0.10.1rc735.dist-info}/WHEEL +0 -0
- {qontract_reconcile-0.10.1rc733.dist-info → qontract_reconcile-0.10.1rc735.dist-info}/entry_points.txt +0 -0
- {qontract_reconcile-0.10.1rc733.dist-info → qontract_reconcile-0.10.1rc735.dist-info}/top_level.txt +0 -0
{qontract_reconcile-0.10.1rc733.dist-info → qontract_reconcile-0.10.1rc735.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.1rc735
|
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.1rc733.dist-info → qontract_reconcile-0.10.1rc735.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
|
@@ -425,7 +428,8 @@ reconcile/templating/lib/rendering.py,sha256=_BVQ2gqip8K1AgLYfaTWh8NKJFTW6VjUZ6r
|
|
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
|
@@ -607,7 +611,7 @@ reconcile/utils/helpers.py,sha256=k9svgFFZG7H5FvHYY0g5jJyvgvh2UDZxf0Ib221teag,11
|
|
607
611
|
reconcile/utils/imap_client.py,sha256=byFAJATbITJPsGECSbvXBOcCnoeTUpDFiEjzOAxLm_U,1975
|
608
612
|
reconcile/utils/instrumented_wrappers.py,sha256=eVwMoa6FCrYxLv3RML3WpZF9qKVfCTjMxphgVXG03OM,1073
|
609
613
|
reconcile/utils/jenkins_api.py,sha256=MyJSB_S3uYf3sXnt9t03-gZNQ7tbdd7Wusv3MoF2fRc,7113
|
610
|
-
reconcile/utils/jira_client.py,sha256=
|
614
|
+
reconcile/utils/jira_client.py,sha256=KjsakExjlii6lkxUlHRScBtPG4wuomJOTIoNiM-edQw,7322
|
611
615
|
reconcile/utils/jjb_client.py,sha256=Pdy0dLCFvD6GPCaC0tZydYgkVJPOxYXIiwWECZaFJBU,14551
|
612
616
|
reconcile/utils/jsonpath.py,sha256=NRpAEijKN4cMDjo7qivNPqpm0__GQQ1TiE0PBEBO45s,5572
|
613
617
|
reconcile/utils/jump_host.py,sha256=AdwmCZYNhRe53VwV2iAsUdVyUdVtSd4REmdThJDkM5w,4973
|
@@ -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.1rc735.dist-info/METADATA,sha256=AQww9xL2v9VdTWvywruYZoPnQRbH2TO1wlIpHlpfGqE,2382
|
774
|
+
qontract_reconcile-0.10.1rc735.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
|
775
|
+
qontract_reconcile-0.10.1rc735.dist-info/entry_points.txt,sha256=rIxI5zWtHNlfpDeq1a7pZXAPoqf7HG32KMTN3MeWK_8,429
|
776
|
+
qontract_reconcile-0.10.1rc735.dist-info/top_level.txt,sha256=l5ISPoXzt0SdR4jVdkfa7RPSKNc8zAHYWAnR-Dw8Ey8,24
|
777
|
+
qontract_reconcile-0.10.1rc735.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)
|
@@ -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")
|
reconcile/utils/jira_client.py
CHANGED
@@ -96,11 +96,12 @@ class JiraClient:
|
|
96
96
|
if settings and settings["jiraWatcher"]:
|
97
97
|
read_timeout = settings["jiraWatcher"]["readTimeout"]
|
98
98
|
connect_timeout = settings["jiraWatcher"]["connectTimeout"]
|
99
|
-
self.timeout = (read_timeout, connect_timeout)
|
100
99
|
if not self.server:
|
101
100
|
raise RuntimeError("JiraClient.server is not set.")
|
102
101
|
|
103
|
-
self.jira = JIRA(
|
102
|
+
self.jira = JIRA(
|
103
|
+
self.server, token_auth=token_auth, timeout=(read_timeout, connect_timeout)
|
104
|
+
)
|
104
105
|
|
105
106
|
@staticmethod
|
106
107
|
def create(
|
@@ -215,7 +216,13 @@ class JiraClient:
|
|
215
216
|
raise RuntimeError("JiraClient.server is not set.")
|
216
217
|
|
217
218
|
# use anonymous access to get public projects
|
218
|
-
jira_api_anon = JIRA(
|
219
|
+
jira_api_anon = JIRA(
|
220
|
+
server=self.server,
|
221
|
+
timeout=(
|
222
|
+
JiraClient.DEFAULT_READ_TIMEOUT,
|
223
|
+
JiraClient.DEFAULT_CONNECT_TIMEOUT,
|
224
|
+
),
|
225
|
+
)
|
219
226
|
return [project.key for project in jira_api_anon.projects()]
|
220
227
|
|
221
228
|
def components(self) -> list[str]:
|
File without changes
|
File without changes
|
{qontract_reconcile-0.10.1rc733.dist-info → qontract_reconcile-0.10.1rc735.dist-info}/top_level.txt
RENAMED
File without changes
|