qontract-reconcile 0.10.1rc584__py3-none-any.whl → 0.10.1rc586__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.1rc584
3
+ Version: 0.10.1rc586
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
@@ -59,7 +59,7 @@ reconcile/ocm_github_idp.py,sha256=glwXMsIBcl38-OmDDQCpe0YoLLXfoRgVQmqwXMEXjds,3
59
59
  reconcile/ocm_groups.py,sha256=AmQ61fjJYS5PxwNEWtOAvOoJM86VfRQ0-ic6wgw6PU0,2888
60
60
  reconcile/ocm_machine_pools.py,sha256=eebJ6iiTdUcuKE5zBcfNxW1OGmPOvgBtmVu3xNVOoyY,16608
61
61
  reconcile/ocm_update_recommended_version.py,sha256=IYkfLXIprOW1jguZeELcGP1iBPuj-b53R-FTqKulMl8,4204
62
- reconcile/ocm_upgrade_scheduler_org_updater.py,sha256=ta8hMJ-su5mRcPpYrvB1COsojXV-SU3PzLPbQhy2Q0I,4190
62
+ reconcile/ocm_upgrade_scheduler_org_updater.py,sha256=49Ss6sp9_n5F9914gXb-uEap4Vm2t-KPTJRFFViJMIo,4184
63
63
  reconcile/openshift_base.py,sha256=7aifvl-ay5wpY6encbUX9pGbKdjiwJmevZ3XWGRzpCM,49696
64
64
  reconcile/openshift_cluster_bots.py,sha256=eRPYZqWMKFNxLlSN0QG97V5t1iIESQ0BbGaiaQP5VB0,10940
65
65
  reconcile/openshift_clusterrolebindings.py,sha256=QfSy1Ik8eEY5XObc1Q4xyhqyErZenJmbPv_u9wcDNNo,5864
@@ -70,7 +70,7 @@ reconcile/openshift_namespaces.py,sha256=DboMc6t0vXD54lL9ZP9P9fQnCRo2g_0z5FWubtW
70
70
  reconcile/openshift_network_policies.py,sha256=_qqv7yj17OM1J8KJPsFmzFZ85gzESJeBocC672z4_WU,4231
71
71
  reconcile/openshift_resourcequotas.py,sha256=yUi56PiOn3inMMfq_x_FEHmaW-reGipzoorjdar372g,2415
72
72
  reconcile/openshift_resources.py,sha256=kwsY5cko7udEKNlhL2oKiKv_5wzEw9wmmwROE016ng8,1400
73
- reconcile/openshift_resources_base.py,sha256=1EAgMvGIpp_O4-PnJWgOI8UxphIsQ18kYaNRORdMf8s,46998
73
+ reconcile/openshift_resources_base.py,sha256=3BnifJYoq7hQvrXjtuBppg36uR_tjm1kF2Q80-Pcbac,39349
74
74
  reconcile/openshift_rolebindings.py,sha256=LlImloBisEqzc36jaatic-TeM3hzqMEfxogF-dM4Yhw,6599
75
75
  reconcile/openshift_routes.py,sha256=fXvuPSjcjVw1X3j2EQvUAdbOepmIFdKk-M3qP8QzPiw,1075
76
76
  reconcile/openshift_saas_deploy.py,sha256=fmhopPEbyZsGQHRPzyzpKEvoBXEGN3aPxFi7Utq0emU,12788
@@ -137,10 +137,10 @@ reconcile/aws_saml_idp/integration.py,sha256=uqec-EnxnfGOgQtg33S-Q1wTCv0sVBHNo02
137
137
  reconcile/aws_saml_roles/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
138
138
  reconcile/aws_saml_roles/integration.py,sha256=kgAUaCfKA2ujvIym8sPSWoBf1EnRKi2vVbgKyi8eELU,5695
139
139
  reconcile/aws_version_sync/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
140
- reconcile/aws_version_sync/integration.py,sha256=dN_lZIeM5i0p6-yHYadGg65QgDs0nVWYJkPzU-KGwuo,15196
140
+ reconcile/aws_version_sync/integration.py,sha256=0rgFLL2usTnngjIgFXIEfVXapFsDI1A493Mftqjfbk0,17292
141
141
  reconcile/aws_version_sync/utils.py,sha256=sVv-48PKi2VITlqqvmpbjnFDOPeGqfKzgkpIszlmjL0,1708
142
142
  reconcile/aws_version_sync/merge_request_manager/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
143
- reconcile/aws_version_sync/merge_request_manager/merge_request.py,sha256=FeNcQaory5AXNVuVk-jJxPwtI4uSoURgkTH3rXAb2cc,6198
143
+ reconcile/aws_version_sync/merge_request_manager/merge_request.py,sha256=lk8oiclpVcFbTCNSKHfDQ9H1sPha4e-39SumMItWUw4,6263
144
144
  reconcile/aws_version_sync/merge_request_manager/merge_request_manager.py,sha256=6gYFzpdJ-KcftPtOP3nhe3F9N184-UGfo_f8MQrzGxg,8322
145
145
  reconcile/change_owners/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
146
146
  reconcile/change_owners/approver.py,sha256=GV8nwS-YJOJ8O-b9v3u60RSYECYH2EKAycjpoW6VmvU,2228
@@ -346,7 +346,7 @@ reconcile/oum/models.py,sha256=0ZyCnULRxAbIEXX60BkkPZVg53DCD6ZJ6wnNT2ANROM,1743
346
346
  reconcile/oum/providers.py,sha256=3kEjXvsTPzXc7gzrdO7hWqgzcMmMZMpk2S0X7wQUTWU,1767
347
347
  reconcile/oum/standalone.py,sha256=bzyV8wz3SrERG9zJRFiJCBzSIGwDNj9sNqUytngDw94,7368
348
348
  reconcile/prometheus_rules_tester/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
349
- reconcile/prometheus_rules_tester/integration.py,sha256=OBEVXqixTjnzi36VpPFR3rEEIDtPcSY0bopxd3M1vz8,9161
349
+ reconcile/prometheus_rules_tester/integration.py,sha256=hCJJ0udrvgMM78_gFUGvZWOH8azI2tABQKpQq6lHhY0,9232
350
350
  reconcile/rhidp/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
351
351
  reconcile/rhidp/common.py,sha256=suTh9T4dPOgrKi-rDALgjzCSL9JHGnkTYALFIIjNCJE,6801
352
352
  reconcile/rhidp/metrics.py,sha256=Yp0GtpjhieEdru0qkG3osBTJiKUzg6CAjwPoFTQDnCg,417
@@ -427,13 +427,13 @@ reconcile/test/test_ocm_clusters.py,sha256=NOxhJnlgOlRZF0-sfXCiRP0aiNiBiMl35MTCC
427
427
  reconcile/test/test_ocm_clusters_manifest_updates.py,sha256=jFRVfc5jby1kI2x_gT6wcqPPgkav1et9wZH6JqQbNSY,3278
428
428
  reconcile/test/test_ocm_machine_pools.py,sha256=3qo6t2Jfr1Wee0NUacyLTDmatp0o7CUNpkVOpHiOiGk,29737
429
429
  reconcile/test/test_ocm_update_recommended_version.py,sha256=iA4BVirTGVXlwcOyeR52IuNO81X_8NR6ZNd7ZFE7igs,4328
430
- reconcile/test/test_ocm_upgrade_scheduler_org_updater.py,sha256=zYRGUX7pAmxSv9oFYw2ZnPGa-YAPgDfmqXOJM4eE-8A,4353
430
+ reconcile/test/test_ocm_upgrade_scheduler_org_updater.py,sha256=hT6sbdGUx8LGnMVvNI7wOVHcoan1YurckytlvtJdzGk,4347
431
431
  reconcile/test/test_openshift_base.py,sha256=uVsnMghAQhHaJTreeOw4x2INTKJ6qeiZiiteWeKflW8,33874
432
432
  reconcile/test/test_openshift_cluster_bots.py,sha256=L-yuKvMgB0LBCdfLu7wozh_lk6S_m3umXt3m_ECfLEI,8023
433
433
  reconcile/test/test_openshift_namespace_labels.py,sha256=P1hqi6P88NijNrurdXG_QR2usyo3EYZSy9zpwYHvDsM,12104
434
434
  reconcile/test/test_openshift_namespaces.py,sha256=HmRnCE5EnFt3MYceVEFHmk8wWRtCrxu2AFGFkY9pdyA,9214
435
435
  reconcile/test/test_openshift_resource.py,sha256=lbTf48jX1q6rGnRiA5pPvfU0uPfY8zhNylMtryn0sLI,12995
436
- reconcile/test/test_openshift_resources_base.py,sha256=4UucdsD0nCMFT1WmgNXf4r7ZZ11cJ_MP13IcK7_Vs0g,15042
436
+ reconcile/test/test_openshift_resources_base.py,sha256=LtlR9x3o7KkSEw0JN0fZhinFeAAxBAQlB_9PpBnKwOM,14353
437
437
  reconcile/test/test_openshift_saas_deploy.py,sha256=YLJGkc--u5aP0UkQ-b9ZGEFGS2gw25jjcSgknQdI3Ic,5892
438
438
  reconcile/test/test_openshift_saas_deploy_change_tester.py,sha256=1yVe54Hx9YdVjn6qdnKge5Sa_s732c-8uZqCnuT1gGI,12871
439
439
  reconcile/test/test_openshift_tekton_resources.py,sha256=RtRWsdm51S13OSkENC9nY_rOH0QELSCaO5tjF0XqIDI,11222
@@ -465,6 +465,7 @@ reconcile/test/test_terraform_vpc_peerings.py,sha256=ubcsKh0TrUIwuI1-W3ETIgzsFvz
465
465
  reconcile/test/test_terraform_vpc_peerings_build_desired_state.py,sha256=DAfpb12I0PlqnuVUHK2vh4LH4d1OylT3H2GE_3TGZZI,47852
466
466
  reconcile/test/test_three_way_diff_strategy.py,sha256=2fjEqE2w4pIzKq18PRcADTSe01aGwsZfMGloU8xfNaE,3346
467
467
  reconcile/test/test_unleash.py,sha256=c1s_FRAZrAzzd3FbZrzHYjJzHELhoxPHBZnEzqsfMQg,6416
468
+ reconcile/test/test_utils_jinja2.py,sha256=TpzQlpFnLGzNEZp5WOh0o7AuBiGEktqO4MuwiiJW2YY,3895
468
469
  reconcile/test/test_vault_replication.py,sha256=wlc4jm9f8P641UvvxIFFFc5_unJysNkOVrKJscjhQr0,16867
469
470
  reconcile/test/test_vault_utils.py,sha256=vbJnc89XAuE07qbTuWxHM5o9F6R9SO5aHXA38fwxT7A,1122
470
471
  reconcile/test/test_version_bump.py,sha256=q6-3Y1roriI6YWpFwaHOMN7emEP3yL33sh_0VdbmG7E,511
@@ -549,7 +550,7 @@ reconcile/utils/environ.py,sha256=VnW3zp6Un_UJn5BU4FU8RfhuqtZp0s-VeuuHnqC_WcQ,51
549
550
  reconcile/utils/exceptions.py,sha256=DwfnWUpVOotpP79RWZ2pycmG6nKCL00RBIeZLYkQPW4,635
550
551
  reconcile/utils/expiration.py,sha256=BXwKE50sNIV-Lszke97fxitNkLxYszoOLW1LBgp_yqg,1246
551
552
  reconcile/utils/extended_early_exit.py,sha256=2qvw9W2PW0t4JCZFD8wD3BINWIy02lkvpOrd2YDaYHE,6195
552
- reconcile/utils/external_resource_spec.py,sha256=OGPKH3IKXgJszRTgE5U_QKgU-s4BHQnx97Lj-Krz46k,6655
553
+ reconcile/utils/external_resource_spec.py,sha256=IRY8MCsyWKzt-Qj_hXiFKgkCvZu6VVyj6IYtqEb05BA,6618
553
554
  reconcile/utils/external_resources.py,sha256=a2CkJ3KLociYBnc_9F2VWfZGWMhzDl6fDNhwo2U-MWU,7501
554
555
  reconcile/utils/filtering.py,sha256=zZnHH0u0SaTDyzuFXZ_mREURGLvjEqQIQy4z-7QBVlc,419
555
556
  reconcile/utils/git.py,sha256=Qad7mfPuS9s7eKODeWSewehwSGgJPCbQuLda1qg_6GA,1522
@@ -564,7 +565,6 @@ reconcile/utils/helpers.py,sha256=k9svgFFZG7H5FvHYY0g5jJyvgvh2UDZxf0Ib221teag,11
564
565
  reconcile/utils/imap_client.py,sha256=byFAJATbITJPsGECSbvXBOcCnoeTUpDFiEjzOAxLm_U,1975
565
566
  reconcile/utils/instrumented_wrappers.py,sha256=eVwMoa6FCrYxLv3RML3WpZF9qKVfCTjMxphgVXG03OM,1073
566
567
  reconcile/utils/jenkins_api.py,sha256=MyJSB_S3uYf3sXnt9t03-gZNQ7tbdd7Wusv3MoF2fRc,7113
567
- reconcile/utils/jinja2_ext.py,sha256=l628RR9r9dAGBWLVegoCbSqnjojeizNGiq9Cstt02nE,1129
568
568
  reconcile/utils/jira_client.py,sha256=f7u3xvI4tk27LOnlZxxG1OjELC5vC6wzlyk3Fo7tDnY,7007
569
569
  reconcile/utils/jjb_client.py,sha256=Pdy0dLCFvD6GPCaC0tZydYgkVJPOxYXIiwWECZaFJBU,14551
570
570
  reconcile/utils/jsonpath.py,sha256=NRpAEijKN4cMDjo7qivNPqpm0__GQQ1TiE0PBEBO45s,5572
@@ -601,7 +601,7 @@ reconcile/utils/state.py,sha256=SAa6QLHu9lr0yqLCBy2AypNx1IPCJWlrRBrvlzAKsOU,1450
601
601
  reconcile/utils/structs.py,sha256=LcbLEg8WxfRqM6nW7NhcWN0YeqF7SQzxOgntmLs1SgY,352
602
602
  reconcile/utils/template.py,sha256=wTvRU4AnAV_o042tD4Mwls2dwWMuk7MKnde3MaCjaYg,331
603
603
  reconcile/utils/terraform_client.py,sha256=_jBriLBwU005bDxWlq7CRByOkVCfiH47oBzB0ArNAY8,31901
604
- reconcile/utils/terrascript_aws_client.py,sha256=ZV5FK0E1vdUnjsaMXnfIoE7WHU0VocKFzHL_32IDGcE,269694
604
+ reconcile/utils/terrascript_aws_client.py,sha256=IQxaDYujdfD-AVH-_RtqklDV-RGRZSjWAZFIkP_6NFI,269716
605
605
  reconcile/utils/three_way_diff_strategy.py,sha256=nyqeQsLCoPI6e16k2CF3b9KNgQLU-rPf5RtfdUfVMwE,4468
606
606
  reconcile/utils/throughput.py,sha256=iP4UWAe2LVhDo69mPPmgo9nQ7RxHD6_GS8MZe-aSiuM,344
607
607
  reconcile/utils/unleash.py,sha256=1D56CsZfE3ShDtN3IErE1T2eeIwNmxhK-yYbCotJ99E,3601
@@ -620,6 +620,10 @@ reconcile/utils/glitchtip/models.py,sha256=_oqZXNkyRTsAnx6tF4WUURSBj0cc9UNS4okOQ
620
620
  reconcile/utils/internal_groups/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
621
621
  reconcile/utils/internal_groups/client.py,sha256=abREA8RwXKybXFjCK8CAcCr-iUp2r0tAbIEJ-c-PXws,4538
622
622
  reconcile/utils/internal_groups/models.py,sha256=jlkH_hyyyuwS0J1IpuS7W1AyQSKQ2QpHelXoH36edbE,2316
623
+ reconcile/utils/jinja2/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
624
+ reconcile/utils/jinja2/extensions.py,sha256=zV_x8MhSHAynKhFnG3fULXrwsm5fUG_88IygZHSnN0o,1284
625
+ reconcile/utils/jinja2/filters.py,sha256=_kJjdMsY3lGS5PUn4NnpXUQDNrL1IwiKsB-0MhTMGYM,4521
626
+ reconcile/utils/jinja2/utils.py,sha256=NW2BLizE-SGudgtdKqlo02CZlNda1W-swVp6uldKtUs,5779
623
627
  reconcile/utils/membershipsources/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
624
628
  reconcile/utils/membershipsources/app_interface_resolver.py,sha256=IlDiRtJZ0AfAGKEawybB6SvsKbm1POTXL6fpEt699E0,1979
625
629
  reconcile/utils/membershipsources/models.py,sha256=IFu6KHFe-HUTJPiAO3fEw7i22yv4_ytgBW-h_wrO6V4,2015
@@ -660,7 +664,7 @@ reconcile/utils/runtime/sharding.py,sha256=roCdbnBklhTK_g34zbgQYqzpKPaNQ8J6Xd9XL
660
664
  reconcile/utils/saasherder/__init__.py,sha256=J3MBZBFa5YmhqYm08QsjBXz8mFcVOCiOCkyIcw41t7E,343
661
665
  reconcile/utils/saasherder/interfaces.py,sha256=XXY35h8VWQ66z3LBPxaoUAMkIW50264DQiecrzyV6oA,9076
662
666
  reconcile/utils/saasherder/models.py,sha256=PBv8DuAb6KUw_ayn5Ufiya20cCAelBv6Iv--x7hbpa4,5449
663
- reconcile/utils/saasherder/saasherder.py,sha256=vZ0S_7cT2FP1al2zOwLNuOedx8M7MIvokXiCsdm-5W4,85739
667
+ reconcile/utils/saasherder/saasherder.py,sha256=72b0u-cIHg62R2uQCqlGcQfW5TbCxWDKf0dfmgVMUbY,85733
664
668
  reconcile/utils/terraform/__init__.py,sha256=zNbiyTWo35AT1sFTElL2j_AA0jJ_yWE_bfFn-nD2xik,250
665
669
  reconcile/utils/terraform/config.py,sha256=5UVrd563TMcvi4ooa5JvWVDW1I3bIWg484u79evfV_8,164
666
670
  reconcile/utils/terraform/config_client.py,sha256=py-Ree-QUYD6Hvng6bM40VgSuttteehIKNgwOSoJO1o,4706
@@ -688,8 +692,8 @@ tools/test/test_app_interface_metrics_exporter.py,sha256=SX7qL3D1SIRKFo95FoQztvf
688
692
  tools/test/test_qontract_cli.py,sha256=EzJwaPxQw5CLPahLjh91oRT0pi1WCgOXZ_KgiNXZMAw,2948
689
693
  tools/test/test_sd_app_sre_alert_report.py,sha256=v363r9zM7__0kR5K6mvJoGFcM9BvE33fWAayrqkpojA,2116
690
694
  tools/test/test_sre_checkpoints.py,sha256=SKqPPTl9ua0RFdSSofnoQX-JZE6dFLO3LRhfQzqtfh8,2607
691
- qontract_reconcile-0.10.1rc584.dist-info/METADATA,sha256=v-BDoqNXZ9Fyh_WtSl7fukxxlwwGYE32f4hkG5CT_ag,2349
692
- qontract_reconcile-0.10.1rc584.dist-info/WHEEL,sha256=oiQVh_5PnQM0E3gPdiz09WCNmwiHDMaGer_elqB3coM,92
693
- qontract_reconcile-0.10.1rc584.dist-info/entry_points.txt,sha256=rTjAv28I_CHLM8ID3OPqMI_suoQ9s7tFbim4aYjn9kk,376
694
- qontract_reconcile-0.10.1rc584.dist-info/top_level.txt,sha256=l5ISPoXzt0SdR4jVdkfa7RPSKNc8zAHYWAnR-Dw8Ey8,24
695
- qontract_reconcile-0.10.1rc584.dist-info/RECORD,,
695
+ qontract_reconcile-0.10.1rc586.dist-info/METADATA,sha256=i0GRb6YD41fyHubkOLfO3pHuPskCUvJP2_T3dpYlHlk,2349
696
+ qontract_reconcile-0.10.1rc586.dist-info/WHEEL,sha256=oiQVh_5PnQM0E3gPdiz09WCNmwiHDMaGer_elqB3coM,92
697
+ qontract_reconcile-0.10.1rc586.dist-info/entry_points.txt,sha256=rTjAv28I_CHLM8ID3OPqMI_suoQ9s7tFbim4aYjn9kk,376
698
+ qontract_reconcile-0.10.1rc586.dist-info/top_level.txt,sha256=l5ISPoXzt0SdR4jVdkfa7RPSKNc8zAHYWAnR-Dw8Ey8,24
699
+ qontract_reconcile-0.10.1rc586.dist-info/RECORD,,
@@ -3,11 +3,13 @@ from collections.abc import (
3
3
  Callable,
4
4
  Iterable,
5
5
  )
6
+ from enum import Enum
6
7
  from typing import Any
7
8
 
8
9
  import semver
9
10
  from pydantic import (
10
11
  BaseModel,
12
+ root_validator,
11
13
  validator,
12
14
  )
13
15
 
@@ -66,6 +68,12 @@ class ExternalResourceProvisioner(BaseModel):
66
68
  path: str | None = None
67
69
 
68
70
 
71
+ class VersionFormat(str, Enum):
72
+ MAJOR = "major"
73
+ MAJOR_MINOR = "major_minor"
74
+ MAJOR_MINOR_PATCH = "major_minor_patch"
75
+
76
+
69
77
  class ExternalResource(BaseModel):
70
78
  namespace_file: str | None = None
71
79
  provider: str = "aws"
@@ -74,6 +82,8 @@ class ExternalResource(BaseModel):
74
82
  resource_identifier: str
75
83
  resource_engine: str
76
84
  resource_engine_version: semver.VersionInfo
85
+ # if None, it'll be set via root_validator
86
+ resource_engine_version_format: VersionFormat | None = None
77
87
  # used to map AWS cache name to resource_identifier
78
88
  redis_replication_group_id: str | None = None
79
89
 
@@ -98,6 +108,46 @@ class ExternalResource(BaseModel):
98
108
  return v
99
109
  return parse_semver(v, optional_minor_and_patch=True)
100
110
 
111
+ @root_validator(pre=True)
112
+ def set_resource_engine_version_format(cls, values: dict) -> dict:
113
+ resource_engine_version, resource_engine_version_format = (
114
+ values.get("resource_engine_version"),
115
+ values.get("resource_engine_version_format"),
116
+ )
117
+ if not resource_engine_version:
118
+ # make mypy happy
119
+ raise ValueError("resource_engine_version is required")
120
+
121
+ if resource_engine_version_format is None:
122
+ match resource_engine_version.count("."):
123
+ case 0:
124
+ values["resource_engine_version_format"] = VersionFormat.MAJOR
125
+ case 1:
126
+ values["resource_engine_version_format"] = VersionFormat.MAJOR_MINOR
127
+ case 2:
128
+ values["resource_engine_version_format"] = (
129
+ VersionFormat.MAJOR_MINOR_PATCH
130
+ )
131
+ case _:
132
+ raise ValueError(
133
+ f"Invalid version format: {resource_engine_version}"
134
+ )
135
+ return values
136
+
137
+ @property
138
+ def resource_engine_version_string(self) -> str:
139
+ match self.resource_engine_version_format:
140
+ case VersionFormat.MAJOR:
141
+ return f"{self.resource_engine_version.major}"
142
+ case VersionFormat.MAJOR_MINOR:
143
+ return f"{self.resource_engine_version.major}.{self.resource_engine_version.minor}"
144
+ case VersionFormat.MAJOR_MINOR_PATCH:
145
+ return f"{self.resource_engine_version.major}.{self.resource_engine_version.minor}.{self.resource_engine_version.patch}"
146
+ case _:
147
+ raise ValueError(
148
+ f"Invalid version format: {self.resource_engine_version_format}"
149
+ )
150
+
101
151
 
102
152
  AwsExternalResources = list[ExternalResource]
103
153
  AppInterfaceExternalResources = list[ExternalResource]
@@ -319,7 +369,7 @@ class AVSIntegration(QontractReconcileIntegration[AVSIntegrationParams]):
319
369
  resource_provider=app_interface_resource.resource_provider,
320
370
  resource_identifier=app_interface_resource.resource_identifier,
321
371
  resource_engine=app_interface_resource.resource_engine,
322
- resource_engine_version=str(aws_resource.resource_engine_version),
372
+ resource_engine_version=aws_resource.resource_engine_version_string,
323
373
  )
324
374
 
325
375
  @defer
@@ -162,7 +162,8 @@ class Renderer:
162
162
  resource_identifier,
163
163
  )
164
164
  overrides = resource.setdefault("overrides", {})
165
- overrides["engine_version"] = resource_engine_version
165
+ # ensure that the engine version is always a string
166
+ overrides["engine_version"] = f"{resource_engine_version}"
166
167
  with StringIO() as stream:
167
168
  yml.dump(content, stream)
168
169
  return stream.getvalue()
@@ -12,7 +12,7 @@ from reconcile import (
12
12
  mr_client_gateway,
13
13
  queries,
14
14
  )
15
- from reconcile.openshift_resources_base import process_jinja2_template
15
+ from reconcile.utils.jinja2.utils import process_jinja2_template
16
16
  from reconcile.utils.ocm import (
17
17
  OCMMap,
18
18
  OCMSpec,
@@ -13,7 +13,6 @@ from collections.abc import (
13
13
  )
14
14
  from contextlib import contextmanager
15
15
  from dataclasses import dataclass
16
- from functools import cache
17
16
  from textwrap import indent
18
17
  from threading import Lock
19
18
  from typing import (
@@ -22,14 +21,10 @@ from typing import (
22
21
  Protocol,
23
22
  Tuple,
24
23
  )
25
- from urllib import parse
26
24
 
27
25
  import anymarkup
28
- import jinja2
29
- import jinja2.sandbox
30
26
  from deepdiff import DeepHash
31
27
  from sretoolbox.utils import (
32
- retry,
33
28
  threaded,
34
29
  )
35
30
 
@@ -37,7 +32,6 @@ import reconcile.openshift_base as ob
37
32
  from reconcile import queries
38
33
  from reconcile.change_owners.diff import IDENTIFIER_FIELD_NAME
39
34
  from reconcile.checkpoint import url_makes_sense
40
- from reconcile.github_users import init_github
41
35
  from reconcile.utils import (
42
36
  amtool,
43
37
  gql,
@@ -45,9 +39,12 @@ from reconcile.utils import (
45
39
  )
46
40
  from reconcile.utils.defer import defer
47
41
  from reconcile.utils.exceptions import FetchResourceError
48
- from reconcile.utils.jinja2_ext import (
49
- B64EncodeExtension,
50
- RaiseErrorExtension,
42
+ from reconcile.utils.jinja2.utils import (
43
+ FetchSecretError,
44
+ lookup_github_file_content,
45
+ lookup_secret,
46
+ process_extracurlyjinja2_template,
47
+ process_jinja2_template,
51
48
  )
52
49
  from reconcile.utils.oc import (
53
50
  OC_Map,
@@ -64,7 +61,7 @@ from reconcile.utils.openshift_resource import (
64
61
  )
65
62
  from reconcile.utils.openshift_resource import OpenshiftResource as OR
66
63
  from reconcile.utils.runtime.integration import DesiredStateShardConfig
67
- from reconcile.utils.secret_reader import SecretNotFound, SecretReader
64
+ from reconcile.utils.secret_reader import SecretReader
68
65
  from reconcile.utils.semver_helper import make_semver
69
66
  from reconcile.utils.sharding import is_in_shard
70
67
  from reconcile.utils.vault import (
@@ -254,21 +251,11 @@ def _locked_error_log(msg: str):
254
251
  logging.error(msg)
255
252
 
256
253
 
257
- class FetchSecretError(Exception):
258
- def __init__(self, msg):
259
- super().__init__("error fetching secret: " + str(msg))
260
-
261
-
262
254
  class FetchRouteError(Exception):
263
255
  def __init__(self, msg):
264
256
  super().__init__("error fetching route: " + str(msg))
265
257
 
266
258
 
267
- class Jinja2TemplateError(Exception):
268
- def __init__(self, msg):
269
- super().__init__("error processing jinja2 template: " + str(msg))
270
-
271
-
272
259
  class ResourceTemplateRenderError(Exception):
273
260
  pass
274
261
 
@@ -287,230 +274,6 @@ class UnknownTemplateTypeError(Exception):
287
274
  super().__init__("unknown template type error: " + str(msg))
288
275
 
289
276
 
290
- @retry()
291
- def lookup_secret(
292
- path,
293
- key,
294
- version=None,
295
- tvars=None,
296
- allow_not_found=False,
297
- settings=None,
298
- secret_reader=None,
299
- ):
300
- if tvars is not None:
301
- path = process_jinja2_template(
302
- body=path, vars=tvars, settings=settings, secret_reader=secret_reader
303
- )
304
- key = process_jinja2_template(
305
- body=key, vars=tvars, settings=settings, secret_reader=secret_reader
306
- )
307
- if version and not isinstance(version, int):
308
- version = process_jinja2_template(
309
- body=version, vars=tvars, settings=settings, secret_reader=secret_reader
310
- )
311
- secret = {"path": path, "field": key, "version": version}
312
- try:
313
- if not secret_reader:
314
- secret_reader = SecretReader(settings)
315
- return secret_reader.read(secret)
316
- except SecretNotFound as e:
317
- if allow_not_found:
318
- return None
319
- raise FetchSecretError(e)
320
- except Exception as e:
321
- raise FetchSecretError(e)
322
-
323
-
324
- def lookup_github_file_content(
325
- repo, path, ref, tvars=None, settings=None, secret_reader=None
326
- ):
327
- if tvars is not None:
328
- repo = process_jinja2_template(
329
- body=repo, vars=tvars, settings=settings, secret_reader=secret_reader
330
- )
331
- path = process_jinja2_template(
332
- body=path, vars=tvars, settings=settings, secret_reader=secret_reader
333
- )
334
- ref = process_jinja2_template(
335
- body=ref, vars=tvars, settings=settings, secret_reader=secret_reader
336
- )
337
-
338
- gh = init_github()
339
- c = gh.get_repo(repo).get_contents(path, ref).decoded_content
340
- return c.decode("utf-8")
341
-
342
-
343
- def lookup_graphql_query_results(query: str, **kwargs) -> list[Any]:
344
- gqlapi = gql.get_api()
345
- resource = gqlapi.get_resource(query)["content"]
346
- rendered_resource = jinja2.Template(resource).render(**kwargs)
347
- results = list(gqlapi.query(rendered_resource).values())[0]
348
- return results
349
-
350
-
351
- def hash_list(input: Iterable) -> str:
352
- """
353
- Deterministic hash of a list for jinja2 templates.
354
- The order of the list doesn't matter as it is sorted
355
- before hashing. Note, that the list elements
356
- must be flat primitives (no dicts/lists).
357
- """
358
- lst = list(input)
359
- str_lst = []
360
- for el in lst:
361
- if isinstance(el, (list, dict)):
362
- raise RuntimeError(
363
- f"jinja2 hash_list function received non-primitive value {el}. All values received {lst}"
364
- )
365
- str_lst.append(str(el))
366
- msg = "a" # keep non-empty for hashing empty list
367
- msg += "".join(sorted(str_lst))
368
- m = hashlib.sha256()
369
- m.update(msg.encode("utf-8"))
370
- return m.hexdigest()
371
-
372
-
373
- def eval_filter(input, **kwargs) -> str:
374
- """Jinja2 filter be used when the string
375
- is in itself a jinja2 template that must be
376
- evaluated with kwargs. For example in the case
377
- of the slo-document expression fields.
378
- :param input: template string
379
- :kwargs: variables that will be used to evaluate the
380
- input string
381
- :return: rendered string
382
- """
383
- return jinja2.Template(input).render(**kwargs)
384
-
385
-
386
- def json_to_dict(input):
387
- """Jinja2 filter to parse JSON strings into dictionaries.
388
- This becomes useful to access Graphql queries data (labels)
389
- :param input: json string
390
- :return: dict with the parsed inputs contents
391
- """
392
- data = json.loads(input)
393
- return data
394
-
395
-
396
- def urlescape(
397
- string: str,
398
- safe: str = "/",
399
- encoding: Optional[str] = None,
400
- ) -> str:
401
- """Jinja2 filter that is a simple wrapper around urllib's URL quoting
402
- functions that takes a string value and makes it safe for use as URL
403
- components escaping any reserved characters using URL encoding. See:
404
- urllib.parse.quote() and urllib.parse.quote_plus() for reference.
405
-
406
- :param str string: String value to escape.
407
- :param str safe: Optional characters that should not be escaped.
408
- :param encoding: Encoding to apply to the string to be escaped. Defaults
409
- to UTF-8. Unsupported characters raise a UnicodeEncodeError error.
410
- :type encoding: typing.Optional[str]
411
- :returns: A string with reserved characters escaped.
412
- :rtype: str
413
- """
414
- return parse.quote(string, safe=safe, encoding=encoding)
415
-
416
-
417
- def urlunescape(string: str, encoding: Optional[str] = None) -> str:
418
- """Jinja2 filter that is a simple wrapper around urllib's URL unquoting
419
- functions that takes an URL-encoded string value and unescapes it
420
- replacing any URL-encoded values with their character equivalent. See:
421
- urllib.parse.unquote() and urllib.parse.unquote_plus() for reference.
422
-
423
- :param str string: String value to unescape.
424
- :param encoding: Encoding to apply to the string to be unescaped. Defaults
425
- to UTF-8. Unsupported characters are replaced by placeholder values.
426
- :type encoding: typing.Optional[str]
427
- :returns: A string with URL-encoded sequences unescaped.
428
- :rtype: str
429
- """
430
- if encoding is None:
431
- encoding = "utf-8"
432
- return parse.unquote(string, encoding=encoding)
433
-
434
-
435
- @cache
436
- def compile_jinja2_template(body, extra_curly: bool = False):
437
- env: dict = {}
438
- if extra_curly:
439
- env = {
440
- "block_start_string": "{{%",
441
- "block_end_string": "%}}",
442
- "variable_start_string": "{{{",
443
- "variable_end_string": "}}}",
444
- "comment_start_string": "{{#",
445
- "comment_end_string": "#}}",
446
- }
447
-
448
- jinja_env = jinja2.sandbox.SandboxedEnvironment(
449
- extensions=[B64EncodeExtension, RaiseErrorExtension],
450
- undefined=jinja2.StrictUndefined,
451
- **env,
452
- )
453
- jinja_env.filters.update({
454
- "json_to_dict": json_to_dict,
455
- "urlescape": urlescape,
456
- "urlunescape": urlunescape,
457
- "eval": eval_filter,
458
- })
459
-
460
- return jinja_env.from_string(body)
461
-
462
-
463
- def process_jinja2_template(
464
- body, vars=None, extra_curly: bool = False, settings=None, secret_reader=None
465
- ):
466
- if vars is None:
467
- vars = {}
468
- vars.update({
469
- "vault": lambda p, k, v=None, allow_not_found=False: lookup_secret(
470
- path=p,
471
- key=k,
472
- version=v,
473
- tvars=vars,
474
- allow_not_found=allow_not_found,
475
- settings=settings,
476
- secret_reader=secret_reader,
477
- ),
478
- "github": lambda u, p, r, v=None: lookup_github_file_content(
479
- repo=u,
480
- path=p,
481
- ref=r,
482
- tvars=vars,
483
- settings=settings,
484
- secret_reader=secret_reader,
485
- ),
486
- "urlescape": lambda u, s="/", e=None: urlescape(string=u, safe=s, encoding=e),
487
- "urlunescape": lambda u, e=None: urlunescape(string=u, encoding=e),
488
- "hash_list": hash_list,
489
- "query": lookup_graphql_query_results,
490
- "url": url_makes_sense,
491
- })
492
- try:
493
- template = compile_jinja2_template(body, extra_curly)
494
- r = template.render(vars)
495
- except Exception as e:
496
- raise Jinja2TemplateError(e)
497
- return r
498
-
499
-
500
- def process_extracurlyjinja2_template(
501
- body, vars=None, env=None, settings=None, secret_reader=None
502
- ):
503
- if vars is None:
504
- vars = {}
505
- return process_jinja2_template(
506
- body,
507
- vars=vars,
508
- extra_curly=True,
509
- settings=settings,
510
- secret_reader=secret_reader,
511
- )
512
-
513
-
514
277
  def check_alertmanager_config(data, path, alertmanager_config_key, decode_base64=False):
515
278
  try:
516
279
  config = data[alertmanager_config_key]
@@ -29,6 +29,7 @@ from reconcile.utils import (
29
29
  gql,
30
30
  promtool,
31
31
  )
32
+ from reconcile.utils.jinja2.utils import process_extracurlyjinja2_template
32
33
  from reconcile.utils.runtime.integration import DesiredStateShardConfig
33
34
  from reconcile.utils.semver_helper import make_semver
34
35
  from reconcile.utils.structs import CommandExecutionResult
@@ -91,7 +92,7 @@ def fetch_rule_and_tests(
91
92
  test_raw_yaml = gql.get_resource(test_path)["content"]
92
93
 
93
94
  if rule.resource["type"] == "resource-template-extracurlyjinja2":
94
- test_raw_yaml = orb.process_extracurlyjinja2_template(
95
+ test_raw_yaml = process_extracurlyjinja2_template(
95
96
  body=test_raw_yaml,
96
97
  vars=variables,
97
98
  settings=vault_settings.dict(by_alias=True),
@@ -3,7 +3,7 @@ import json
3
3
  import pytest
4
4
 
5
5
  from reconcile.ocm_upgrade_scheduler_org_updater import render_policy
6
- from reconcile.openshift_resources_base import Jinja2TemplateError
6
+ from reconcile.utils.jinja2.utils import Jinja2TemplateError
7
7
  from reconcile.utils.ocm import (
8
8
  OCMClusterNetwork,
9
9
  OCMSpec,
@@ -13,7 +13,6 @@ from reconcile.openshift_base import CurrentStateSpec
13
13
  from reconcile.openshift_resources_base import (
14
14
  CheckClusterScopedResourceDuplicates,
15
15
  canonicalize_namespaces,
16
- hash_list,
17
16
  ob,
18
17
  )
19
18
  from reconcile.test.fixtures import Fixtures
@@ -441,36 +440,6 @@ def test_check_error():
441
440
  print(e)
442
441
 
443
442
 
444
- def test_hash_list_empty():
445
- assert hash_list([])[:6] == "ca9781"
446
-
447
-
448
- def test_hash_list_string():
449
- assert hash_list(["a", "b"])[:6] == "38760e"
450
- assert hash_list(["b", "a"])[:6] == "38760e"
451
-
452
-
453
- def test_hash_list_int():
454
- assert hash_list([1, 2])[:6] == "f37508"
455
- assert hash_list([2, 1])[:6] == "f37508"
456
-
457
-
458
- def test_hash_list_bool():
459
- assert hash_list([True, False])[:6] == "e0ca28"
460
- assert hash_list([False, True])[:6] == "e0ca28"
461
-
462
-
463
- def test_hash_list_error():
464
- with pytest.raises(RuntimeError):
465
- hash_list([{}])
466
-
467
- with pytest.raises(RuntimeError):
468
- hash_list([[]])
469
-
470
- with pytest.raises(RuntimeError):
471
- hash_list(["a", {}])
472
-
473
-
474
443
  def test_cluster_params():
475
444
  with pytest.raises(RuntimeError):
476
445
  orb.run(dry_run=False, exclude_cluster=["test-cluster"])
@@ -0,0 +1,123 @@
1
+ import pytest
2
+ from jsonpath_ng.exceptions import JsonPathParserError
3
+
4
+ from reconcile.utils.jinja2.filters import (
5
+ extract_jsonpath,
6
+ hash_list,
7
+ json_pointers,
8
+ matches_jsonpath,
9
+ )
10
+
11
+
12
+ def test_hash_list_empty() -> None:
13
+ assert hash_list([])[:6] == "ca9781"
14
+
15
+
16
+ def test_hash_list_string() -> None:
17
+ assert hash_list(["a", "b"])[:6] == "38760e"
18
+ assert hash_list(["b", "a"])[:6] == "38760e"
19
+
20
+
21
+ def test_hash_list_int() -> None:
22
+ assert hash_list([1, 2])[:6] == "f37508"
23
+ assert hash_list([2, 1])[:6] == "f37508"
24
+
25
+
26
+ def test_hash_list_bool() -> None:
27
+ assert hash_list([True, False])[:6] == "e0ca28"
28
+ assert hash_list([False, True])[:6] == "e0ca28"
29
+
30
+
31
+ def test_hash_list_error() -> None:
32
+ with pytest.raises(RuntimeError):
33
+ hash_list([{}])
34
+
35
+ with pytest.raises(RuntimeError):
36
+ hash_list([[]])
37
+
38
+ with pytest.raises(RuntimeError):
39
+ hash_list(["a", {}])
40
+
41
+
42
+ def test_extract_jsonpath_dict_basic() -> None:
43
+ input = {"a": "A", "b": {"b1": "B1", "b2": ["B", 2]}}
44
+ assert extract_jsonpath(input, "a") == ["A"]
45
+ assert extract_jsonpath(input, "b.b1") == ["B1"]
46
+ assert extract_jsonpath(input, "b.b2") == [["B", 2]]
47
+ assert extract_jsonpath(input, "b.b2[0]") == ["B"]
48
+ assert extract_jsonpath(input, "c") == []
49
+
50
+
51
+ def test_extract_jsonpath_dict_multiple() -> None:
52
+ input = {
53
+ "items": [
54
+ {"name": "a", "value": "A"},
55
+ {"name": "b", "value": "B1"},
56
+ {"name": "b", "value": "B2"},
57
+ ]
58
+ }
59
+ assert extract_jsonpath(input, "items[0]") == [{"name": "a", "value": "A"}]
60
+ assert extract_jsonpath(input, "items[?(@.name=='a')]") == [
61
+ {"name": "a", "value": "A"}
62
+ ]
63
+ assert extract_jsonpath(input, "items[?(@.name=='a')].value") == ["A"]
64
+ assert extract_jsonpath(input, "items[?(@.name=='b')]") == [
65
+ {"name": "b", "value": "B1"},
66
+ {"name": "b", "value": "B2"},
67
+ ]
68
+ assert extract_jsonpath(input, "items[?(@.name=='b')].value") == ["B1", "B2"]
69
+ assert extract_jsonpath(input, "items[?(@.name=='c')].value") == []
70
+
71
+
72
+ def test_extract_jsonpath_list() -> None:
73
+ input = ["a", "b"]
74
+ assert extract_jsonpath(input, "[0]") == ["a"]
75
+
76
+
77
+ def test_extract_jsonpath_str() -> None:
78
+ assert extract_jsonpath("a", "[0]") == ["a"]
79
+ assert extract_jsonpath("a", "something") == []
80
+
81
+
82
+ def test_extract_jsonpath_none() -> None:
83
+ assert extract_jsonpath(None, "a") == []
84
+
85
+
86
+ def test_extract_jsonpath_errors() -> None:
87
+ input = {"a": "A", "b": "B"}
88
+ with pytest.raises(JsonPathParserError):
89
+ extract_jsonpath(input, "THIS IS AN INVALID JSONPATH]")
90
+ with pytest.raises(AssertionError):
91
+ extract_jsonpath("a", "")
92
+
93
+
94
+ def test_matches_jsonpath() -> None:
95
+ input = {"a": "A", "b": "B"}
96
+ assert matches_jsonpath(input, "a")
97
+ assert not matches_jsonpath(input, "c")
98
+ with pytest.raises(JsonPathParserError):
99
+ matches_jsonpath(input, "THIS IS AN INVALID JSONPATH]")
100
+ with pytest.raises(AssertionError):
101
+ matches_jsonpath("a", "")
102
+ with pytest.raises(AssertionError):
103
+ matches_jsonpath("a", None)
104
+
105
+
106
+ def test_json_pointers() -> None:
107
+ input = {
108
+ "items": [
109
+ {"name": "a", "value": "A"},
110
+ {"name": "b", "value": "B1"},
111
+ {"name": "b", "value": "B2"},
112
+ ]
113
+ }
114
+ assert json_pointers(input, "items") == ["/items"]
115
+ assert json_pointers(input, "items[*]") == ["/items/0", "/items/1", "/items/2"]
116
+ assert json_pointers(input, "items[0]") == ["/items/0"]
117
+ assert json_pointers(input, "items[0].name") == ["/items/0/name"]
118
+ assert json_pointers(input, "items[4]") == []
119
+ assert json_pointers(input, "items[4].name") == []
120
+
121
+ assert json_pointers(input, "items[?@.name=='a']") == ["/items/0"]
122
+ assert json_pointers(input, "items[?@.name=='b']") == ["/items/1", "/items/2"]
123
+ assert json_pointers(input, "items[?@.name=='c']") == []
@@ -15,7 +15,7 @@ import yaml
15
15
  from pydantic import BaseModel
16
16
  from pydantic.dataclasses import dataclass
17
17
 
18
- from reconcile import openshift_resources_base
18
+ from reconcile.utils.jinja2.utils import process_jinja2_template
19
19
  from reconcile.utils.metrics import GaugeMetric
20
20
  from reconcile.utils.openshift_resource import (
21
21
  SECRET_MAX_KEY_LENGTH,
@@ -58,9 +58,7 @@ class GenericSecretOutputFormatConfig(OutputFormatProcessor):
58
58
  if self.data:
59
59
  # the jinja2 rendering has the capabilitiy to change the passed
60
60
  # vars dict - make a copy to protect against it
61
- rendered_data = openshift_resources_base.process_jinja2_template(
62
- self.data, dict(vars)
63
- )
61
+ rendered_data = process_jinja2_template(self.data, dict(vars))
64
62
  parsed_data = yaml.safe_load(rendered_data)
65
63
  self.validate_k8s_secret_data(parsed_data)
66
64
  return cast(dict[str, str], parsed_data)
File without changes
@@ -1,15 +1,17 @@
1
1
  import base64
2
2
  import textwrap
3
+ from typing import Callable
3
4
 
4
5
  from jinja2 import nodes
5
6
  from jinja2.exceptions import TemplateRuntimeError
6
7
  from jinja2.ext import Extension
8
+ from jinja2.parser import Parser
7
9
 
8
10
 
9
11
  class B64EncodeExtension(Extension):
10
12
  tags = {"b64encode"}
11
13
 
12
- def parse(self, parser):
14
+ def parse(self, parser: Parser) -> nodes.CallBlock:
13
15
  lineno = next(parser.stream).lineno
14
16
 
15
17
  body = parser.parse_statements(["name:endb64encode"], drop_needle=True)
@@ -19,7 +21,7 @@ class B64EncodeExtension(Extension):
19
21
  ).set_lineno(lineno)
20
22
 
21
23
  @staticmethod
22
- def _b64encode(caller):
24
+ def _b64encode(caller: Callable) -> str:
23
25
  content = caller()
24
26
  content = textwrap.dedent(content)
25
27
  return base64.b64encode(content.encode()).decode("utf-8")
@@ -28,7 +30,7 @@ class B64EncodeExtension(Extension):
28
30
  class RaiseErrorExtension(Extension):
29
31
  tags = {"raise_error"}
30
32
 
31
- def parse(self, parser):
33
+ def parse(self, parser: Parser) -> nodes.CallBlock:
32
34
  lineno = next(parser.stream).lineno
33
35
 
34
36
  msg = parser.parse_expression()
@@ -42,5 +44,5 @@ class RaiseErrorExtension(Extension):
42
44
  )
43
45
 
44
46
  @staticmethod
45
- def _raise_error(msg, caller):
47
+ def _raise_error(msg: str, caller: Callable) -> None:
46
48
  raise TemplateRuntimeError(msg)
@@ -0,0 +1,128 @@
1
+ import hashlib
2
+ import json
3
+ import re
4
+ from collections.abc import Iterable
5
+ from typing import Any, Optional
6
+ from urllib import parse
7
+
8
+ import jinja2
9
+
10
+ from reconcile.utils.jsonpath import parse_jsonpath
11
+
12
+
13
+ def json_to_dict(input: str) -> Any:
14
+ """Jinja2 filter to parse JSON strings into dictionaries.
15
+ This becomes useful to access Graphql queries data (labels)
16
+ :param input: json string
17
+ :return: dict with the parsed inputs contents
18
+ """
19
+ data = json.loads(input)
20
+ return data
21
+
22
+
23
+ def urlescape(string: str, safe: str = "/", encoding: Optional[str] = None) -> str:
24
+ """Jinja2 filter that is a simple wrapper around urllib's URL quoting
25
+ functions that takes a string value and makes it safe for use as URL
26
+ components escaping any reserved characters using URL encoding. See:
27
+ urllib.parse.quote() and urllib.parse.quote_plus() for reference.
28
+
29
+ :param str string: String value to escape.
30
+ :param str safe: Optional characters that should not be escaped.
31
+ :param encoding: Encoding to apply to the string to be escaped. Defaults
32
+ to UTF-8. Unsupported characters raise a UnicodeEncodeError error.
33
+ :type encoding: typing.Optional[str]
34
+ :returns: A string with reserved characters escaped.
35
+ :rtype: str
36
+ """
37
+ return parse.quote(string, safe=safe, encoding=encoding)
38
+
39
+
40
+ def urlunescape(string: str, encoding: Optional[str] = None) -> str:
41
+ """Jinja2 filter that is a simple wrapper around urllib's URL unquoting
42
+ functions that takes an URL-encoded string value and unescapes it
43
+ replacing any URL-encoded values with their character equivalent. See:
44
+ urllib.parse.unquote() and urllib.parse.unquote_plus() for reference.
45
+
46
+ :param str string: String value to unescape.
47
+ :param encoding: Encoding to apply to the string to be unescaped. Defaults
48
+ to UTF-8. Unsupported characters are replaced by placeholder values.
49
+ :type encoding: typing.Optional[str]
50
+ :returns: A string with URL-encoded sequences unescaped.
51
+ :rtype: str
52
+ """
53
+ if encoding is None:
54
+ encoding = "utf-8"
55
+ return parse.unquote(string, encoding=encoding)
56
+
57
+
58
+ def eval_filter(input: str, **kwargs: dict[str, Any]) -> str:
59
+ """Jinja2 filter be used when the string
60
+ is in itself a jinja2 template that must be
61
+ evaluated with kwargs. For example in the case
62
+ of the slo-document expression fields.
63
+ :param input: template string
64
+ :kwargs: variables that will be used to evaluate the
65
+ input string
66
+ :return: rendered string
67
+ """
68
+ return jinja2.Template(input).render(**kwargs)
69
+
70
+
71
+ def hash_list(input: Iterable) -> str:
72
+ """
73
+ Deterministic hash of a list for jinja2 templates.
74
+ The order of the list doesn't matter as it is sorted
75
+ before hashing. Note, that the list elements
76
+ must be flat primitives (no dicts/lists).
77
+ """
78
+ lst = list(input)
79
+ str_lst = []
80
+ for el in lst:
81
+ if isinstance(el, (list, dict)):
82
+ raise RuntimeError(
83
+ f"jinja2 hash_list function received non-primitive value {el}. All values received {lst}"
84
+ )
85
+ str_lst.append(str(el))
86
+ msg = "a" # keep non-empty for hashing empty list
87
+ msg += "".join(sorted(str_lst))
88
+ m = hashlib.sha256()
89
+ m.update(msg.encode("utf-8"))
90
+ return m.hexdigest()
91
+
92
+
93
+ def _find_jsonpath(input: Any, jsonpath: str | None) -> Any:
94
+ assert jsonpath is not None and len(jsonpath) > 0
95
+ return parse_jsonpath(jsonpath).find(input)
96
+
97
+
98
+ def extract_jsonpath(input: Any, jsonpath: str) -> Any:
99
+ """
100
+ Extracts data from the input using jsonpath.
101
+ The result is a list of matching elements.
102
+ The list will be empty if nothing matches.
103
+ """
104
+ return [i.value for i in _find_jsonpath(input, jsonpath)]
105
+
106
+
107
+ def matches_jsonpath(input: Any, jsonpath: str | None) -> bool:
108
+ """
109
+ Returns True if the input matches the provided jsonpath
110
+ """
111
+ return len(_find_jsonpath(input, jsonpath)) > 0
112
+
113
+
114
+ def _convert_pointer(pointer: str) -> str:
115
+ """
116
+ Converts a jsonpath_ng pointer (eg "items.[2].type.[3]")
117
+ to a rfc6901 one (https://www.rfc-editor.org/rfc/rfc6901)
118
+ "/items/2/type/3"
119
+ """
120
+ elems = [e[1:-1] if re.match(r"\[\d\]", e) else e for e in pointer.split(".")]
121
+ return "/" + "/".join(elems)
122
+
123
+
124
+ def json_pointers(input: Any, jsonpath: str) -> list[str]:
125
+ """
126
+ Finds the RFC6901 JSON pointers of the input elements matching the given jsonpath
127
+ """
128
+ return [_convert_pointer(str(i.full_path)) for i in _find_jsonpath(input, jsonpath)]
@@ -0,0 +1,188 @@
1
+ from functools import cache
2
+ from typing import Any, Optional
3
+
4
+ import jinja2
5
+ from jinja2.sandbox import SandboxedEnvironment
6
+ from sretoolbox.utils import retry
7
+
8
+ from reconcile.checkpoint import url_makes_sense
9
+ from reconcile.github_users import init_github
10
+ from reconcile.utils import gql
11
+ from reconcile.utils.jinja2.extensions import B64EncodeExtension, RaiseErrorExtension
12
+ from reconcile.utils.jinja2.filters import (
13
+ eval_filter,
14
+ extract_jsonpath,
15
+ hash_list,
16
+ json_pointers,
17
+ json_to_dict,
18
+ matches_jsonpath,
19
+ urlescape,
20
+ urlunescape,
21
+ )
22
+ from reconcile.utils.secret_reader import SecretNotFound, SecretReader, SecretReaderBase
23
+
24
+
25
+ class Jinja2TemplateError(Exception):
26
+ def __init__(self, msg: Any):
27
+ super().__init__("error processing jinja2 template: " + str(msg))
28
+
29
+
30
+ @cache
31
+ def compile_jinja2_template(body: str, extra_curly: bool = False) -> Any:
32
+ env: dict = {}
33
+ if extra_curly:
34
+ env = {
35
+ "block_start_string": "{{%",
36
+ "block_end_string": "%}}",
37
+ "variable_start_string": "{{{",
38
+ "variable_end_string": "}}}",
39
+ "comment_start_string": "{{#",
40
+ "comment_end_string": "#}}",
41
+ }
42
+
43
+ jinja_env = SandboxedEnvironment(
44
+ extensions=[B64EncodeExtension, RaiseErrorExtension],
45
+ undefined=jinja2.StrictUndefined,
46
+ **env,
47
+ )
48
+ jinja_env.filters.update({
49
+ "json_to_dict": json_to_dict,
50
+ "urlescape": urlescape,
51
+ "urlunescape": urlunescape,
52
+ "eval": eval_filter,
53
+ "extract_jsonpath": extract_jsonpath,
54
+ "matches_jsonpath": matches_jsonpath,
55
+ "json_pointers": json_pointers,
56
+ })
57
+
58
+ return jinja_env.from_string(body)
59
+
60
+
61
+ def lookup_github_file_content(
62
+ repo: str,
63
+ path: str,
64
+ ref: str,
65
+ tvars: Optional[dict[str, Any]] = None,
66
+ settings: Optional[dict[str, Any]] = None,
67
+ secret_reader: Optional[SecretReaderBase] = None,
68
+ ) -> str:
69
+ if tvars is not None:
70
+ repo = process_jinja2_template(
71
+ body=repo, vars=tvars, settings=settings, secret_reader=secret_reader
72
+ )
73
+ path = process_jinja2_template(
74
+ body=path, vars=tvars, settings=settings, secret_reader=secret_reader
75
+ )
76
+ ref = process_jinja2_template(
77
+ body=ref, vars=tvars, settings=settings, secret_reader=secret_reader
78
+ )
79
+
80
+ gh = init_github()
81
+ c = gh.get_repo(repo).get_contents(path, ref).decoded_content
82
+ return c.decode("utf-8")
83
+
84
+
85
+ def lookup_graphql_query_results(query: str, **kwargs: dict[str, Any]) -> list[Any]:
86
+ gqlapi = gql.get_api()
87
+ resource = gqlapi.get_resource(query)["content"]
88
+ rendered_resource = jinja2.Template(resource).render(**kwargs)
89
+ results = list(gqlapi.query(rendered_resource).values())[0]
90
+ return results
91
+
92
+
93
+ @retry()
94
+ def lookup_secret(
95
+ path: str,
96
+ key: str,
97
+ version: Optional[str] = None,
98
+ tvars: Optional[dict[str, Any]] = None,
99
+ allow_not_found: bool = False,
100
+ settings: Optional[dict[str, Any]] = None,
101
+ secret_reader: Optional[SecretReaderBase] = None,
102
+ ) -> Optional[str]:
103
+ if tvars is not None:
104
+ path = process_jinja2_template(
105
+ body=path, vars=tvars, settings=settings, secret_reader=secret_reader
106
+ )
107
+ key = process_jinja2_template(
108
+ body=key, vars=tvars, settings=settings, secret_reader=secret_reader
109
+ )
110
+ if version and not isinstance(version, int):
111
+ version = process_jinja2_template(
112
+ body=version, vars=tvars, settings=settings, secret_reader=secret_reader
113
+ )
114
+ secret = {"path": path, "field": key, "version": version}
115
+ try:
116
+ if not secret_reader:
117
+ secret_reader = SecretReader(settings)
118
+ return secret_reader.read(secret)
119
+ except SecretNotFound as e:
120
+ if allow_not_found:
121
+ return None
122
+ raise FetchSecretError(e)
123
+ except Exception as e:
124
+ raise FetchSecretError(e)
125
+
126
+
127
+ def process_jinja2_template(
128
+ body: str,
129
+ vars: Optional[dict[str, Any]] = None,
130
+ extra_curly: bool = False,
131
+ settings: Optional[dict[str, Any]] = None,
132
+ secret_reader: Optional[SecretReaderBase] = None,
133
+ ) -> Any:
134
+ if vars is None:
135
+ vars = {}
136
+ vars.update({
137
+ "vault": lambda p, k, v=None: lookup_secret(
138
+ path=p,
139
+ key=k,
140
+ version=v,
141
+ tvars=vars,
142
+ allow_not_found=False,
143
+ settings=settings,
144
+ secret_reader=secret_reader,
145
+ ),
146
+ "github": lambda u, p, r, v=None: lookup_github_file_content(
147
+ repo=u,
148
+ path=p,
149
+ ref=r,
150
+ tvars=vars,
151
+ settings=settings,
152
+ secret_reader=secret_reader,
153
+ ),
154
+ "urlescape": lambda u, s="/", e=None: urlescape(string=u, safe=s, encoding=e),
155
+ "urlunescape": lambda u, e=None: urlunescape(string=u, encoding=e),
156
+ "hash_list": hash_list,
157
+ "query": lookup_graphql_query_results,
158
+ "url": url_makes_sense,
159
+ })
160
+ try:
161
+ template = compile_jinja2_template(body, extra_curly)
162
+ r = template.render(vars)
163
+ except Exception as e:
164
+ raise Jinja2TemplateError(e)
165
+ return r
166
+
167
+
168
+ def process_extracurlyjinja2_template(
169
+ body: str,
170
+ vars: Optional[dict[str, Any]] = None,
171
+ extra_curly: bool = True,
172
+ settings: Optional[dict[str, Any]] = None,
173
+ secret_reader: Optional[SecretReaderBase] = None,
174
+ ) -> Any:
175
+ if vars is None:
176
+ vars = {}
177
+ return process_jinja2_template(
178
+ body,
179
+ vars=vars,
180
+ extra_curly=True,
181
+ settings=settings,
182
+ secret_reader=secret_reader,
183
+ )
184
+
185
+
186
+ class FetchSecretError(Exception):
187
+ def __init__(self, msg: Any):
188
+ super().__init__("error fetching secret: " + str(msg))
@@ -2031,7 +2031,7 @@ class SaasHerder: # pylint: disable=too-many-public-methods
2031
2031
  @staticmethod
2032
2032
  def resolve_templated_parameters(saas_files: Iterable[SaasFile]) -> None:
2033
2033
  """Resolve templated target parameters in saas files."""
2034
- from reconcile.openshift_resources_base import ( # noqa: PLC0415 - # avoid circular import
2034
+ from reconcile.utils.jinja2.utils import ( # noqa: PLC0415 - # avoid circular import
2035
2035
  compile_jinja2_template,
2036
2036
  )
2037
2037
 
@@ -140,7 +140,6 @@ from terrascript.resource import (
140
140
  random_id,
141
141
  )
142
142
 
143
- import reconcile.openshift_resources_base as orb
144
143
  import reconcile.utils.aws_helper as awsh
145
144
  from reconcile import queries
146
145
  from reconcile.github_org import get_default_config
@@ -176,6 +175,7 @@ from reconcile.utils.external_resources import (
176
175
  from reconcile.utils.git import is_file_in_git_repo
177
176
  from reconcile.utils.gitlab_api import GitLabApi
178
177
  from reconcile.utils.jenkins_api import JenkinsApi
178
+ from reconcile.utils.jinja2.utils import process_extracurlyjinja2_template
179
179
  from reconcile.utils.ocm import OCMMap
180
180
  from reconcile.utils.password_validator import (
181
181
  PasswordPolicy,
@@ -5342,7 +5342,7 @@ class TerrascriptClient: # pylint: disable=too-many-public-methods
5342
5342
  part = []
5343
5343
  for c in cloudinit_configs:
5344
5344
  raw = self.get_raw_values(c["content"])
5345
- content = orb.process_extracurlyjinja2_template(
5345
+ content = process_extracurlyjinja2_template(
5346
5346
  body=raw["content"], vars=vars, secret_reader=self.secret_reader
5347
5347
  )
5348
5348
  # https://www.terraform.io/docs/language/expressions/strings.html#escape-sequences