qontract-reconcile 0.10.1rc465__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.1rc465.dist-info → qontract_reconcile-0.10.1rc467.dist-info}/METADATA +1 -1
- {qontract_reconcile-0.10.1rc465.dist-info → qontract_reconcile-0.10.1rc467.dist-info}/RECORD +16 -13
- reconcile/cli.py +9 -21
- reconcile/gql_definitions/common/jiralert_settings.py +68 -0
- reconcile/gql_definitions/jira_permissions_validator/jira_boards_for_permissions_validator.py +32 -0
- reconcile/gql_definitions/rhidp/organizations.py +94 -0
- reconcile/jira_permissions_validator.py +229 -17
- reconcile/rhidp/common.py +20 -0
- reconcile/rhidp/ocm_oidc_idp/integration.py +5 -2
- reconcile/rhidp/sso_client/integration.py +5 -2
- reconcile/test/test_jira_permissions_validator.py +386 -0
- reconcile/typed_queries/jiralert_settings.py +22 -0
- reconcile/utils/jira_client.py +49 -0
- reconcile/gql_definitions/ocm_oidc_idp/__init__.py +0 -0
- {qontract_reconcile-0.10.1rc465.dist-info → qontract_reconcile-0.10.1rc467.dist-info}/WHEEL +0 -0
- {qontract_reconcile-0.10.1rc465.dist-info → qontract_reconcile-0.10.1rc467.dist-info}/entry_points.txt +0 -0
- {qontract_reconcile-0.10.1rc465.dist-info → qontract_reconcile-0.10.1rc467.dist-info}/top_level.txt +0 -0
{qontract_reconcile-0.10.1rc465.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.1rc465.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
|
@@ -256,7 +257,6 @@ reconcile/gql_definitions/membershipsources/roles.py,sha256=d3nv3GLsj_eKgwB1glsi
|
|
256
257
|
reconcile/gql_definitions/ocm_labels/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
257
258
|
reconcile/gql_definitions/ocm_labels/clusters.py,sha256=enTeemxY5GQAQj58eQUK0mzsSBpLrcxM9rgoEWXRT1Q,2905
|
258
259
|
reconcile/gql_definitions/ocm_labels/organizations.py,sha256=mmYB5C5Fp_nPzwBDKdKG4qWiLre2VkZ26U_2O-jRKC4,2001
|
259
|
-
reconcile/gql_definitions/ocm_oidc_idp/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
260
260
|
reconcile/gql_definitions/ocm_subscription_labels/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
261
261
|
reconcile/gql_definitions/openshift_groups/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
262
262
|
reconcile/gql_definitions/openshift_groups/managed_groups.py,sha256=mBWZX9xxeW3eB1ylnAI5x_7UBacRqJf_H6um-fB_nKc,2013
|
@@ -264,6 +264,7 @@ reconcile/gql_definitions/openshift_groups/managed_roles.py,sha256=J-uBsTczOHhUC
|
|
264
264
|
reconcile/gql_definitions/quay_membership/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
265
265
|
reconcile/gql_definitions/quay_membership/quay_membership.py,sha256=H2xHvdNr3K0QzB2dituwStUIWCqePt35dkgeUZycECM,2824
|
266
266
|
reconcile/gql_definitions/rhidp/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
267
|
+
reconcile/gql_definitions/rhidp/organizations.py,sha256=8KVbWyvjDlvn-VGpYF06f4ZH6_PZMNCCZ8fa9W0s-Tk,2553
|
267
268
|
reconcile/gql_definitions/service_dependencies/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
268
269
|
reconcile/gql_definitions/service_dependencies/jenkins_instance_fragment.py,sha256=gEcYRrdhGKG83cOpGEnecE0mCxpQHLRzXFCp5FBIhLA,699
|
269
270
|
reconcile/gql_definitions/service_dependencies/service_dependencies.py,sha256=CpMq9KjhFA61yniLo_11ypVInoeMBXbNmcY7_VAep-0,4700
|
@@ -325,15 +326,15 @@ reconcile/oum/standalone.py,sha256=bzyV8wz3SrERG9zJRFiJCBzSIGwDNj9sNqUytngDw94,7
|
|
325
326
|
reconcile/prometheus_rules_tester/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
326
327
|
reconcile/prometheus_rules_tester/integration.py,sha256=OBEVXqixTjnzi36VpPFR3rEEIDtPcSY0bopxd3M1vz8,9161
|
327
328
|
reconcile/rhidp/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
328
|
-
reconcile/rhidp/common.py,sha256=
|
329
|
+
reconcile/rhidp/common.py,sha256=suTh9T4dPOgrKi-rDALgjzCSL9JHGnkTYALFIIjNCJE,6801
|
329
330
|
reconcile/rhidp/metrics.py,sha256=Yp0GtpjhieEdru0qkG3osBTJiKUzg6CAjwPoFTQDnCg,417
|
330
331
|
reconcile/rhidp/ocm_oidc_idp/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
331
332
|
reconcile/rhidp/ocm_oidc_idp/base.py,sha256=BTjNl83Sn5jXHb9ogr4FCWeM1z6YrqZ2jYOYHg7_ZCI,7303
|
332
|
-
reconcile/rhidp/ocm_oidc_idp/integration.py,sha256=
|
333
|
+
reconcile/rhidp/ocm_oidc_idp/integration.py,sha256=S6gfEMd_KbmqIT2W4lylVdO83EHvAoElw-65ih5loKA,1953
|
333
334
|
reconcile/rhidp/ocm_oidc_idp/metrics.py,sha256=mfk4grSmH8zfA6Oz5Q4L76Uh1Ip6HlqUl23ap2vWlRE,591
|
334
335
|
reconcile/rhidp/sso_client/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
335
336
|
reconcile/rhidp/sso_client/base.py,sha256=EfQ2ewcOKh5idg46UKAkY6z0m_nGQfvnQKffa2OL4Sw,8576
|
336
|
-
reconcile/rhidp/sso_client/integration.py,sha256=
|
337
|
+
reconcile/rhidp/sso_client/integration.py,sha256=kA8g7c38ZBSdrRtyfEqy_WgSreD1PbwY7ZIN-3tZRPc,2221
|
337
338
|
reconcile/rhidp/sso_client/metrics.py,sha256=Tq7tSOsqL3XdcPUdozxqzSPIodUeOV87UCTqpuuqqhw,1013
|
338
339
|
reconcile/saas_auto_promotions_manager/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
339
340
|
reconcile/saas_auto_promotions_manager/integration.py,sha256=1_e9LX-9oMHiuWPlQZ34WbsrxfW0IFANZtV_jXKW_PQ,5580
|
@@ -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.")
|
@@ -2262,12 +2269,6 @@ def ocm_github_idp(ctx, vault_input_path):
|
|
2262
2269
|
required=False,
|
2263
2270
|
envvar="RHIDP_OCM_ENV",
|
2264
2271
|
)
|
2265
|
-
@click.option(
|
2266
|
-
"--ocm-org-ids",
|
2267
|
-
help="A comma seperated list of OCM organization IDs RHIDP should operator on. If none is specified, all organizations are considered.",
|
2268
|
-
required=False,
|
2269
|
-
envvar="RHIDP_OCM_ORG_IDS",
|
2270
|
-
)
|
2271
2272
|
@click.option(
|
2272
2273
|
"--default-auth-name",
|
2273
2274
|
default="redhat-sso",
|
@@ -2291,7 +2292,6 @@ def ocm_github_idp(ctx, vault_input_path):
|
|
2291
2292
|
def ocm_oidc_idp(
|
2292
2293
|
ctx,
|
2293
2294
|
ocm_env,
|
2294
|
-
ocm_org_ids,
|
2295
2295
|
default_auth_name,
|
2296
2296
|
default_auth_issuer_url,
|
2297
2297
|
vault_input_path,
|
@@ -2301,13 +2301,11 @@ def ocm_oidc_idp(
|
|
2301
2301
|
OCMOidcIdpParams,
|
2302
2302
|
)
|
2303
2303
|
|
2304
|
-
parsed_ocm_org_ids = set(ocm_org_ids.split(",")) if ocm_org_ids else None
|
2305
2304
|
run_class_integration(
|
2306
2305
|
integration=OCMOidcIdp(
|
2307
2306
|
OCMOidcIdpParams(
|
2308
2307
|
vault_input_path=vault_input_path,
|
2309
2308
|
ocm_environment=ocm_env,
|
2310
|
-
ocm_organization_ids=parsed_ocm_org_ids,
|
2311
2309
|
default_auth_name=default_auth_name,
|
2312
2310
|
default_auth_issuer_url=default_auth_issuer_url,
|
2313
2311
|
)
|
@@ -2341,12 +2339,6 @@ def ocm_oidc_idp(
|
|
2341
2339
|
required=False,
|
2342
2340
|
envvar="RHIDP_OCM_ENV",
|
2343
2341
|
)
|
2344
|
-
@click.option(
|
2345
|
-
"--ocm-org-ids",
|
2346
|
-
help="A comma seperated list of OCM organization IDs RHIDP should operator on. If none is specified, all organizations are considered.",
|
2347
|
-
required=False,
|
2348
|
-
envvar="RHIDP_OCM_ORG_IDS",
|
2349
|
-
)
|
2350
2342
|
@click.option(
|
2351
2343
|
"--default-auth-name",
|
2352
2344
|
default="redhat-sso",
|
@@ -2368,7 +2360,6 @@ def rhidp_sso_client(
|
|
2368
2360
|
contact_emails,
|
2369
2361
|
vault_input_path,
|
2370
2362
|
ocm_env,
|
2371
|
-
ocm_org_ids,
|
2372
2363
|
default_auth_name,
|
2373
2364
|
default_auth_issuer_url,
|
2374
2365
|
):
|
@@ -2385,9 +2376,6 @@ def rhidp_sso_client(
|
|
2385
2376
|
),
|
2386
2377
|
vault_input_path=vault_input_path,
|
2387
2378
|
ocm_environment=ocm_env,
|
2388
|
-
ocm_organization_ids=set(ocm_org_ids.split(","))
|
2389
|
-
if ocm_org_ids
|
2390
|
-
else None,
|
2391
2379
|
default_auth_name=default_auth_name,
|
2392
2380
|
default_auth_issuer_url=default_auth_issuer_url,
|
2393
2381
|
contacts=list(set(contact_emails.split(","))),
|
@@ -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):
|
@@ -0,0 +1,94 @@
|
|
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
|
+
from reconcile.gql_definitions.fragments.disable import DisableAutomations
|
21
|
+
from reconcile.gql_definitions.fragments.ocm_environment import OCMEnvironment
|
22
|
+
|
23
|
+
|
24
|
+
DEFINITION = """
|
25
|
+
fragment DisableAutomations on DisableClusterAutomations_v1 {
|
26
|
+
integrations
|
27
|
+
}
|
28
|
+
|
29
|
+
fragment OCMEnvironment on OpenShiftClusterManagerEnvironment_v1 {
|
30
|
+
name
|
31
|
+
url
|
32
|
+
accessTokenClientId
|
33
|
+
accessTokenUrl
|
34
|
+
accessTokenClientSecret {
|
35
|
+
... VaultSecret
|
36
|
+
}
|
37
|
+
}
|
38
|
+
|
39
|
+
fragment VaultSecret on VaultSecret_v1 {
|
40
|
+
path
|
41
|
+
field
|
42
|
+
version
|
43
|
+
format
|
44
|
+
}
|
45
|
+
|
46
|
+
query RhIdpOrganizations($name: String) {
|
47
|
+
organizations: ocm_instances_v1(name: $name) {
|
48
|
+
name
|
49
|
+
environment {
|
50
|
+
...OCMEnvironment
|
51
|
+
}
|
52
|
+
orgId
|
53
|
+
disable {
|
54
|
+
...DisableAutomations
|
55
|
+
}
|
56
|
+
}
|
57
|
+
}
|
58
|
+
"""
|
59
|
+
|
60
|
+
|
61
|
+
class ConfiguredBaseModel(BaseModel):
|
62
|
+
class Config:
|
63
|
+
smart_union=True
|
64
|
+
extra=Extra.forbid
|
65
|
+
|
66
|
+
|
67
|
+
class OpenShiftClusterManagerV1(ConfiguredBaseModel):
|
68
|
+
name: str = Field(..., alias="name")
|
69
|
+
environment: OCMEnvironment = Field(..., alias="environment")
|
70
|
+
org_id: str = Field(..., alias="orgId")
|
71
|
+
disable: Optional[DisableAutomations] = Field(..., alias="disable")
|
72
|
+
|
73
|
+
|
74
|
+
class RhIdpOrganizationsQueryData(ConfiguredBaseModel):
|
75
|
+
organizations: Optional[list[OpenShiftClusterManagerV1]] = Field(..., alias="organizations")
|
76
|
+
|
77
|
+
|
78
|
+
def query(query_func: Callable, **kwargs: Any) -> RhIdpOrganizationsQueryData:
|
79
|
+
"""
|
80
|
+
This is a convenience function which queries and parses the data into
|
81
|
+
concrete types. It should be compatible with most GQL clients.
|
82
|
+
You do not have to use it to consume the generated data classes.
|
83
|
+
Alternatively, you can also mime and alternate the behavior
|
84
|
+
of this function in the caller.
|
85
|
+
|
86
|
+
Parameters:
|
87
|
+
query_func (Callable): Function which queries your GQL Server
|
88
|
+
kwargs: optional arguments that will be passed to the query function
|
89
|
+
|
90
|
+
Returns:
|
91
|
+
RhIdpOrganizationsQueryData: queried data parsed into generated classes
|
92
|
+
"""
|
93
|
+
raw_data: dict[Any, Any] = query_func(DEFINITION, **kwargs)
|
94
|
+
return RhIdpOrganizationsQueryData(**raw_data)
|
@@ -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
|
+
}
|
reconcile/rhidp/common.py
CHANGED
@@ -17,8 +17,15 @@ from reconcile.gql_definitions.common.ocm_environments import (
|
|
17
17
|
)
|
18
18
|
from reconcile.gql_definitions.fragments.ocm_environment import OCMEnvironment
|
19
19
|
from reconcile.gql_definitions.fragments.vault_secret import VaultSecret
|
20
|
+
from reconcile.gql_definitions.rhidp.organizations import (
|
21
|
+
OpenShiftClusterManagerV1,
|
22
|
+
)
|
23
|
+
from reconcile.gql_definitions.rhidp.organizations import (
|
24
|
+
query as ocm_orgs_query,
|
25
|
+
)
|
20
26
|
from reconcile.rhidp.metrics import RhIdpClusterCounter
|
21
27
|
from reconcile.utils import gql
|
28
|
+
from reconcile.utils.disabled_integrations import integration_is_enabled
|
22
29
|
from reconcile.utils.metrics import MetricsContainer
|
23
30
|
from reconcile.utils.ocm.base import OCMCluster
|
24
31
|
from reconcile.utils.ocm.clusters import (
|
@@ -192,3 +199,16 @@ def get_ocm_environments(env_name: str | None) -> list[OCMEnvironment]:
|
|
192
199
|
gql.get_api().query,
|
193
200
|
variables={"name": env_name} if env_name else None,
|
194
201
|
).environments
|
202
|
+
|
203
|
+
|
204
|
+
def get_ocm_orgs_from_env(
|
205
|
+
env_name: str, int_name: str
|
206
|
+
) -> list[OpenShiftClusterManagerV1]:
|
207
|
+
orgs = ocm_orgs_query(
|
208
|
+
gql.get_api().query,
|
209
|
+
).organizations
|
210
|
+
return [
|
211
|
+
org
|
212
|
+
for org in orgs or []
|
213
|
+
if integration_is_enabled(int_name, org) and org.environment.name == env_name
|
214
|
+
]
|
@@ -2,6 +2,7 @@ from reconcile.rhidp.common import (
|
|
2
2
|
build_cluster_objects,
|
3
3
|
discover_clusters,
|
4
4
|
get_ocm_environments,
|
5
|
+
get_ocm_orgs_from_env,
|
5
6
|
)
|
6
7
|
from reconcile.rhidp.ocm_oidc_idp.base import run
|
7
8
|
from reconcile.utils.ocm_base_client import init_ocm_base_client
|
@@ -16,7 +17,6 @@ QONTRACT_INTEGRATION = "ocm-oidc-idp"
|
|
16
17
|
class OCMOidcIdpParams(PydanticRunParams):
|
17
18
|
vault_input_path: str
|
18
19
|
ocm_environment: str | None = None
|
19
|
-
ocm_organization_ids: set[str] | None = None
|
20
20
|
default_auth_name: str
|
21
21
|
default_auth_issuer_url: str
|
22
22
|
|
@@ -33,7 +33,10 @@ class OCMOidcIdp(QontractReconcileIntegration[OCMOidcIdpParams]):
|
|
33
33
|
ocm_api = init_ocm_base_client(ocm_env, self.secret_reader)
|
34
34
|
# data query
|
35
35
|
cluster_details = discover_clusters(
|
36
|
-
ocm_api=ocm_api,
|
36
|
+
ocm_api=ocm_api,
|
37
|
+
org_ids={
|
38
|
+
org.org_id for org in get_ocm_orgs_from_env(ocm_env.name, self.name)
|
39
|
+
},
|
37
40
|
)
|
38
41
|
clusters = build_cluster_objects(
|
39
42
|
cluster_details=cluster_details,
|
@@ -2,6 +2,7 @@ from reconcile.rhidp.common import (
|
|
2
2
|
build_cluster_objects,
|
3
3
|
discover_clusters,
|
4
4
|
get_ocm_environments,
|
5
|
+
get_ocm_orgs_from_env,
|
5
6
|
)
|
6
7
|
from reconcile.rhidp.sso_client.base import run
|
7
8
|
from reconcile.utils.ocm_base_client import init_ocm_base_client
|
@@ -18,7 +19,6 @@ class SSOClientParams(PydanticRunParams):
|
|
18
19
|
keycloak_vault_paths: list[str]
|
19
20
|
vault_input_path: str
|
20
21
|
ocm_environment: str | None = None
|
21
|
-
ocm_organization_ids: set[str] | None = None
|
22
22
|
default_auth_name: str
|
23
23
|
default_auth_issuer_url: str
|
24
24
|
contacts: list[str]
|
@@ -36,7 +36,10 @@ class SSOClient(QontractReconcileIntegration[SSOClientParams]):
|
|
36
36
|
for ocm_env in get_ocm_environments(self.params.ocm_environment):
|
37
37
|
ocm_api = init_ocm_base_client(ocm_env, self.secret_reader)
|
38
38
|
cluster_details = discover_clusters(
|
39
|
-
ocm_api=ocm_api,
|
39
|
+
ocm_api=ocm_api,
|
40
|
+
org_ids={
|
41
|
+
org.org_id for org in get_ocm_orgs_from_env(ocm_env.name, self.name)
|
42
|
+
},
|
40
43
|
)
|
41
44
|
clusters = build_cluster_objects(
|
42
45
|
cluster_details=cluster_details,
|
@@ -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
|
File without changes
|
{qontract_reconcile-0.10.1rc465.dist-info → qontract_reconcile-0.10.1rc467.dist-info}/top_level.txt
RENAMED
File without changes
|