qontract-reconcile 0.10.1rc734__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: qontract-reconcile
3
- Version: 0.10.1rc734
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
@@ -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=lAZ68_w6IcvXfB6C8D97DtdRlpodqjrCGLLYGvRUkSs,96887
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=Z6Z7duvS9W4cbciBED4oK40Vg9QyYti3zXvoEXM-fak,4422
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/test_acs_policies.py,sha256=pffUzH4IHKuXntvGMi-iV0Epg4YsCBF2G2-R9nYIt40,15699
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=8qZhCYteLCe3xSIeiNj-HXm6J0nBLFGBbZ7b7xXlA6I,2381
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.1rc734.dist-info/METADATA,sha256=4yl8FRQHqk6s4yWQgBGblLVlgOJiOKeSuEo-S5mcl8I,2382
769
- qontract_reconcile-0.10.1rc734.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
770
- qontract_reconcile-0.10.1rc734.dist-info/entry_points.txt,sha256=rIxI5zWtHNlfpDeq1a7pZXAPoqf7HG32KMTN3MeWK_8,429
771
- qontract_reconcile-0.10.1rc734.dist-info/top_level.txt,sha256=l5ISPoXzt0SdR4jVdkfa7RPSKNc8zAHYWAnR-Dw8Ey8,24
772
- qontract_reconcile-0.10.1rc734.dist-info/RECORD,,
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",
@@ -75,3 +75,8 @@ class AcsBaseApi(BaseModel):
75
75
  raise
76
76
 
77
77
  return response
78
+
79
+ def generic_request_json(
80
+ self, path: str, verb: str, json: Optional[Any] = None
81
+ ) -> Any:
82
+ return self.generic_request(path, verb, json=json).json()
@@ -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")