qontract-reconcile 0.10.1rc466__py3-none-any.whl → 0.10.1rc467__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.1rc466.dist-info → qontract_reconcile-0.10.1rc467.dist-info}/METADATA +1 -1
- {qontract_reconcile-0.10.1rc466.dist-info → qontract_reconcile-0.10.1rc467.dist-info}/RECORD +12 -9
- reconcile/cli.py +9 -2
- reconcile/gql_definitions/common/jiralert_settings.py +68 -0
- reconcile/gql_definitions/jira_permissions_validator/jira_boards_for_permissions_validator.py +32 -0
- reconcile/jira_permissions_validator.py +229 -17
- reconcile/test/test_jira_permissions_validator.py +386 -0
- reconcile/typed_queries/jiralert_settings.py +22 -0
- reconcile/utils/jira_client.py +49 -0
- {qontract_reconcile-0.10.1rc466.dist-info → qontract_reconcile-0.10.1rc467.dist-info}/WHEEL +0 -0
- {qontract_reconcile-0.10.1rc466.dist-info → qontract_reconcile-0.10.1rc467.dist-info}/entry_points.txt +0 -0
- {qontract_reconcile-0.10.1rc466.dist-info → qontract_reconcile-0.10.1rc467.dist-info}/top_level.txt +0 -0
{qontract_reconcile-0.10.1rc466.dist-info → qontract_reconcile-0.10.1rc467.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.1rc467
|
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.1rc466.dist-info → qontract_reconcile-0.10.1rc467.dist-info}/RECORD
RENAMED
@@ -8,7 +8,7 @@ reconcile/aws_iam_password_reset.py,sha256=NwErtrqgBiXr7eGCAHdtGGOx0S7-4JnSc29Ie
|
|
8
8
|
reconcile/aws_support_cases_sos.py,sha256=Jk6_XjDeJSYxgRGqcEAOcynt9qJF2r5HPIPcSKmoBv8,2974
|
9
9
|
reconcile/blackbox_exporter_endpoint_monitoring.py,sha256=W_VJagnsJR1v5oqjlI3RJJE0_nhtJ0m81RS8zWA5u5c,3538
|
10
10
|
reconcile/checkpoint.py,sha256=R2WFXUXLTB4sWMi4GeA4eegsuf_1-Q4vH8M0Toh3Ij4,5036
|
11
|
-
reconcile/cli.py,sha256=
|
11
|
+
reconcile/cli.py,sha256=2FIP7dyW9lxJwbJFyZou5noGIl0B0gEj9IKLNM0vk-I,81792
|
12
12
|
reconcile/closedbox_endpoint_monitoring_base.py,sha256=SMhkcQqprWvThrIJa3U_3uh5w1h-alleW1QnCJFY4Qw,4909
|
13
13
|
reconcile/cluster_deployment_mapper.py,sha256=2Ah-nu-Mdig0pjuiZl_XLrmVAjYzFjORR3dMlCgkmw0,2352
|
14
14
|
reconcile/dashdotdb_base.py,sha256=a5aPLVxyqPSbjdB0Ty-uliOtxwvEbbEljHJKxdK3-Zk,4813
|
@@ -44,7 +44,7 @@ reconcile/jenkins_roles.py,sha256=f8ELpZY36UjoaCpR_9LijQuIMuB6a7sVLFf_H1ct9Hc,44
|
|
44
44
|
reconcile/jenkins_webhooks.py,sha256=j8vhJMWcRhOdc9XzRSm0CPj84jsF3e4Syjm7r1BIsDE,1978
|
45
45
|
reconcile/jenkins_webhooks_cleaner.py,sha256=JsN_NVPfZJwv1JtSzZXDIHUqGiefL-DRffFnDGau9aY,1539
|
46
46
|
reconcile/jenkins_worker_fleets.py,sha256=PMNGOX0krubFjInPiFT0za0KCiWBLEcVDuXdKRd1BrE,5378
|
47
|
-
reconcile/jira_permissions_validator.py,sha256=
|
47
|
+
reconcile/jira_permissions_validator.py,sha256=mXMB5t958gwP1yFQJU8Aml8r0QjdbJ6Dx-6-U12qOiA,9890
|
48
48
|
reconcile/jira_watcher.py,sha256=eyOQ92t8TFi6gogfNTO448h_h1CUyr24E0MPHc51R-o,3617
|
49
49
|
reconcile/ldap_users.py,sha256=uEWQ0V41tN9KCZi4ZKPamjrJ6djSpdpvDBo7yJ0e7ZI,3008
|
50
50
|
reconcile/mr_client_gateway.py,sha256=WhjMd-sIXDFCV8-rt8CEjurJ5OYB1pOD0K3o0tZRXQg,1885
|
@@ -196,6 +196,7 @@ reconcile/gql_definitions/common/clusters_minimal.py,sha256=yZpjS9qWyusCEiWtD8wz
|
|
196
196
|
reconcile/gql_definitions/common/clusters_with_peering.py,sha256=GJjV0coYt2IAyeV00rEFZWxz6YOW5txt0FzRdLT2T5w,11099
|
197
197
|
reconcile/gql_definitions/common/github_orgs.py,sha256=rZ0pDAA2_9hF9N-ykRZIxPtEmczTSjuA_k3nkp0k1W0,2039
|
198
198
|
reconcile/gql_definitions/common/jira_settings.py,sha256=Fmjxhlhr69kc4jkG_0k17fuYlQVucbNex0jXYu83wbY,1990
|
199
|
+
reconcile/gql_definitions/common/jiralert_settings.py,sha256=H96nMg_r2YcOvioj3aIkwqtFrALGSLt7uhbx9jGSUTo,1984
|
199
200
|
reconcile/gql_definitions/common/namespaces.py,sha256=AmE6XSxGVKYUHjmWI8y2scHw1ya9EfTnEkvffzyRKDE,8922
|
200
201
|
reconcile/gql_definitions/common/namespaces_minimal.py,sha256=XVt8LFe-bGYbjN3ysX3b9sFGmLX4snQ_A9ZouQGaaAI,3429
|
201
202
|
reconcile/gql_definitions/common/ocm_environments.py,sha256=mQyZR04tI_-paCo2FxrK0G-Zl8-izKntuo7Z9fIaY0M,1991
|
@@ -245,7 +246,7 @@ reconcile/gql_definitions/integrations/integrations.py,sha256=R-COVEcr8OWiOjuYTv
|
|
245
246
|
reconcile/gql_definitions/jenkins_configs/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
246
247
|
reconcile/gql_definitions/jenkins_configs/jenkins_configs.py,sha256=0nMkH0G-AjQwu53fqHykth6X6jjbHdW2hBp5n7N-r24,2766
|
247
248
|
reconcile/gql_definitions/jira_permissions_validator/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
248
|
-
reconcile/gql_definitions/jira_permissions_validator/jira_boards_for_permissions_validator.py,sha256=
|
249
|
+
reconcile/gql_definitions/jira_permissions_validator/jira_boards_for_permissions_validator.py,sha256=4_Uz4by1AOfLMicEkBHrUt6hyKYHnbQ9naTsrKbq0as,3365
|
249
250
|
reconcile/gql_definitions/jumphosts/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
250
251
|
reconcile/gql_definitions/jumphosts/jumphosts.py,sha256=gN595lx7K1XsB2AfxDQ911TBVBbCoxibVeujnsGue_Q,2371
|
251
252
|
reconcile/gql_definitions/ldap_groups/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
@@ -386,6 +387,7 @@ reconcile/test/test_gitlab_members.py,sha256=dP_dm-1THba9Vyzcq-EX1tdmBoX2hq8R-MY
|
|
386
387
|
reconcile/test/test_instrumented_wrappers.py,sha256=CZzhnQH0c4i7-Rxjg7-0dfFMvVPegLHL46z5NHOOCwo,608
|
387
388
|
reconcile/test/test_integrations_manager.py,sha256=l6KwSFT0NS9VSR-b_9z_ZEGXDWH3EMitUEMC_1h8Xkk,38184
|
388
389
|
reconcile/test/test_jenkins_worker_fleets.py,sha256=o1jlT7OBBSgu0M3iI4xMdz_x6SciF7yhNBpLk5gTJfg,2361
|
390
|
+
reconcile/test/test_jira_permissions_validator.py,sha256=FGLLsRV52PQNsYPaxNv7HWxERMouQ9RmUddh-IWJ3ns,12949
|
389
391
|
reconcile/test/test_jump_host.py,sha256=yczTqvT-hNAf9zBMuFjqka9fQOA31SCNG7D-9K9MRPw,3323
|
390
392
|
reconcile/test/test_ldap_users.py,sha256=8jjzVgoiRRylGad6-TvkugoFGXt3eko--zVVKjmZDn4,3812
|
391
393
|
reconcile/test/test_make.py,sha256=zTdjgq-3idFlec_0qJenk9wWw0QMLvSpJfPsptXmync,677
|
@@ -479,6 +481,7 @@ reconcile/typed_queries/clusters_with_peering.py,sha256=lIai7SJJD0bqIJbe7virgrbY
|
|
479
481
|
reconcile/typed_queries/github_orgs.py,sha256=UZhoPl8qvA_tcO7CZlN8GuMKckt3ywd47Suu61rgHsc,258
|
480
482
|
reconcile/typed_queries/gitlab_instances.py,sha256=ZVQHy2W9xIp53f5qYkjKLHLHgOVtQpxTfcmM1C2046g,291
|
481
483
|
reconcile/typed_queries/jira_settings.py,sha256=i0ddx5xxHrM1v-9mtL_6OB-jBFLw7-HS6xenpIDjrkw,570
|
484
|
+
reconcile/typed_queries/jiralert_settings.py,sha256=y59S5xvYmuaGxszzfKhVLjbCyDwKiaSIlajocbK5MDE,793
|
482
485
|
reconcile/typed_queries/namespaces.py,sha256=vItPrn7sfcHOix-VvkzQkf54_ljzI_ymyxh5esdBJ5Y,262
|
483
486
|
reconcile/typed_queries/namespaces_minimal.py,sha256=rUtqNQ0ORXXUTQfnpsMURymAJ4gYtE77V-Lb3LiJFEY,278
|
484
487
|
reconcile/typed_queries/pagerduty_instances.py,sha256=QCHqEAakiH6eSob0Pnnn3IBd8Ga0zpEp1Z6Qu3v2uH4,733
|
@@ -527,7 +530,7 @@ reconcile/utils/imap_client.py,sha256=byFAJATbITJPsGECSbvXBOcCnoeTUpDFiEjzOAxLm_
|
|
527
530
|
reconcile/utils/instrumented_wrappers.py,sha256=eVwMoa6FCrYxLv3RML3WpZF9qKVfCTjMxphgVXG03OM,1073
|
528
531
|
reconcile/utils/jenkins_api.py,sha256=MyJSB_S3uYf3sXnt9t03-gZNQ7tbdd7Wusv3MoF2fRc,7113
|
529
532
|
reconcile/utils/jinja2_ext.py,sha256=l628RR9r9dAGBWLVegoCbSqnjojeizNGiq9Cstt02nE,1129
|
530
|
-
reconcile/utils/jira_client.py,sha256=
|
533
|
+
reconcile/utils/jira_client.py,sha256=CxQYWc90YWFESpvFW61Gw5VQAfG9Qn2M4o1WrAeiqv4,6444
|
531
534
|
reconcile/utils/jjb_client.py,sha256=Pdy0dLCFvD6GPCaC0tZydYgkVJPOxYXIiwWECZaFJBU,14551
|
532
535
|
reconcile/utils/jsonpath.py,sha256=NRpAEijKN4cMDjo7qivNPqpm0__GQQ1TiE0PBEBO45s,5572
|
533
536
|
reconcile/utils/jump_host.py,sha256=AdwmCZYNhRe53VwV2iAsUdVyUdVtSd4REmdThJDkM5w,4973
|
@@ -646,8 +649,8 @@ tools/test/test_app_interface_metrics_exporter.py,sha256=SX7qL3D1SIRKFo95FoQztvf
|
|
646
649
|
tools/test/test_qontract_cli.py,sha256=awwTHEc2DWlykuqGIYM0WOBoSL0KRnOraCLk3C7izis,1401
|
647
650
|
tools/test/test_sd_app_sre_alert_report.py,sha256=v363r9zM7__0kR5K6mvJoGFcM9BvE33fWAayrqkpojA,2116
|
648
651
|
tools/test/test_sre_checkpoints.py,sha256=SKqPPTl9ua0RFdSSofnoQX-JZE6dFLO3LRhfQzqtfh8,2607
|
649
|
-
qontract_reconcile-0.10.
|
650
|
-
qontract_reconcile-0.10.
|
651
|
-
qontract_reconcile-0.10.
|
652
|
-
qontract_reconcile-0.10.
|
653
|
-
qontract_reconcile-0.10.
|
652
|
+
qontract_reconcile-0.10.1rc467.dist-info/METADATA,sha256=yhMB3IIg1C8P75RrPC2gBb4Pr3NTpREbOTWw91lKSZY,2348
|
653
|
+
qontract_reconcile-0.10.1rc467.dist-info/WHEEL,sha256=oiQVh_5PnQM0E3gPdiz09WCNmwiHDMaGer_elqB3coM,92
|
654
|
+
qontract_reconcile-0.10.1rc467.dist-info/entry_points.txt,sha256=rTjAv28I_CHLM8ID3OPqMI_suoQ9s7tFbim4aYjn9kk,376
|
655
|
+
qontract_reconcile-0.10.1rc467.dist-info/top_level.txt,sha256=l5ISPoXzt0SdR4jVdkfa7RPSKNc8zAHYWAnR-Dw8Ey8,24
|
656
|
+
qontract_reconcile-0.10.1rc467.dist-info/RECORD,,
|
reconcile/cli.py
CHANGED
@@ -884,11 +884,18 @@ def jenkins_webhooks_cleaner(ctx):
|
|
884
884
|
|
885
885
|
|
886
886
|
@integration.command(short_help="Validate permissions in Jira.")
|
887
|
+
@click.option(
|
888
|
+
"--exit-on-permission-errors/--no-exit-on-permission-errors",
|
889
|
+
help="Throw and error in case of board permission errors. Useful for PR checks.",
|
890
|
+
default=True,
|
891
|
+
)
|
887
892
|
@click.pass_context
|
888
|
-
def jira_permissions_validator(ctx):
|
893
|
+
def jira_permissions_validator(ctx, exit_on_permission_errors):
|
889
894
|
import reconcile.jira_permissions_validator
|
890
895
|
|
891
|
-
run_integration(
|
896
|
+
run_integration(
|
897
|
+
reconcile.jira_permissions_validator, ctx.obj, exit_on_permission_errors
|
898
|
+
)
|
892
899
|
|
893
900
|
|
894
901
|
@integration.command(short_help="Watch for changes in Jira boards and notify on Slack.")
|
@@ -0,0 +1,68 @@
|
|
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 JiralertSettings {
|
23
|
+
settings: app_interface_settings_v1 {
|
24
|
+
jiralert {
|
25
|
+
defaultIssueType
|
26
|
+
defaultReopenState
|
27
|
+
}
|
28
|
+
}
|
29
|
+
}
|
30
|
+
"""
|
31
|
+
|
32
|
+
|
33
|
+
class ConfiguredBaseModel(BaseModel):
|
34
|
+
class Config:
|
35
|
+
smart_union=True
|
36
|
+
extra=Extra.forbid
|
37
|
+
|
38
|
+
|
39
|
+
class JiralertSettingsV1(ConfiguredBaseModel):
|
40
|
+
default_issue_type: str = Field(..., alias="defaultIssueType")
|
41
|
+
default_reopen_state: str = Field(..., alias="defaultReopenState")
|
42
|
+
|
43
|
+
|
44
|
+
class AppInterfaceSettingsV1(ConfiguredBaseModel):
|
45
|
+
jiralert: Optional[JiralertSettingsV1] = Field(..., alias="jiralert")
|
46
|
+
|
47
|
+
|
48
|
+
class JiralertSettingsQueryData(ConfiguredBaseModel):
|
49
|
+
settings: Optional[list[AppInterfaceSettingsV1]] = Field(..., alias="settings")
|
50
|
+
|
51
|
+
|
52
|
+
def query(query_func: Callable, **kwargs: Any) -> JiralertSettingsQueryData:
|
53
|
+
"""
|
54
|
+
This is a convenience function which queries and parses the data into
|
55
|
+
concrete types. It should be compatible with most GQL clients.
|
56
|
+
You do not have to use it to consume the generated data classes.
|
57
|
+
Alternatively, you can also mime and alternate the behavior
|
58
|
+
of this function in the caller.
|
59
|
+
|
60
|
+
Parameters:
|
61
|
+
query_func (Callable): Function which queries your GQL Server
|
62
|
+
kwargs: optional arguments that will be passed to the query function
|
63
|
+
|
64
|
+
Returns:
|
65
|
+
JiralertSettingsQueryData: queried data parsed into generated classes
|
66
|
+
"""
|
67
|
+
raw_data: dict[Any, Any] = query_func(DEFINITION, **kwargs)
|
68
|
+
return JiralertSettingsQueryData(**raw_data)
|
reconcile/gql_definitions/jira_permissions_validator/jira_boards_for_permissions_validator.py
CHANGED
@@ -38,6 +38,19 @@ query JiraBoardsForPermissionValidation {
|
|
38
38
|
... VaultSecret
|
39
39
|
}
|
40
40
|
}
|
41
|
+
issueType
|
42
|
+
issueResolveState
|
43
|
+
issueReopenState
|
44
|
+
issueSecurityId
|
45
|
+
severityPriorityMappings {
|
46
|
+
name
|
47
|
+
mappings {
|
48
|
+
priority
|
49
|
+
}
|
50
|
+
}
|
51
|
+
disable {
|
52
|
+
integrations
|
53
|
+
}
|
41
54
|
}
|
42
55
|
}
|
43
56
|
"""
|
@@ -54,10 +67,29 @@ class JiraServerV1(ConfiguredBaseModel):
|
|
54
67
|
token: VaultSecret = Field(..., alias="token")
|
55
68
|
|
56
69
|
|
70
|
+
class SeverityPriorityMappingV1(ConfiguredBaseModel):
|
71
|
+
priority: str = Field(..., alias="priority")
|
72
|
+
|
73
|
+
|
74
|
+
class JiraSeverityPriorityMappingsV1(ConfiguredBaseModel):
|
75
|
+
name: str = Field(..., alias="name")
|
76
|
+
mappings: list[SeverityPriorityMappingV1] = Field(..., alias="mappings")
|
77
|
+
|
78
|
+
|
79
|
+
class DisableJiraBoardAutomationsV1(ConfiguredBaseModel):
|
80
|
+
integrations: Optional[list[str]] = Field(..., alias="integrations")
|
81
|
+
|
82
|
+
|
57
83
|
class JiraBoardV1(ConfiguredBaseModel):
|
58
84
|
path: str = Field(..., alias="path")
|
59
85
|
name: str = Field(..., alias="name")
|
60
86
|
server: JiraServerV1 = Field(..., alias="server")
|
87
|
+
issue_type: Optional[str] = Field(..., alias="issueType")
|
88
|
+
issue_resolve_state: Optional[str] = Field(..., alias="issueResolveState")
|
89
|
+
issue_reopen_state: Optional[str] = Field(..., alias="issueReopenState")
|
90
|
+
issue_security_id: Optional[str] = Field(..., alias="issueSecurityId")
|
91
|
+
severity_priority_mappings: JiraSeverityPriorityMappingsV1 = Field(..., alias="severityPriorityMappings")
|
92
|
+
disable: Optional[DisableJiraBoardAutomationsV1] = Field(..., alias="disable")
|
61
93
|
|
62
94
|
|
63
95
|
class JiraBoardsForPermissionValidationQueryData(ConfiguredBaseModel):
|
@@ -1,6 +1,18 @@
|
|
1
1
|
import logging
|
2
2
|
import sys
|
3
|
+
from collections.abc import Callable, Iterable
|
4
|
+
from enum import IntFlag, auto
|
5
|
+
from typing import Any
|
3
6
|
|
7
|
+
from jira import JIRAError
|
8
|
+
from pydantic import BaseModel
|
9
|
+
|
10
|
+
from reconcile.gql_definitions.jira_permissions_validator.jira_boards_for_permissions_validator import (
|
11
|
+
DEFINITION as JIRA_BOARDS_DEFINITION,
|
12
|
+
)
|
13
|
+
from reconcile.gql_definitions.jira_permissions_validator.jira_boards_for_permissions_validator import (
|
14
|
+
JiraBoardV1,
|
15
|
+
)
|
4
16
|
from reconcile.gql_definitions.jira_permissions_validator.jira_boards_for_permissions_validator import (
|
5
17
|
query as query_jira_boards,
|
6
18
|
)
|
@@ -9,31 +21,231 @@ from reconcile.typed_queries.app_interface_vault_settings import (
|
|
9
21
|
get_app_interface_vault_settings,
|
10
22
|
)
|
11
23
|
from reconcile.typed_queries.jira_settings import get_jira_settings
|
12
|
-
from reconcile.
|
13
|
-
from reconcile.utils
|
14
|
-
from reconcile.utils.
|
24
|
+
from reconcile.typed_queries.jiralert_settings import get_jiralert_settings
|
25
|
+
from reconcile.utils import gql, metrics
|
26
|
+
from reconcile.utils.disabled_integrations import integration_is_enabled
|
27
|
+
from reconcile.utils.jira_client import JiraClient, JiraWatcherSettings
|
28
|
+
from reconcile.utils.secret_reader import SecretReaderBase, create_secret_reader
|
15
29
|
|
16
30
|
QONTRACT_INTEGRATION = "jira-permissions-validator"
|
17
31
|
|
32
|
+
NameToIdMap = dict[str, str]
|
33
|
+
|
34
|
+
|
35
|
+
class BaseMetric(BaseModel):
|
36
|
+
"""Base class for metrics"""
|
37
|
+
|
38
|
+
jira_server: str
|
39
|
+
board: str
|
40
|
+
|
41
|
+
|
42
|
+
class PermissionErrorCounter(BaseMetric, metrics.GaugeMetric):
|
43
|
+
"""Boards with permission errors."""
|
44
|
+
|
45
|
+
@classmethod
|
46
|
+
def name(cls) -> str:
|
47
|
+
return "jira_permissions_validator_permission_error"
|
18
48
|
|
19
|
-
|
49
|
+
|
50
|
+
class ValidationError(IntFlag):
|
51
|
+
CANT_CREATE_ISSUE = auto()
|
52
|
+
CANT_TRANSITION_ISSUES = auto()
|
53
|
+
INVALID_ISSUE_TYPE = auto()
|
54
|
+
INVALID_ISSUE_STATE = auto()
|
55
|
+
INVALID_SECURITY_LEVEL = auto()
|
56
|
+
INVALID_PRIORITY = auto()
|
57
|
+
PERMISSION_ERROR = auto()
|
58
|
+
|
59
|
+
|
60
|
+
def board_is_valid(
|
61
|
+
jira: JiraClient,
|
62
|
+
board: JiraBoardV1,
|
63
|
+
default_issue_type: str,
|
64
|
+
default_reopen_state: str,
|
65
|
+
jira_server_priorities: NameToIdMap,
|
66
|
+
) -> ValidationError:
|
67
|
+
error = ValidationError(0)
|
68
|
+
try:
|
69
|
+
if not jira.can_create_issues():
|
70
|
+
logging.error(f"[{board.name}] can not create issues in project")
|
71
|
+
error |= ValidationError.CANT_CREATE_ISSUE
|
72
|
+
|
73
|
+
if not jira.can_transition_issues():
|
74
|
+
logging.error(
|
75
|
+
f"[{board.name}] AppSRE Jira Bot user does not have the permission to change the issue status."
|
76
|
+
)
|
77
|
+
error |= ValidationError.CANT_TRANSITION_ISSUES
|
78
|
+
|
79
|
+
issue_type = board.issue_type if board.issue_type else default_issue_type
|
80
|
+
project_issue_types = jira.project_issue_types(board.name)
|
81
|
+
project_issue_types_str = [i.name for i in project_issue_types]
|
82
|
+
if issue_type not in project_issue_types_str:
|
83
|
+
logging.error(
|
84
|
+
f"[{board.name}] {issue_type} is not a valid issue type in project. Valid issue types: {project_issue_types_str}"
|
85
|
+
)
|
86
|
+
error |= ValidationError.INVALID_ISSUE_TYPE
|
87
|
+
|
88
|
+
available_states = []
|
89
|
+
for project_issue_type in project_issue_types:
|
90
|
+
if issue_type == project_issue_type.name:
|
91
|
+
available_states = project_issue_type.statuses
|
92
|
+
break
|
93
|
+
|
94
|
+
if not available_states:
|
95
|
+
logging.error(
|
96
|
+
f"[{board.name}] {issue_type} doesn't have any status. Choose a different issue type."
|
97
|
+
)
|
98
|
+
error |= ValidationError.INVALID_ISSUE_TYPE
|
99
|
+
|
100
|
+
reopen_state = (
|
101
|
+
board.issue_reopen_state
|
102
|
+
if board.issue_reopen_state
|
103
|
+
else default_reopen_state
|
104
|
+
)
|
105
|
+
if reopen_state.lower() not in [t.lower() for t in available_states]:
|
106
|
+
logging.error(
|
107
|
+
f"[{board.name}] '{reopen_state}' is not a valid state in project. Valid states: {available_states}"
|
108
|
+
)
|
109
|
+
error |= ValidationError.INVALID_ISSUE_STATE
|
110
|
+
|
111
|
+
if board.issue_resolve_state and board.issue_resolve_state.lower() not in [
|
112
|
+
t.lower() for t in available_states
|
113
|
+
]:
|
114
|
+
logging.error(
|
115
|
+
f"[{board.name}] '{board.issue_resolve_state}' is not a valid state in project. Valid states: {available_states}"
|
116
|
+
)
|
117
|
+
error |= ValidationError.INVALID_ISSUE_STATE
|
118
|
+
|
119
|
+
if board.issue_security_id:
|
120
|
+
security_levels = jira.security_levels()
|
121
|
+
if board.issue_security_id not in [level.id for level in security_levels]:
|
122
|
+
logging.error(
|
123
|
+
f"[{board.name}] {board.issue_security_id} is not a valid security level in project. Valid security ids: "
|
124
|
+
+ ", ".join([
|
125
|
+
f"{level.name} - {level.id}" for level in jira.security_levels()
|
126
|
+
])
|
127
|
+
)
|
128
|
+
error |= ValidationError.INVALID_SECURITY_LEVEL
|
129
|
+
|
130
|
+
project_priorities = jira.project_priority_scheme()
|
131
|
+
for priority in board.severity_priority_mappings.mappings:
|
132
|
+
if priority.priority not in jira_server_priorities:
|
133
|
+
logging.error(
|
134
|
+
f"[{board.name}] {priority.priority} is not a valid Jira priority. Valid priorities: {project_priorities}"
|
135
|
+
)
|
136
|
+
error |= ValidationError.INVALID_PRIORITY
|
137
|
+
continue
|
138
|
+
if jira_server_priorities[priority.priority] not in project_priorities:
|
139
|
+
logging.error(
|
140
|
+
f"[{board.name}] {priority.priority} is not a valid priority in project. Valid priorities: {project_priorities}"
|
141
|
+
)
|
142
|
+
error |= ValidationError.INVALID_PRIORITY
|
143
|
+
except JIRAError as e:
|
144
|
+
if e.status_code != 403:
|
145
|
+
raise
|
146
|
+
logging.error(
|
147
|
+
f"[{board.name}] AppSRE Jira Bot user does not have all necessary permissions. Try granting the user the administrator permissions. API URL: {e.url}"
|
148
|
+
)
|
149
|
+
error |= ValidationError.PERMISSION_ERROR
|
150
|
+
|
151
|
+
return error
|
152
|
+
|
153
|
+
|
154
|
+
def validate_boards(
|
155
|
+
metrics_container: metrics.MetricsContainer,
|
156
|
+
secret_reader: SecretReaderBase,
|
157
|
+
exit_on_permission_errors: bool,
|
158
|
+
jira_client_settings: JiraWatcherSettings | None,
|
159
|
+
jira_boards: Iterable[JiraBoardV1],
|
160
|
+
default_issue_type: str,
|
161
|
+
default_reopen_state: str,
|
162
|
+
jira_client_class: type[JiraClient] = JiraClient,
|
163
|
+
) -> bool:
|
164
|
+
error = False
|
165
|
+
jira_clients: dict[str, JiraClient] = {}
|
166
|
+
for board in jira_boards:
|
167
|
+
logging.debug(f"[{board.name}] checking ...")
|
168
|
+
if board.server.server_url not in jira_clients:
|
169
|
+
jira_clients[board.server.server_url] = jira_client_class.create(
|
170
|
+
project_name=board.name,
|
171
|
+
token=secret_reader.read_secret(board.server.token),
|
172
|
+
server_url=board.server.server_url,
|
173
|
+
jira_watcher_settings=jira_client_settings,
|
174
|
+
)
|
175
|
+
|
176
|
+
jira = jira_clients[board.server.server_url]
|
177
|
+
jira.project = board.name
|
178
|
+
try:
|
179
|
+
error_flags = board_is_valid(
|
180
|
+
jira=jira,
|
181
|
+
board=board,
|
182
|
+
default_issue_type=default_issue_type,
|
183
|
+
default_reopen_state=default_reopen_state,
|
184
|
+
jira_server_priorities={p.name: p.id for p in jira.priorities()},
|
185
|
+
)
|
186
|
+
match error_flags:
|
187
|
+
case 0:
|
188
|
+
# no errors
|
189
|
+
logging.debug(f"[{board.name}] is valid")
|
190
|
+
case ValidationError.PERMISSION_ERROR:
|
191
|
+
# we don't have all the permissions, but we can create jira tickets
|
192
|
+
metrics_container.set_gauge(
|
193
|
+
PermissionErrorCounter(
|
194
|
+
jira_server=board.server.server_url,
|
195
|
+
board=board.name,
|
196
|
+
),
|
197
|
+
value=1,
|
198
|
+
)
|
199
|
+
# don't fail during PR checks at the moment
|
200
|
+
# this make the transistion to the new integration behaviour much smoother
|
201
|
+
if exit_on_permission_errors:
|
202
|
+
error = True
|
203
|
+
case (
|
204
|
+
ValidationError.PERMISSION_ERROR
|
205
|
+
| ValidationError.CANT_CREATE_ISSUE
|
206
|
+
):
|
207
|
+
# we can't create jira tickets, and we don't have all needed the permissions
|
208
|
+
error = True
|
209
|
+
case _:
|
210
|
+
error = True
|
211
|
+
except Exception as e:
|
212
|
+
logging.error(f"[{board.name}] {e}")
|
213
|
+
error = True
|
214
|
+
return error
|
215
|
+
|
216
|
+
|
217
|
+
def get_jira_boards(query_func: Callable) -> list[JiraBoardV1]:
|
218
|
+
return [
|
219
|
+
board
|
220
|
+
for board in query_jira_boards(query_func=query_func).jira_boards or []
|
221
|
+
if integration_is_enabled(QONTRACT_INTEGRATION, board)
|
222
|
+
]
|
223
|
+
|
224
|
+
|
225
|
+
def run(dry_run: bool, exit_on_permission_errors: bool) -> None:
|
20
226
|
gql_api = gql.get_api()
|
21
|
-
settings = get_jira_settings(gql_api=gql_api)
|
227
|
+
settings = get_jira_settings(gql_api=gql_api.query)
|
228
|
+
jiralert_settings = get_jiralert_settings(query_func=gql_api.query)
|
22
229
|
vault_settings = get_app_interface_vault_settings()
|
23
230
|
secret_reader = create_secret_reader(use_vault=vault_settings.vault)
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
231
|
+
boards = get_jira_boards(query_func=gql_api.query)
|
232
|
+
|
233
|
+
with metrics.transactional_metrics("jira-boards") as metrics_container:
|
234
|
+
error = validate_boards(
|
235
|
+
metrics_container=metrics_container,
|
236
|
+
secret_reader=secret_reader,
|
237
|
+
exit_on_permission_errors=exit_on_permission_errors,
|
238
|
+
jira_client_settings=settings.jira_watcher,
|
239
|
+
jira_boards=boards,
|
240
|
+
default_issue_type=jiralert_settings.default_issue_type,
|
241
|
+
default_reopen_state=jiralert_settings.default_reopen_state,
|
33
242
|
)
|
34
|
-
if not jira.can_create_issues():
|
35
|
-
error = True
|
36
|
-
logging.error(f"can not create issues in project {jira.project}")
|
37
243
|
|
38
244
|
if error:
|
39
245
|
sys.exit(ExitCodes.ERROR)
|
246
|
+
|
247
|
+
|
248
|
+
def early_exit_desired_state(*args: Any, **kwargs: Any) -> dict[str, Any]:
|
249
|
+
return {
|
250
|
+
"boards": gql.get_api().query(JIRA_BOARDS_DEFINITION)["jira_boards"],
|
251
|
+
}
|
@@ -0,0 +1,386 @@
|
|
1
|
+
from collections.abc import Callable, Mapping
|
2
|
+
from typing import Any
|
3
|
+
from unittest.mock import Mock
|
4
|
+
|
5
|
+
import pytest
|
6
|
+
from jira import JIRAError
|
7
|
+
from pytest_mock import MockerFixture
|
8
|
+
|
9
|
+
from reconcile.gql_definitions.jira_permissions_validator.jira_boards_for_permissions_validator import (
|
10
|
+
JiraBoardV1,
|
11
|
+
)
|
12
|
+
from reconcile.jira_permissions_validator import (
|
13
|
+
ValidationError,
|
14
|
+
board_is_valid,
|
15
|
+
get_jira_boards,
|
16
|
+
validate_boards,
|
17
|
+
)
|
18
|
+
from reconcile.test.fixtures import Fixtures
|
19
|
+
from reconcile.utils import metrics
|
20
|
+
from reconcile.utils.jira_client import IssueType, JiraClient, SecurityLevel
|
21
|
+
|
22
|
+
|
23
|
+
@pytest.fixture
|
24
|
+
def fx() -> Fixtures:
|
25
|
+
return Fixtures("jira_permissions_validator")
|
26
|
+
|
27
|
+
|
28
|
+
@pytest.fixture
|
29
|
+
def raw_fixture_data(fx: Fixtures) -> dict[str, Any]:
|
30
|
+
return fx.get_anymarkup("boards.yml")
|
31
|
+
|
32
|
+
|
33
|
+
@pytest.fixture
|
34
|
+
def query_func(
|
35
|
+
data_factory: Callable[[type[JiraBoardV1], Mapping[str, Any]], Mapping[str, Any]],
|
36
|
+
raw_fixture_data: dict[str, Any],
|
37
|
+
) -> Callable:
|
38
|
+
return lambda *args, **kwargs: {
|
39
|
+
"jira_boards": [
|
40
|
+
data_factory(JiraBoardV1, item) for item in raw_fixture_data["jira_boards"]
|
41
|
+
]
|
42
|
+
}
|
43
|
+
|
44
|
+
|
45
|
+
@pytest.fixture
|
46
|
+
def boards(query_func: Callable) -> list[JiraBoardV1]:
|
47
|
+
return get_jira_boards(query_func)
|
48
|
+
|
49
|
+
|
50
|
+
def test_jira_permissions_validator_get_jira_boards(
|
51
|
+
query_func: Callable, gql_class_factory: Callable
|
52
|
+
) -> None:
|
53
|
+
default = {
|
54
|
+
"name": "jira-board-default",
|
55
|
+
"server": {
|
56
|
+
"serverUrl": "https://jira-server.com",
|
57
|
+
"token": {"path": "vault/path/token", "field": "token"},
|
58
|
+
},
|
59
|
+
"issueResolveState": "Closed",
|
60
|
+
"severityPriorityMappings": {
|
61
|
+
"name": "major-major",
|
62
|
+
"mappings": [
|
63
|
+
{"priority": "Minor"},
|
64
|
+
{"priority": "Major"},
|
65
|
+
{"priority": "Critical"},
|
66
|
+
],
|
67
|
+
},
|
68
|
+
}
|
69
|
+
custom = {
|
70
|
+
"name": "jira-board-custom",
|
71
|
+
"server": {
|
72
|
+
"serverUrl": "https://jira-server.com",
|
73
|
+
"token": {"path": "vault/path/token", "field": "token"},
|
74
|
+
},
|
75
|
+
"issueType": "bug",
|
76
|
+
"issueResolveState": "Closed",
|
77
|
+
"issueReopenState": "Open",
|
78
|
+
"issueSecurityId": 32168,
|
79
|
+
"severityPriorityMappings": {
|
80
|
+
"name": "major-major",
|
81
|
+
"mappings": [
|
82
|
+
{"priority": "Minor"},
|
83
|
+
{"priority": "Major"},
|
84
|
+
{"priority": "Major"},
|
85
|
+
{"priority": "Critical"},
|
86
|
+
],
|
87
|
+
},
|
88
|
+
}
|
89
|
+
assert get_jira_boards(query_func) == [
|
90
|
+
gql_class_factory(JiraBoardV1, default),
|
91
|
+
gql_class_factory(JiraBoardV1, custom),
|
92
|
+
]
|
93
|
+
|
94
|
+
|
95
|
+
@pytest.mark.parametrize(
|
96
|
+
"board_is_valid, exit_on_permission_errors, expected, metric_set",
|
97
|
+
[
|
98
|
+
(0, True, False, False),
|
99
|
+
(ValidationError.CANT_CREATE_ISSUE, True, True, False),
|
100
|
+
(ValidationError.CANT_TRANSITION_ISSUES, True, True, False),
|
101
|
+
(ValidationError.INVALID_ISSUE_TYPE, True, True, False),
|
102
|
+
(ValidationError.INVALID_ISSUE_STATE, True, True, False),
|
103
|
+
(ValidationError.INVALID_SECURITY_LEVEL, True, True, False),
|
104
|
+
(ValidationError.INVALID_PRIORITY, True, True, False),
|
105
|
+
(ValidationError.PERMISSION_ERROR, True, True, True),
|
106
|
+
# special case: CANT_CREATE_ISSUE and PERMISSION_ERROR
|
107
|
+
(
|
108
|
+
ValidationError.CANT_CREATE_ISSUE | ValidationError.PERMISSION_ERROR,
|
109
|
+
True,
|
110
|
+
True,
|
111
|
+
False,
|
112
|
+
),
|
113
|
+
(
|
114
|
+
ValidationError.CANT_CREATE_ISSUE | ValidationError.PERMISSION_ERROR,
|
115
|
+
False,
|
116
|
+
True,
|
117
|
+
False,
|
118
|
+
),
|
119
|
+
# test with another error
|
120
|
+
(
|
121
|
+
ValidationError.INVALID_PRIORITY | ValidationError.PERMISSION_ERROR,
|
122
|
+
True,
|
123
|
+
True,
|
124
|
+
False,
|
125
|
+
),
|
126
|
+
(
|
127
|
+
ValidationError.INVALID_PRIORITY | ValidationError.PERMISSION_ERROR,
|
128
|
+
False,
|
129
|
+
True,
|
130
|
+
False,
|
131
|
+
),
|
132
|
+
],
|
133
|
+
)
|
134
|
+
def test_jira_permissions_validator_validate_boards(
|
135
|
+
mocker: MockerFixture,
|
136
|
+
boards: list[JiraBoardV1],
|
137
|
+
secret_reader: Mock,
|
138
|
+
board_is_valid: ValidationError,
|
139
|
+
exit_on_permission_errors: bool,
|
140
|
+
expected: bool,
|
141
|
+
metric_set: bool,
|
142
|
+
) -> None:
|
143
|
+
board_is_valid_mock = mocker.patch(
|
144
|
+
"reconcile.jira_permissions_validator.board_is_valid"
|
145
|
+
)
|
146
|
+
board_is_valid_mock.return_value = board_is_valid
|
147
|
+
metrics_container_mock = mocker.create_autospec(spec=metrics.MetricsContainer)
|
148
|
+
jira_client_class = mocker.create_autospec(spec=JiraClient)
|
149
|
+
assert (
|
150
|
+
validate_boards(
|
151
|
+
metrics_container=metrics_container_mock,
|
152
|
+
secret_reader=secret_reader,
|
153
|
+
exit_on_permission_errors=exit_on_permission_errors,
|
154
|
+
jira_client_settings=None,
|
155
|
+
jira_boards=boards,
|
156
|
+
default_issue_type="task",
|
157
|
+
default_reopen_state="new",
|
158
|
+
jira_client_class=jira_client_class,
|
159
|
+
)
|
160
|
+
== expected
|
161
|
+
)
|
162
|
+
if metric_set:
|
163
|
+
metrics_container_mock.set_gauge.assert_called()
|
164
|
+
else:
|
165
|
+
metrics_container_mock.set_gauge.assert_not_called()
|
166
|
+
|
167
|
+
|
168
|
+
def test_jira_permissions_validator_board_is_valid_happy_path(
|
169
|
+
mocker: MockerFixture, gql_class_factory: Callable
|
170
|
+
) -> None:
|
171
|
+
board = gql_class_factory(
|
172
|
+
JiraBoardV1,
|
173
|
+
{
|
174
|
+
"name": "jira-board-default",
|
175
|
+
"server": {
|
176
|
+
"serverUrl": "https://jira-server.com",
|
177
|
+
"token": {"path": "vault/path/token", "field": "token"},
|
178
|
+
},
|
179
|
+
"issueType": "bug",
|
180
|
+
"issueResolveState": "Closed",
|
181
|
+
"issueReopenState": "Open",
|
182
|
+
"issueSecurityId": "32168",
|
183
|
+
"severityPriorityMappings": {
|
184
|
+
"name": "major-major",
|
185
|
+
"mappings": [
|
186
|
+
{"priority": "Minor"},
|
187
|
+
{"priority": "Major"},
|
188
|
+
{"priority": "Critical"},
|
189
|
+
],
|
190
|
+
},
|
191
|
+
},
|
192
|
+
)
|
193
|
+
jira_client = mocker.create_autospec(spec=JiraClient)
|
194
|
+
jira_client.can_create_issues.return_value = True
|
195
|
+
jira_client.can_transition_issues.return_value = True
|
196
|
+
jira_client.project_issue_types.return_value = [
|
197
|
+
IssueType(id="1", name="task", statuses=["open", "closed"]),
|
198
|
+
IssueType(id="2", name="bug", statuses=["open", "closed"]),
|
199
|
+
]
|
200
|
+
jira_client.security_levels.return_value = [
|
201
|
+
SecurityLevel(id="32168", name="foo"),
|
202
|
+
SecurityLevel(id="1", name="bar"),
|
203
|
+
]
|
204
|
+
jira_client.project_priority_scheme.return_value = ["1", "2", "3"]
|
205
|
+
assert board_is_valid(
|
206
|
+
jira=jira_client,
|
207
|
+
board=board,
|
208
|
+
default_issue_type="task",
|
209
|
+
default_reopen_state="new",
|
210
|
+
jira_server_priorities={"Minor": "1", "Major": "2", "Critical": "3"},
|
211
|
+
) == ValidationError(0)
|
212
|
+
|
213
|
+
|
214
|
+
def test_jira_permissions_validator_board_is_valid_all_errors(
|
215
|
+
mocker: MockerFixture, gql_class_factory: Callable
|
216
|
+
) -> None:
|
217
|
+
board = gql_class_factory(
|
218
|
+
JiraBoardV1,
|
219
|
+
{
|
220
|
+
"name": "jira-board-default",
|
221
|
+
"server": {
|
222
|
+
"serverUrl": "https://jira-server.com",
|
223
|
+
"token": {"path": "vault/path/token", "field": "token"},
|
224
|
+
},
|
225
|
+
"issueType": "bug",
|
226
|
+
"issueResolveState": "Closed",
|
227
|
+
"issueReopenState": "Open",
|
228
|
+
"issueSecurityId": "32168",
|
229
|
+
"severityPriorityMappings": {
|
230
|
+
"name": "major-major",
|
231
|
+
"mappings": [
|
232
|
+
{"priority": "Minor"},
|
233
|
+
{"priority": "Major"},
|
234
|
+
{"priority": "Critical"},
|
235
|
+
],
|
236
|
+
},
|
237
|
+
},
|
238
|
+
)
|
239
|
+
jira_client = mocker.create_autospec(spec=JiraClient)
|
240
|
+
jira_client.can_create_issues.return_value = False
|
241
|
+
jira_client.can_transition_issues.return_value = False
|
242
|
+
jira_client.project_issue_types.return_value = []
|
243
|
+
jira_client.security_levels.return_value = [
|
244
|
+
SecurityLevel(id="1", name="bar"),
|
245
|
+
]
|
246
|
+
jira_client.project_priority_scheme.return_value = ["1", "2"]
|
247
|
+
assert (
|
248
|
+
board_is_valid(
|
249
|
+
jira=jira_client,
|
250
|
+
board=board,
|
251
|
+
default_issue_type="task",
|
252
|
+
default_reopen_state="new",
|
253
|
+
jira_server_priorities={"Minor": "1", "Major": "2", "Critical": "3"},
|
254
|
+
)
|
255
|
+
== ValidationError.CANT_CREATE_ISSUE
|
256
|
+
| ValidationError.CANT_TRANSITION_ISSUES
|
257
|
+
| ValidationError.INVALID_ISSUE_TYPE
|
258
|
+
| ValidationError.INVALID_ISSUE_STATE
|
259
|
+
| ValidationError.INVALID_SECURITY_LEVEL
|
260
|
+
| ValidationError.INVALID_PRIORITY
|
261
|
+
)
|
262
|
+
|
263
|
+
|
264
|
+
def test_jira_permissions_validator_board_is_valid_bad_issue_status(
|
265
|
+
mocker: MockerFixture, gql_class_factory: Callable
|
266
|
+
) -> None:
|
267
|
+
board = gql_class_factory(
|
268
|
+
JiraBoardV1,
|
269
|
+
{
|
270
|
+
"name": "jira-board-default",
|
271
|
+
"server": {
|
272
|
+
"serverUrl": "https://jira-server.com",
|
273
|
+
"token": {"path": "vault/path/token", "field": "token"},
|
274
|
+
},
|
275
|
+
"issueType": "bug",
|
276
|
+
"issueResolveState": "Closed",
|
277
|
+
"issueReopenState": "Open",
|
278
|
+
"issueSecurityId": "32168",
|
279
|
+
"severityPriorityMappings": {
|
280
|
+
"name": "major-major",
|
281
|
+
"mappings": [
|
282
|
+
{"priority": "Minor"},
|
283
|
+
{"priority": "Major"},
|
284
|
+
{"priority": "Critical"},
|
285
|
+
],
|
286
|
+
},
|
287
|
+
},
|
288
|
+
)
|
289
|
+
jira_client = mocker.create_autospec(spec=JiraClient)
|
290
|
+
jira_client.can_create_issues.return_value = True
|
291
|
+
jira_client.can_transition_issues.return_value = True
|
292
|
+
jira_client.project_issue_types.return_value = [
|
293
|
+
IssueType(id="1", name="task", statuses=["not - open", "closed"]),
|
294
|
+
IssueType(id="2", name="bug", statuses=["not - open", "closed"]),
|
295
|
+
]
|
296
|
+
jira_client.security_levels.return_value = [
|
297
|
+
SecurityLevel(id="32168", name="foo"),
|
298
|
+
SecurityLevel(id="1", name="bar"),
|
299
|
+
]
|
300
|
+
jira_client.project_priority_scheme.return_value = ["1", "2", "3"]
|
301
|
+
assert (
|
302
|
+
board_is_valid(
|
303
|
+
jira=jira_client,
|
304
|
+
board=board,
|
305
|
+
default_issue_type="task",
|
306
|
+
default_reopen_state="new",
|
307
|
+
jira_server_priorities={"Minor": "1", "Major": "2", "Critical": "3"},
|
308
|
+
)
|
309
|
+
== ValidationError.INVALID_ISSUE_STATE
|
310
|
+
)
|
311
|
+
|
312
|
+
|
313
|
+
def test_jira_permissions_validator_board_is_valid_permission_error(
|
314
|
+
mocker: MockerFixture, gql_class_factory: Callable
|
315
|
+
) -> None:
|
316
|
+
board = gql_class_factory(
|
317
|
+
JiraBoardV1,
|
318
|
+
{
|
319
|
+
"name": "jira-board-default",
|
320
|
+
"server": {
|
321
|
+
"serverUrl": "https://jira-server.com",
|
322
|
+
"token": {"path": "vault/path/token", "field": "token"},
|
323
|
+
},
|
324
|
+
"issueType": "bug",
|
325
|
+
"issueResolveState": "Closed",
|
326
|
+
"issueReopenState": "Open",
|
327
|
+
"issueSecurityId": "32168",
|
328
|
+
"severityPriorityMappings": {
|
329
|
+
"name": "major-major",
|
330
|
+
"mappings": [
|
331
|
+
{"priority": "Minor"},
|
332
|
+
{"priority": "Major"},
|
333
|
+
{"priority": "Critical"},
|
334
|
+
],
|
335
|
+
},
|
336
|
+
},
|
337
|
+
)
|
338
|
+
jira_client = mocker.create_autospec(spec=JiraClient)
|
339
|
+
jira_client.can_create_issues.side_effect = JIRAError(status_code=403)
|
340
|
+
assert (
|
341
|
+
board_is_valid(
|
342
|
+
jira=jira_client,
|
343
|
+
board=board,
|
344
|
+
default_issue_type="task",
|
345
|
+
default_reopen_state="new",
|
346
|
+
jira_server_priorities={"Minor": "1", "Major": "2", "Critical": "3"},
|
347
|
+
)
|
348
|
+
== ValidationError.PERMISSION_ERROR
|
349
|
+
)
|
350
|
+
|
351
|
+
|
352
|
+
def test_jira_permissions_validator_board_is_valid_exception(
|
353
|
+
mocker: MockerFixture, gql_class_factory: Callable
|
354
|
+
) -> None:
|
355
|
+
board = gql_class_factory(
|
356
|
+
JiraBoardV1,
|
357
|
+
{
|
358
|
+
"name": "jira-board-default",
|
359
|
+
"server": {
|
360
|
+
"serverUrl": "https://jira-server.com",
|
361
|
+
"token": {"path": "vault/path/token", "field": "token"},
|
362
|
+
},
|
363
|
+
"issueType": "bug",
|
364
|
+
"issueResolveState": "Closed",
|
365
|
+
"issueReopenState": "Open",
|
366
|
+
"issueSecurityId": "32168",
|
367
|
+
"severityPriorityMappings": {
|
368
|
+
"name": "major-major",
|
369
|
+
"mappings": [
|
370
|
+
{"priority": "Minor"},
|
371
|
+
{"priority": "Major"},
|
372
|
+
{"priority": "Critical"},
|
373
|
+
],
|
374
|
+
},
|
375
|
+
},
|
376
|
+
)
|
377
|
+
jira_client = mocker.create_autospec(spec=JiraClient)
|
378
|
+
jira_client.can_create_issues.side_effect = JIRAError(status_code=401)
|
379
|
+
with pytest.raises(JIRAError):
|
380
|
+
board_is_valid(
|
381
|
+
jira=jira_client,
|
382
|
+
board=board,
|
383
|
+
default_issue_type="task",
|
384
|
+
default_reopen_state="new",
|
385
|
+
jira_server_priorities={"Minor": "1", "Major": "2", "Critical": "3"},
|
386
|
+
)
|
@@ -0,0 +1,22 @@
|
|
1
|
+
from collections.abc import Callable
|
2
|
+
|
3
|
+
from reconcile.gql_definitions.common.jiralert_settings import (
|
4
|
+
JiralertSettingsV1,
|
5
|
+
query,
|
6
|
+
)
|
7
|
+
from reconcile.utils import gql
|
8
|
+
from reconcile.utils.exceptions import AppInterfaceSettingsError
|
9
|
+
|
10
|
+
|
11
|
+
def get_jiralert_settings(
|
12
|
+
query_func: Callable | None = None,
|
13
|
+
) -> JiralertSettingsV1:
|
14
|
+
"""Returns App Interface Settings and raises err if none are found"""
|
15
|
+
if not query_func:
|
16
|
+
query_func = gql.get_api().query
|
17
|
+
data = query(query_func)
|
18
|
+
if data.settings and len(data.settings) == 1:
|
19
|
+
if data.settings[0].jiralert:
|
20
|
+
return data.settings[0].jiralert
|
21
|
+
return JiralertSettingsV1(defaultIssueType="Task", defaultReopenState="To Do")
|
22
|
+
raise AppInterfaceSettingsError("jira settings not uniquely defined.")
|
reconcile/utils/jira_client.py
CHANGED
@@ -16,6 +16,7 @@ from jira import (
|
|
16
16
|
Issue,
|
17
17
|
)
|
18
18
|
from jira.client import ResultList
|
19
|
+
from pydantic import BaseModel
|
19
20
|
|
20
21
|
from reconcile.utils.secret_reader import SecretReader
|
21
22
|
|
@@ -25,6 +26,28 @@ class JiraWatcherSettings(Protocol):
|
|
25
26
|
connect_timeout: int
|
26
27
|
|
27
28
|
|
29
|
+
class SecurityLevel(BaseModel):
|
30
|
+
"""Jira security level."""
|
31
|
+
|
32
|
+
id: str
|
33
|
+
name: str
|
34
|
+
|
35
|
+
|
36
|
+
class Priority(BaseModel):
|
37
|
+
"""Jira priority."""
|
38
|
+
|
39
|
+
id: str
|
40
|
+
name: str
|
41
|
+
|
42
|
+
|
43
|
+
class IssueType(BaseModel):
|
44
|
+
"""Jira issue type."""
|
45
|
+
|
46
|
+
id: str
|
47
|
+
name: str
|
48
|
+
statuses: list[str]
|
49
|
+
|
50
|
+
|
28
51
|
class JiraClient:
|
29
52
|
"""Wrapper around Jira client."""
|
30
53
|
|
@@ -156,3 +179,29 @@ class JiraClient:
|
|
156
179
|
|
157
180
|
def can_create_issues(self) -> bool:
|
158
181
|
return self.can_i("CREATE_ISSUES")
|
182
|
+
|
183
|
+
def can_transition_issues(self) -> bool:
|
184
|
+
return self.can_i("TRANSITION_ISSUES")
|
185
|
+
|
186
|
+
def project_issue_types(self, project: str) -> list[IssueType]:
|
187
|
+
return [
|
188
|
+
IssueType(id=t.id, name=t.name, statuses=[s.name for s in t.statuses])
|
189
|
+
for t in self.jira.issue_types_for_project(project)
|
190
|
+
]
|
191
|
+
|
192
|
+
def security_levels(self) -> list[SecurityLevel]:
|
193
|
+
"""Return a list of all available security levels for the project.
|
194
|
+
|
195
|
+
This API endpoint needs admin/owner project permissions.
|
196
|
+
"""
|
197
|
+
scheme = self.jira.project_issue_security_level_scheme(self.project)
|
198
|
+
return [SecurityLevel(id=level.id, name=level.name) for level in scheme.levels]
|
199
|
+
|
200
|
+
def priorities(self) -> list[Priority]:
|
201
|
+
"""Return a list of all available Jira priorities."""
|
202
|
+
return [Priority(id=p.id, name=p.name) for p in self.jira.priorities()]
|
203
|
+
|
204
|
+
def project_priority_scheme(self) -> list[str]:
|
205
|
+
"""Return a list of all priority IDs for the project."""
|
206
|
+
scheme = self.jira.project_priority_scheme(self.project)
|
207
|
+
return scheme.optionIds
|
File without changes
|
File without changes
|
{qontract_reconcile-0.10.1rc466.dist-info → qontract_reconcile-0.10.1rc467.dist-info}/top_level.txt
RENAMED
File without changes
|