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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: qontract-reconcile
3
- Version: 0.10.1rc465
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
@@ -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=1GnZxqThlQe3QFlyNuniseg6mzJxc0RsvQccOqtYTpA,82291
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=Iul5-2_QgQ8joGfP542UQYA0Y5Qm5chvdRzUltCo_yM,1434
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=Er-zz8g6EZn6CThzopzBMsOKmUd2kAPo9__8WFLr4WA,2256
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=5opE7n9Xh2dPex6uFRI84BdyFWrus_WZb1_i22PXRYA,6221
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=PoP-Z7q2G2VZRbJqjQDOoQBNKWNOnKiaYlQHzG9cGBQ,1882
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=izxicGeUTG0wd9nWZ5eJp_rcUGi6mf_PMNGWWwA-oq8,2150
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=pQw4LKZL5d-Guaj4BMiIVrL6EZsmAMYII1-b8xYZ8Yk,5021
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.1rc465.dist-info/METADATA,sha256=EfvqZ0xkde3EPtjMgiV5RJj6CBJNKy9rAVDta4qZ31M,2348
650
- qontract_reconcile-0.10.1rc465.dist-info/WHEEL,sha256=oiQVh_5PnQM0E3gPdiz09WCNmwiHDMaGer_elqB3coM,92
651
- qontract_reconcile-0.10.1rc465.dist-info/entry_points.txt,sha256=rTjAv28I_CHLM8ID3OPqMI_suoQ9s7tFbim4aYjn9kk,376
652
- qontract_reconcile-0.10.1rc465.dist-info/top_level.txt,sha256=l5ISPoXzt0SdR4jVdkfa7RPSKNc8zAHYWAnR-Dw8Ey8,24
653
- qontract_reconcile-0.10.1rc465.dist-info/RECORD,,
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(reconcile.jira_permissions_validator, ctx.obj)
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)
@@ -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.utils import gql
13
- from reconcile.utils.jira_client import JiraClient
14
- from reconcile.utils.secret_reader import create_secret_reader
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
- def run(dry_run: bool) -> None:
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
- jira_boards = query_jira_boards(query_func=gql_api.query).jira_boards or []
25
- error = False
26
- for jira_board in jira_boards:
27
- token = secret_reader.read_secret(jira_board.server.token)
28
- jira = JiraClient.create(
29
- project_name=jira_board.name,
30
- token=token,
31
- server_url=jira_board.server.server_url,
32
- jira_watcher_settings=settings.jira_watcher,
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, org_ids=self.params.ocm_organization_ids
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, org_ids=self.params.ocm_organization_ids
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.")
@@ -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