qontract-reconcile 0.10.1rc441__py3-none-any.whl → 0.10.1rc449__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.1rc441
3
+ Version: 0.10.1rc449
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=i6bSWnlH9fh14P14PjVhFLwNl-q3fD733_rXKM_O51c,2992
9
9
  reconcile/blackbox_exporter_endpoint_monitoring.py,sha256=W_VJagnsJR1v5oqjlI3RJJE0_nhtJ0m81RS8zWA5u5c,3538
10
10
  reconcile/checkpoint.py,sha256=figtZRuWUvdpdSnkhAqeGvO5dI02TT6J3heyeFhlwqM,5016
11
- reconcile/cli.py,sha256=DZ5QV5jNNNt_5GXLywtVlZ-grWNnpMa6rd7sYlU2ZT8,82379
11
+ reconcile/cli.py,sha256=NTYh8aT6jEhe6rBLeJKJvYyKESGU7N8WyflGCUJr-Uk,82140
12
12
  reconcile/closedbox_endpoint_monitoring_base.py,sha256=GmEdDSp9yBnwpzzrla6VJfhOZd_qxYh-xtIN5bXjOBo,4909
13
13
  reconcile/cluster_deployment_mapper.py,sha256=2Ah-nu-Mdig0pjuiZl_XLrmVAjYzFjORR3dMlCgkmw0,2352
14
14
  reconcile/dashdotdb_base.py,sha256=NkieQiK1bOlXxmFNHIQ-MOZNPfY1sE1T04yndq0o3gA,4811
@@ -16,7 +16,7 @@ reconcile/dashdotdb_cso.py,sha256=FoXrWGpOwXG5jf0eklN84tjJVUAYzKat7rtq_28JMlQ,36
16
16
  reconcile/dashdotdb_dora.py,sha256=FINH-8dU3_r8EgUOMiF_fbxD9fKs6LFDxe6rufQ9XcM,17214
17
17
  reconcile/dashdotdb_dvo.py,sha256=YXqpI6fBQAql-ybGI0grj9gWMzmKiAvPE__pNju6obk,8996
18
18
  reconcile/dashdotdb_slo.py,sha256=bf1WSh5JP9obHVQsMy0OO71_VTYZgwAopElFZM6DmRo,6714
19
- reconcile/database_access_manager.py,sha256=1FWsBA7hgp2Wxo-khLfEyamkdi70u_0DvMHmkatqTBI,25486
19
+ reconcile/database_access_manager.py,sha256=AoeCrDIxC60sLct31M3gjbjry6DWjItjn9Y_3tA11s4,25586
20
20
  reconcile/dynatrace_token_provider.py,sha256=HWItJ_LavPcUJlpYz5fmfnfOpP2-0Qjkar0EAzBJ5Cw,16602
21
21
  reconcile/email_sender.py,sha256=-5L-Ag_jaEYSzYRoMr52KQBRXz1E8yx9GqLbg2X4XFU,3533
22
22
  reconcile/gabi_authorized_users.py,sha256=rCosZv8Iu9jhWG88YiwK-gftX475aJ1R-PYIJYp_svY,4342
@@ -56,7 +56,7 @@ reconcile/ocm_clusters.py,sha256=ibK0o0rXhAnq4TF522aPkVlH7i0bdtgD8-IKmZ8PHjs,134
56
56
  reconcile/ocm_external_configuration_labels.py,sha256=imEpDv1RBpCSj8tHDv0R76hmNCFtcUzVNgS1yOVl8vs,3870
57
57
  reconcile/ocm_github_idp.py,sha256=IlSpoUlUhYOahqWee6azJiN-N5EZNxNAWh-lrPXTfEc,3946
58
58
  reconcile/ocm_groups.py,sha256=_kiMUndKc6as6cbbvXxVnq8V_Lj7X5lxMJVCuRWuUFE,2888
59
- reconcile/ocm_machine_pools.py,sha256=YGIokK6cWhakPB4pj3h4F6w9Rn12kqsXf4TT4mdj1w8,15218
59
+ reconcile/ocm_machine_pools.py,sha256=eebJ6iiTdUcuKE5zBcfNxW1OGmPOvgBtmVu3xNVOoyY,16608
60
60
  reconcile/ocm_update_recommended_version.py,sha256=IYkfLXIprOW1jguZeELcGP1iBPuj-b53R-FTqKulMl8,4204
61
61
  reconcile/ocm_upgrade_scheduler_org_updater.py,sha256=Kti53htd5yeTSXzMW6jKvQojfaRIN7rMGe5-cC4k7N4,4105
62
62
  reconcile/openshift_base.py,sha256=PFV3XoshiayqSHjOMtW7cOB2jhFLrPN8wq9km1SiWio,47332
@@ -108,7 +108,7 @@ reconcile/terraform_aws_route53.py,sha256=QJMlaYYwcw_N9TJu7VbgZfxrxxR8Wv62GuG05X
108
108
  reconcile/terraform_cloudflare_dns.py,sha256=auU4bzeLwd4S8D8oqpqJbrCUoEdELXrgi7vHOedjYFk,13332
109
109
  reconcile/terraform_cloudflare_resources.py,sha256=EbQQaoDnZ7brvRCpbFtwlD7KLk2hDVNcjhrJGaAywEk,15023
110
110
  reconcile/terraform_cloudflare_users.py,sha256=DfeSnYC9YQgXX6AbJh85tQbJUDv1e2FjiGXgcpVQlPg,13964
111
- reconcile/terraform_repo.py,sha256=inYQKMuCL4gk7WmmDGohpPhlJj-28axbzMqxdiPLBJ4,14948
111
+ reconcile/terraform_repo.py,sha256=gjNJ76Yx5wFE6EbIgyfXRwLXjIrlz3FnvBptdjGABXE,15988
112
112
  reconcile/terraform_resources.py,sha256=WUM0ZEpf2jgcXjfjy3QSwrzpuD6zMN7cyOxetx88f9g,17167
113
113
  reconcile/terraform_tgw_attachments.py,sha256=6PVhBYeIvNnz2PwBdPF0I222_83EzOWX6VKJnqz7UPo,13915
114
114
  reconcile/terraform_users.py,sha256=kXRUxCUchKCP2dbXXOzctynqMii4oyCP6bYZHQTrlTg,10202
@@ -118,11 +118,11 @@ reconcile/vpc_peerings_validator.py,sha256=oiYwmQ2yYBobFhIixmHNUP1GxzUADocMPJnCB
118
118
  reconcile/aus/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
119
119
  reconcile/aus/advanced_upgrade_service.py,sha256=BxQU2O037MXopmTfcAaBDus4RJI4acXe05WqTKuPk_s,21098
120
120
  reconcile/aus/aus_label_source.py,sha256=X6FD4NYcX27llMUSmmBcCh-pG7U5FnBd0zl-0zwCj2U,4118
121
- reconcile/aus/base.py,sha256=MCR0QAtBifRut6N4g72ognryPYuRp1cQ2YBz_4jKnG4,43486
121
+ reconcile/aus/base.py,sha256=h_4MJbMHALy2TeaGs-d5KVTksu7EOGCBFljeFUERQbk,43635
122
122
  reconcile/aus/cluster_version_data.py,sha256=j4UyEBi5mQuvPq5Lo7a_L_0blxvH790wJV07uAiikFU,7126
123
- reconcile/aus/metrics.py,sha256=CI5H3kzWh5VUYjt8NfzcYdJ8L4HdTCcUQTZiLdwdLFc,3506
123
+ reconcile/aus/metrics.py,sha256=lUNIjCCyAvU9f4aTx4i5RkQrjKjKD37i4AE4LlTriHs,3544
124
124
  reconcile/aus/models.py,sha256=oBSVZ-3JTngxKg_bH1vAfREpz55t8K-Y3eC9TA4pOTw,6849
125
- reconcile/aus/ocm_addons_upgrade_scheduler_org.py,sha256=4f4AOoEUBxYdTIUnyev1ECPvbfVHSESQhXl7fuU-iGk,8811
125
+ reconcile/aus/ocm_addons_upgrade_scheduler_org.py,sha256=fshslI27hrqT40qrVsVOQaWxD-jkmzcVZryXzX7plhY,8960
126
126
  reconcile/aus/ocm_upgrade_scheduler.py,sha256=7cK2SakCFkl5EdnqUEAYdUo4pUnnf-SsUR10uytAGyE,3058
127
127
  reconcile/aus/ocm_upgrade_scheduler_org.py,sha256=OBgE5mnVdQQV4tMH0AE2V_PDt9Gy6d-LyuPceqjORts,2331
128
128
  reconcile/aus/upgrades.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -131,7 +131,7 @@ reconcile/aws_ami_cleanup/integration.py,sha256=IW95cpMj2P5ffs-AxsR_TDQCJnYFBhLI
131
131
  reconcile/aws_cloudwatch_log_retention/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
132
132
  reconcile/aws_cloudwatch_log_retention/integration.py,sha256=0UcSZIrGvnGY4m9fj87oejIolIP_qTxtJInpmW9jrQ0,7772
133
133
  reconcile/aws_version_sync/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
134
- reconcile/aws_version_sync/integration.py,sha256=sddkW_XcHF8KMeqE23Mxl3yLrpUkqzv_ArP-EVXcwIU,11432
134
+ reconcile/aws_version_sync/integration.py,sha256=fCDtfI3Lpz1l5l6AwC9tTnoiMeY7yvFLR6l7twixFlE,12248
135
135
  reconcile/aws_version_sync/utils.py,sha256=sVv-48PKi2VITlqqvmpbjnFDOPeGqfKzgkpIszlmjL0,1708
136
136
  reconcile/aws_version_sync/merge_request_manager/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
137
137
  reconcile/aws_version_sync/merge_request_manager/merge_request.py,sha256=FeNcQaory5AXNVuVk-jJxPwtI4uSoURgkTH3rXAb2cc,6198
@@ -376,7 +376,7 @@ reconcile/test/test_checkpoint.py,sha256=sbDtqTbfw5yMZ_mCltMXxkyyGueVLGUjTDtcWhP
376
376
  reconcile/test/test_cli.py,sha256=qx_iBwh4Z-YkK3sbjK1wEziPTgn060EN-baf9DNvR3k,1096
377
377
  reconcile/test/test_closedbox_endpoint_monitoring.py,sha256=isMHYwRWMFARU2nbJgbl69kD6H0eA86noCM4MPVI1fo,7151
378
378
  reconcile/test/test_dashdotdb_dora.py,sha256=XDMdnLDvup8sSqQEynxdPhXQZAafYbd2IUo9flSRJ8I,7917
379
- reconcile/test/test_database_access_manager.py,sha256=wc2Ov9GX8iAc7_ubwPJxsLZQUYt0KpaRzEIF6gfnsqM,19497
379
+ reconcile/test/test_database_access_manager.py,sha256=9yGFmsDt5ar6fy1WvKxBovbFETX5kmkUMOykMe2fEDU,19594
380
380
  reconcile/test/test_gabi_authorized_users.py,sha256=6XnV5Q9inxP81ktGMVKyWucjBTUj8Imy2L0HG3YHyUE,2496
381
381
  reconcile/test/test_github_org.py,sha256=j3KeB4OnSln1gm2hidce49xdMru-j75NS3cM-AEgzZc,4511
382
382
  reconcile/test/test_github_repo_invites.py,sha256=QJ0VFk5B59rx4XtHoT6XOGWw9xRIZMen_cgtviN_Vi8,3419
@@ -392,7 +392,7 @@ reconcile/test/test_make.py,sha256=zTdjgq-3idFlec_0qJenk9wWw0QMLvSpJfPsptXmync,6
392
392
  reconcile/test/test_ocm_additional_routers.py,sha256=dtbpUnD5un6Q3VoLbuFRb_njmt5SSCnBzvSSBcO_Xxs,4248
393
393
  reconcile/test/test_ocm_clusters.py,sha256=MLH6r5G-T3e53tYDoyJYfC6X1oEZGmakk1SXR1w3jJw,22490
394
394
  reconcile/test/test_ocm_clusters_manifest_updates.py,sha256=jFRVfc5jby1kI2x_gT6wcqPPgkav1et9wZH6JqQbNSY,3278
395
- reconcile/test/test_ocm_machine_pools.py,sha256=4z-U3-vGVvYZPdvxP0d1HHfHoD5yXvo7Ek4N2Wyofvo,23547
395
+ reconcile/test/test_ocm_machine_pools.py,sha256=uPtu3XG1xmRiEIZ3Uw_RTsZIgRkm_n6EGURWXqTyGyU,29789
396
396
  reconcile/test/test_ocm_update_recommended_version.py,sha256=ONY3slwUHwEdonvIYRI2Z8nWPDMTSSLGZ2I1dqaz7fg,4328
397
397
  reconcile/test/test_ocm_upgrade_scheduler_org_updater.py,sha256=zYRGUX7pAmxSv9oFYw2ZnPGa-YAPgDfmqXOJM4eE-8A,4353
398
398
  reconcile/test/test_openshift_base.py,sha256=owJsyyatl_7z6igUEDeJcMMV5_jIwcV2Yj7YgsmYkXs,27035
@@ -423,7 +423,7 @@ reconcile/test/test_terraform_aws_route53.py,sha256=xHggb8K1P76OyCfFcogbkmyKle-N
423
423
  reconcile/test/test_terraform_cloudflare_dns.py,sha256=aQTXX8Vr4h9aWvJZTnpZEhMGYoBpT2d45ZxU_ECIQ6o,3425
424
424
  reconcile/test/test_terraform_cloudflare_resources.py,sha256=D-3yvANpQMW_hmxTthsUvRTuyguqCqLJoLaDX1THDks,9351
425
425
  reconcile/test/test_terraform_cloudflare_users.py,sha256=RAFtMMdqZha3jNnNNsqbNQQUDSqUzdoM63rCw7fs4Fo,27456
426
- reconcile/test/test_terraform_repo.py,sha256=kzOc9r12yHcW0DgKWp-jbILPb-4dTkWgYM8IkzVp6pQ,11262
426
+ reconcile/test/test_terraform_repo.py,sha256=gN2fUVVIgqYR4PPLNyVzHi_gUudAkgJ2RNkVVtkcVhA,11668
427
427
  reconcile/test/test_terraform_resources.py,sha256=1ny_QSFuRjV9jxZY8EeT4NVJ5dMv7cLrEEIx_cBpjgk,9075
428
428
  reconcile/test/test_terraform_tgw_attachments.py,sha256=GgDA8hlQ1ujh5g8PtzbYQbJGpNScEgZ8PvDbMFbn68g,35493
429
429
  reconcile/test/test_terraform_users.py,sha256=Xn4y6EcxnNQb6XcPoOhz_Ikxmh9Nrsu88OM1scN9hzY,5434
@@ -554,7 +554,7 @@ reconcile/utils/quay_api.py,sha256=EuOegpb-7ntEjkKLFwM2Oo4Nw7SyFtmyl3sQ9aXMtrM,8
554
554
  reconcile/utils/raw_github_api.py,sha256=ZHC-SZuAyRe1zaMoOU7Krt1-zecDxENd9c_NzQYqK9g,2968
555
555
  reconcile/utils/repo_owners.py,sha256=j-pUjc9PuDzq7KpjNLpnhqfU8tUG4nj2WMhFp4ick7g,6629
556
556
  reconcile/utils/secret_reader.py,sha256=2DeYAAQFjUULEKlLw3UDAUoND6gbqvCh9uKPtlc-0us,10403
557
- reconcile/utils/semver_helper.py,sha256=4Rrkz9Cj9A6oHPVgA-nGj6MBoxlFT4eyxTcslWqow3I,771
557
+ reconcile/utils/semver_helper.py,sha256=dp86KxjlOc8LHzawMvbxRfZamv7KU7b2SVnZQL-Xg6U,1142
558
558
  reconcile/utils/sharding.py,sha256=gkYf0lD3IUKQPEmdRJZ70mdDT1c9qWjbdP7evRsUis4,839
559
559
  reconcile/utils/slack_api.py,sha256=fntPGTn4hHRiScEIZu9LDPwbKeNPe9bR2Rmef9c-GIM,15830
560
560
  reconcile/utils/smtp_client.py,sha256=gJNbBQJpAt5PX4t_TaeNHsXM8vt50bFgndml6yK2b5o,2800
@@ -646,8 +646,8 @@ tools/test/test_app_interface_metrics_exporter.py,sha256=dmEcNwZltP1rd_4DbxIYakO
646
646
  tools/test/test_qontract_cli.py,sha256=awwTHEc2DWlykuqGIYM0WOBoSL0KRnOraCLk3C7izis,1401
647
647
  tools/test/test_sd_app_sre_alert_report.py,sha256=v363r9zM7__0kR5K6mvJoGFcM9BvE33fWAayrqkpojA,2116
648
648
  tools/test/test_sre_checkpoints.py,sha256=SKqPPTl9ua0RFdSSofnoQX-JZE6dFLO3LRhfQzqtfh8,2607
649
- qontract_reconcile-0.10.1rc441.dist-info/METADATA,sha256=Tp4NZpTAgE4BiPYcp0W9kXVyNBkJ150Bmua9tKIJP2c,2348
650
- qontract_reconcile-0.10.1rc441.dist-info/WHEEL,sha256=oiQVh_5PnQM0E3gPdiz09WCNmwiHDMaGer_elqB3coM,92
651
- qontract_reconcile-0.10.1rc441.dist-info/entry_points.txt,sha256=rTjAv28I_CHLM8ID3OPqMI_suoQ9s7tFbim4aYjn9kk,376
652
- qontract_reconcile-0.10.1rc441.dist-info/top_level.txt,sha256=l5ISPoXzt0SdR4jVdkfa7RPSKNc8zAHYWAnR-Dw8Ey8,24
653
- qontract_reconcile-0.10.1rc441.dist-info/RECORD,,
649
+ qontract_reconcile-0.10.1rc449.dist-info/METADATA,sha256=E2bO2KPEa-O0CBcDnYqJkEyOOndI0tpzL0CDdBumIDM,2348
650
+ qontract_reconcile-0.10.1rc449.dist-info/WHEEL,sha256=oiQVh_5PnQM0E3gPdiz09WCNmwiHDMaGer_elqB3coM,92
651
+ qontract_reconcile-0.10.1rc449.dist-info/entry_points.txt,sha256=rTjAv28I_CHLM8ID3OPqMI_suoQ9s7tFbim4aYjn9kk,376
652
+ qontract_reconcile-0.10.1rc449.dist-info/top_level.txt,sha256=l5ISPoXzt0SdR4jVdkfa7RPSKNc8zAHYWAnR-Dw8Ey8,24
653
+ qontract_reconcile-0.10.1rc449.dist-info/RECORD,,
reconcile/aus/base.py CHANGED
@@ -296,6 +296,8 @@ class AdvancedUpgradeSchedulerBaseIntegration(
296
296
  cluster_upgrade_spec.upgrade_policy.conditions.soak_days or 0
297
297
  ),
298
298
  workloads=",".join(cluster_upgrade_spec.upgrade_policy.workloads),
299
+ product=cluster_upgrade_spec.cluster.product.id,
300
+ hypershift=cluster_upgrade_spec.cluster.hypershift.enabled,
299
301
  ),
300
302
  )
301
303
 
reconcile/aus/metrics.py CHANGED
@@ -66,6 +66,8 @@ class AUSClusterUpgradePolicyInfoMetric(AUSBaseMetric, InfoMetric):
66
66
  mutexes: str
67
67
  soak_days: str
68
68
  workloads: str
69
+ product: str
70
+ hypershift: bool
69
71
 
70
72
  @classmethod
71
73
  def name(cls) -> str:
@@ -211,6 +211,8 @@ class OCMAddonsUpgradeSchedulerOrgIntegration(
211
211
  ),
212
212
  workloads=",".join(cluster_upgrade_spec.upgrade_policy.workloads),
213
213
  addon=cluster_upgrade_spec.addon.id,
214
+ product=cluster_upgrade_spec.cluster.product.id,
215
+ hypershift=cluster_upgrade_spec.cluster.hypershift.enabled,
214
216
  ),
215
217
  )
216
218
 
@@ -5,7 +5,11 @@ from collections.abc import (
5
5
  )
6
6
  from typing import Any
7
7
 
8
- from pydantic import BaseModel
8
+ import semver
9
+ from pydantic import (
10
+ BaseModel,
11
+ validator,
12
+ )
9
13
 
10
14
  from reconcile.aws_version_sync.merge_request_manager.merge_request import (
11
15
  Parser,
@@ -42,6 +46,7 @@ from reconcile.utils.runtime.integration import (
42
46
  PydanticRunParams,
43
47
  QontractReconcileIntegration,
44
48
  )
49
+ from reconcile.utils.semver_helper import parse_semver
45
50
  from reconcile.utils.unleash import get_feature_toggle_state
46
51
  from reconcile.utils.vcs import VCS
47
52
 
@@ -67,7 +72,10 @@ class ExternalResource(BaseModel):
67
72
  resource_provider: str
68
73
  resource_identifier: str
69
74
  resource_engine: str
70
- resource_engine_version: str
75
+ resource_engine_version: semver.VersionInfo
76
+
77
+ class Config:
78
+ arbitrary_types_allowed = True
71
79
 
72
80
  @property
73
81
  def key(self) -> tuple:
@@ -79,6 +87,14 @@ class ExternalResource(BaseModel):
79
87
  self.resource_engine,
80
88
  )
81
89
 
90
+ @validator("resource_engine_version", pre=True)
91
+ def parse_resource_engine_version( # pylint: disable=no-self-argument
92
+ cls, v: str | semver.VersionInfo
93
+ ) -> semver.VersionInfo:
94
+ if isinstance(v, semver.VersionInfo):
95
+ return v
96
+ return parse_semver(v, optional_minor_and_patch=True)
97
+
82
98
 
83
99
  AwsExternalResources = list[ExternalResource]
84
100
  AppInterfaceExternalResources = list[ExternalResource]
@@ -228,12 +244,18 @@ class AVSIntegration(QontractReconcileIntegration[AVSIntegrationParams]):
228
244
  current=external_resources_app_interface,
229
245
  desired=external_resources_aws,
230
246
  key=lambda r: r.key,
231
- equal=lambda r1, r2: r1.resource_engine_version
232
- == r2.resource_engine_version,
247
+ equal=lambda external_resources_app_interface, external_resources_aws: external_resources_app_interface.resource_engine_version
248
+ == external_resources_aws.resource_engine_version,
233
249
  )
234
250
  for diff_pair in diff.change.values():
235
251
  aws_resource = diff_pair.desired
236
252
  app_interface_resource = diff_pair.current
253
+ if (
254
+ aws_resource.resource_engine_version
255
+ <= app_interface_resource.resource_engine_version
256
+ ):
257
+ # do not downgrade the version
258
+ continue
237
259
  # make mypy happy
238
260
  assert app_interface_resource.namespace_file
239
261
  assert app_interface_resource.provisioner.path
reconcile/cli.py CHANGED
@@ -1701,17 +1701,10 @@ def aws_version_sync(
1701
1701
  "--output-file",
1702
1702
  help="Specify where to place the output of the integration",
1703
1703
  )
1704
- @click.option(
1705
- "--ignore-state-errors",
1706
- is_flag=True,
1707
- help="Instructs terraform-repo to ignore state load errors and re-create repo states",
1708
- )
1709
1704
  @click.argument("gitlab-project-id", required=False)
1710
1705
  @click.argument("gitlab-merge-request-id", required=False)
1711
1706
  @click.pass_context
1712
- def terraform_repo(
1713
- ctx, output_file, ignore_state_errors, gitlab_project_id, gitlab_merge_request_id
1714
- ):
1707
+ def terraform_repo(ctx, output_file, gitlab_project_id, gitlab_merge_request_id):
1715
1708
  from reconcile import terraform_repo
1716
1709
 
1717
1710
  run_class_integration(
@@ -1719,7 +1712,6 @@ def terraform_repo(
1719
1712
  terraform_repo.TerraformRepoIntegrationParams(
1720
1713
  output_file=output_file,
1721
1714
  validate_git=True,
1722
- ignore_state_errors=ignore_state_errors,
1723
1715
  gitlab_project_id=gitlab_project_id,
1724
1716
  gitlab_merge_request_id=gitlab_merge_request_id,
1725
1717
  )
@@ -102,9 +102,13 @@ REVOKE ALL ON DATABASE "{self._get_db()}" FROM public;
102
102
 
103
103
  \\c "{self._get_db()}"
104
104
 
105
- select 'CREATE ROLE "{self._get_user()}" WITH LOGIN PASSWORD ''{self.connection_parameter.password}'' VALID UNTIL ''infinity'''
105
+ select 'CREATE ROLE "{self._get_user()}"'
106
106
  WHERE NOT EXISTS (SELECT FROM pg_catalog.pg_roles WHERE rolname = '{self._get_db()}');\\gexec
107
107
 
108
+ ALTER ROLE "{self._get_user()}" WITH LOGIN PASSWORD '{self.connection_parameter.password}' VALID UNTIL 'infinity';
109
+
110
+ GRANT CONNECT ON DATABASE "{self._get_db()}" to "{self._get_user()}";
111
+
108
112
  -- rds specific, grant role to admin or create schema fails
109
113
  GRANT "{self._get_user()}" to "{self._get_admin_user()}";
110
114
  CREATE SCHEMA IF NOT EXISTS "{self._get_user()}" AUTHORIZATION "{self._get_user()}";"""
@@ -272,7 +276,7 @@ def get_job_spec(job_data: JobData) -> OpenshiftResource:
272
276
  command,
273
277
  ],
274
278
  "args": [
275
- "-ae",
279
+ "-b",
276
280
  "--host=$(db.host)",
277
281
  "--port=$(db.port)",
278
282
  "--username=$(db.user)",
@@ -4,6 +4,7 @@ from abc import (
4
4
  abstractmethod,
5
5
  )
6
6
  from collections.abc import Mapping
7
+ from enum import Enum
7
8
  from typing import (
8
9
  Iterable,
9
10
  Optional,
@@ -11,6 +12,7 @@ from typing import (
11
12
 
12
13
  from pydantic import (
13
14
  BaseModel,
15
+ Field,
14
16
  root_validator,
15
17
  )
16
18
 
@@ -24,6 +26,7 @@ from reconcile.typed_queries.clusters import get_clusters
24
26
  from reconcile.utils.differ import diff_mappings
25
27
  from reconcile.utils.disabled_integrations import integration_is_enabled
26
28
  from reconcile.utils.ocm import (
29
+ DEFAULT_OCM_MACHINE_POOL_ID,
27
30
  OCM,
28
31
  OCMMap,
29
32
  )
@@ -31,6 +34,12 @@ from reconcile.utils.ocm import (
31
34
  QONTRACT_INTEGRATION = "ocm-machine-pools"
32
35
 
33
36
 
37
+ class ClusterType(Enum):
38
+ OSD = "osd"
39
+ ROSA_CLASSIC = "rosa"
40
+ ROSA_HCP = "hypershift"
41
+
42
+
34
43
  class InvalidUpdateError(Exception):
35
44
  pass
36
45
 
@@ -95,6 +104,7 @@ class AbstractPool(ABC, BaseModel):
95
104
  taints: Optional[list[Mapping[str, str]]]
96
105
  labels: Optional[Mapping[str, str]]
97
106
  cluster: str
107
+ cluster_type: ClusterType = Field(..., exclude=True)
98
108
  autoscaling: Optional[AbstractAutoscaling]
99
109
 
100
110
  @root_validator()
@@ -180,10 +190,19 @@ class MachinePool(AbstractPool):
180
190
  return None
181
191
 
182
192
  def deletable(self) -> bool:
183
- return True
193
+ # OSD NON CCS clusters can't delete default worker machine pool
194
+ return not (
195
+ self.cluster_type == ClusterType.OSD
196
+ and self.id == DEFAULT_OCM_MACHINE_POOL_ID
197
+ )
184
198
 
185
199
  @classmethod
186
- def create_from_gql(cls, pool: ClusterMachinePoolV1, cluster: str):
200
+ def create_from_gql(
201
+ cls,
202
+ pool: ClusterMachinePoolV1,
203
+ cluster: str,
204
+ cluster_type: ClusterType,
205
+ ):
187
206
  autoscaling: Optional[MachinePoolAutoscaling] = None
188
207
  if pool.autoscale:
189
208
  autoscaling = MachinePoolAutoscaling(
@@ -198,6 +217,7 @@ class MachinePool(AbstractPool):
198
217
  taints=[p.dict(by_alias=True) for p in pool.taints or []],
199
218
  labels=pool.labels,
200
219
  cluster=cluster,
220
+ cluster_type=cluster_type,
201
221
  )
202
222
 
203
223
 
@@ -254,11 +274,15 @@ class NodePool(AbstractPool):
254
274
  return None
255
275
 
256
276
  def deletable(self) -> bool:
257
- # As of now, you can not delete the first worker pool(s)
258
- return not self.id.startswith("workers")
277
+ return True
259
278
 
260
279
  @classmethod
261
- def create_from_gql(cls, pool: ClusterMachinePoolV1, cluster: str):
280
+ def create_from_gql(
281
+ cls,
282
+ pool: ClusterMachinePoolV1,
283
+ cluster: str,
284
+ cluster_type: ClusterType,
285
+ ):
262
286
  autoscaling: Optional[NodePoolAutoscaling] = None
263
287
  if pool.autoscale:
264
288
  autoscaling = NodePoolAutoscaling(
@@ -277,6 +301,7 @@ class NodePool(AbstractPool):
277
301
  labels=pool.labels,
278
302
  subnet=pool.subnet,
279
303
  cluster=cluster,
304
+ cluster_type=cluster_type,
280
305
  )
281
306
 
282
307
 
@@ -303,7 +328,7 @@ class PoolHandler(BaseModel):
303
328
 
304
329
  class DesiredMachinePool(BaseModel):
305
330
  cluster_name: str
306
- hypershift: bool
331
+ cluster_type: ClusterType
307
332
  pools: list[ClusterMachinePoolV1]
308
333
 
309
334
  def build_pool_handler(
@@ -312,11 +337,13 @@ class DesiredMachinePool(BaseModel):
312
337
  pool: ClusterMachinePoolV1,
313
338
  ) -> PoolHandler:
314
339
  pool_builder = (
315
- NodePool.create_from_gql if self.hypershift else MachinePool.create_from_gql
340
+ NodePool.create_from_gql
341
+ if self.cluster_type == ClusterType.ROSA_HCP
342
+ else MachinePool.create_from_gql
316
343
  )
317
344
  return PoolHandler(
318
345
  action=action,
319
- pool=pool_builder(pool, self.cluster_name),
346
+ pool=pool_builder(pool, self.cluster_name, self.cluster_type),
320
347
  )
321
348
 
322
349
 
@@ -330,12 +357,25 @@ def fetch_current_state(
330
357
  }
331
358
 
332
359
 
333
- def _is_hypershift(cluster: ClusterV1) -> bool:
334
- return bool(cluster.spec and cluster.spec.hypershift)
360
+ def _classify_cluster_type(cluster: ClusterV1) -> ClusterType:
361
+ if cluster.spec is None:
362
+ raise ValueError(f"cluster {cluster.name} is missing spec")
363
+ match cluster.spec.product:
364
+ case "osd":
365
+ return ClusterType.OSD
366
+ case "rosa":
367
+ return (
368
+ ClusterType.ROSA_HCP
369
+ if cluster.spec.hypershift
370
+ else ClusterType.ROSA_CLASSIC
371
+ )
372
+ case _:
373
+ raise ValueError(f"unknown cluster type for cluster {cluster.name}")
335
374
 
336
375
 
337
376
  def fetch_current_state_for_cluster(cluster, ocm):
338
- if _is_hypershift(cluster):
377
+ cluster_type = _classify_cluster_type(cluster)
378
+ if cluster_type == ClusterType.ROSA_HCP:
339
379
  return [
340
380
  NodePool(
341
381
  id=node_pool["id"],
@@ -354,6 +394,7 @@ def fetch_current_state_for_cluster(cluster, ocm):
354
394
  labels=node_pool.get("labels"),
355
395
  subnet=node_pool.get("subnet"),
356
396
  cluster=cluster.name,
397
+ cluster_type=cluster_type,
357
398
  )
358
399
  for node_pool in ocm.get_node_pools(cluster.name)
359
400
  ]
@@ -371,6 +412,7 @@ def fetch_current_state_for_cluster(cluster, ocm):
371
412
  taints=machine_pool.get("taints"),
372
413
  labels=machine_pool.get("labels"),
373
414
  cluster=cluster.name,
415
+ cluster_type=cluster_type,
374
416
  )
375
417
  for machine_pool in ocm.get_machine_pools(cluster.name)
376
418
  ]
@@ -382,7 +424,7 @@ def create_desired_state_from_gql(
382
424
  return {
383
425
  cluster.name: DesiredMachinePool(
384
426
  cluster_name=cluster.name,
385
- hypershift=_is_hypershift(cluster),
427
+ cluster_type=_classify_cluster_type(cluster),
386
428
  pools=cluster.machine_pools,
387
429
  )
388
430
  for cluster in clusters
@@ -452,6 +494,12 @@ def calculate_diff(
452
494
  pool=current_machine_pool,
453
495
  )
454
496
  )
497
+ else:
498
+ errors.append(
499
+ InvalidUpdateError(
500
+ f"can not delete machine pool {current_machine_pool.id} for cluster {cluster_name}"
501
+ )
502
+ )
455
503
 
456
504
  return diffs, errors
457
505
 
@@ -66,7 +66,6 @@ class OutputFile(BaseModel):
66
66
  class TerraformRepoIntegrationParams(PydanticRunParams):
67
67
  output_file: Optional[str]
68
68
  validate_git: bool
69
- ignore_state_errors: bool
70
69
  gitlab_project_id: Optional[str]
71
70
  gitlab_merge_request_id: Optional[int]
72
71
 
@@ -97,14 +96,35 @@ class TerraformRepoIntegration(
97
96
  defer(state.cleanup)
98
97
 
99
98
  desired = self.get_repos(query_func=gqlapi.query)
100
- existing = self.get_existing_state(state)
101
-
102
- repo_diff_result = self.calculate_diff(
103
- existing_state=existing, desired_state=desired, dry_run=dry_run, state=state
104
- )
99
+ try:
100
+ existing = self.get_existing_state(state)
101
+
102
+ repo_diff_result = self.calculate_diff(
103
+ existing_state=existing,
104
+ desired_state=desired,
105
+ dry_run=dry_run,
106
+ state=state,
107
+ recreate_state=False,
108
+ )
105
109
 
106
- if repo_diff_result:
107
- self.print_output(repo_diff_result, dry_run)
110
+ if repo_diff_result:
111
+ self.print_output(repo_diff_result, dry_run)
112
+ except ValidationError as err:
113
+ # when updating TerraformRepoV1 GQL schema, Pydantic does not gracefully handle these changes and fails to parse
114
+ # the existing state stored in S3. This is due to a behavior in Pydantic V1 that has since been addressed in V2
115
+ # https://docs.pydantic.dev/latest/blog/pydantic-v2/#required-vs-nullable-cleanup
116
+ # so in this case, tf-repo will just recreate all of those state files in S3 and not actually do a plan or apply
117
+ logging.error(err)
118
+ logging.info(
119
+ "Unable to parse existing Terraform-Repo state from S3. Note that this is separate from the actual .tfstate files. Terraform Repo will re-create its own state upon merge and will not update any infrastructure. This typically occurs with changes to the Terraform Repo schema files and is normally resolved once state is re-created."
120
+ )
121
+ repo_diff_result = self.calculate_diff(
122
+ existing_state=[],
123
+ desired_state=desired,
124
+ dry_run=dry_run,
125
+ state=state,
126
+ recreate_state=True,
127
+ )
108
128
 
109
129
  def print_output(self, diff: list[TerraformRepoV1], dry_run: bool) -> None:
110
130
  """Parses and prints the output of a Terraform Repo diff for the executor
@@ -182,15 +202,8 @@ class TerraformRepoIntegration(
182
202
  keys = state.ls()
183
203
  for key in keys:
184
204
  if value := state.get(key.lstrip("/"), None):
185
- try:
186
- repo = TerraformRepoV1.parse_obj(value)
187
- repo_list.append(repo)
188
- except ValidationError as err:
189
- logging.error(
190
- f"{err}\nUnable to parse existing state for repo: '{key}'"
191
- )
192
- if self.params.ignore_state_errors:
193
- logging.info("Ignoring state load error")
205
+ repo = TerraformRepoV1.parse_obj(value)
206
+ repo_list.append(repo)
194
207
 
195
208
  return repo_list
196
209
 
@@ -281,6 +294,7 @@ class TerraformRepoIntegration(
281
294
  desired_state: list[TerraformRepoV1],
282
295
  dry_run: bool,
283
296
  state: Optional[State],
297
+ recreate_state: bool,
284
298
  ) -> Optional[list[TerraformRepoV1]]:
285
299
  """Calculated the difference between existing and desired state
286
300
  to determine what actions the executor will need to take
@@ -293,6 +307,8 @@ class TerraformRepoIntegration(
293
307
  :type dry_run: bool
294
308
  :param state: AWS S3 state
295
309
  :type state: Optional[State]
310
+ :param recreate_state: whether we are recreating our own state
311
+ :type recreate_state: bool
296
312
  :raises ParameterError: if there is an invalid operation performed like trying to delete
297
313
  a representation in A-I before setting the delete flag
298
314
  :return: the terraform repo to act on
@@ -304,7 +320,7 @@ class TerraformRepoIntegration(
304
320
 
305
321
  # validate that only one repo is being modified in each MR
306
322
  # this lets us fail early and avoid multiple GL requests we don't need to make
307
- if dry_run and len(merged) > 1 and not self.params.ignore_state_errors:
323
+ if dry_run and len(merged) > 1 and not recreate_state:
308
324
  raise Exception(
309
325
  "Only one repository can be modified per merge request, please split your change out into multiple MRs. Hint: try rebasing your merge request"
310
326
  )
@@ -131,7 +131,9 @@ def openshift_resource_secet() -> OpenshiftResource:
131
131
  def _assert_create_script(script: str) -> None:
132
132
  assert 'CREATE DATABASE "test"' in script
133
133
  assert "REVOKE ALL ON DATABASE" in script
134
- assert 'CREATE ROLE "test" WITH LOGIN PASSWORD' in script
134
+ assert 'CREATE ROLE "test"' in script
135
+ assert 'ALTER ROLE "test" WITH LOGIN' in script
136
+ assert 'GRANT CONNECT ON DATABASE "test" to "test"' in script
135
137
  assert "CREATE SCHEMA IF NOT EXISTS" in script
136
138
  assert 'GRANT "test" to "admin";' in script
137
139
 
@@ -19,6 +19,7 @@ from reconcile.gql_definitions.common.clusters import (
19
19
  from reconcile.ocm_machine_pools import (
20
20
  AbstractPool,
21
21
  AWSNodePool,
22
+ ClusterType,
22
23
  DesiredMachinePool,
23
24
  InvalidUpdateError,
24
25
  MachinePool,
@@ -63,6 +64,7 @@ def test_pool() -> TestPool:
63
64
  labels=None,
64
65
  taints=None,
65
66
  cluster="cluster1",
67
+ cluster_type=ClusterType.OSD,
66
68
  )
67
69
 
68
70
 
@@ -77,6 +79,7 @@ def current_with_pool() -> Mapping[str, list[AbstractPool]]:
77
79
  labels=None,
78
80
  taints=None,
79
81
  cluster="cluster1",
82
+ cluster_type=ClusterType.OSD,
80
83
  )
81
84
  ]
82
85
  }
@@ -90,6 +93,7 @@ def node_pool() -> NodePool:
90
93
  labels=None,
91
94
  taints=None,
92
95
  cluster="cluster1",
96
+ cluster_type=ClusterType.ROSA_HCP,
93
97
  subnet="subnet1",
94
98
  aws_node_pool=AWSNodePool(
95
99
  instance_type="m5.xlarge",
@@ -105,7 +109,7 @@ def machine_pool() -> MachinePool:
105
109
  labels=None,
106
110
  taints=None,
107
111
  cluster="cluster1",
108
- subnet="subnet1",
112
+ cluster_type=ClusterType.OSD,
109
113
  instance_type="m5.xlarge",
110
114
  )
111
115
 
@@ -129,7 +133,7 @@ def ocm_mock():
129
133
 
130
134
 
131
135
  def test_diff__has_diff_autoscale(cluster_machine_pool: ClusterMachinePoolV1):
132
- pool = TestPool(id="pool1", cluster="cluster1")
136
+ pool = TestPool(id="pool1", cluster="cluster1", cluster_type=ClusterType.OSD)
133
137
 
134
138
  assert cluster_machine_pool.autoscale is None
135
139
  assert not pool._has_diff_autoscale(cluster_machine_pool)
@@ -165,7 +169,7 @@ def test_calculate_diff_create():
165
169
  desired = {
166
170
  "cluster1": DesiredMachinePool(
167
171
  cluster_name="cluster1",
168
- hypershift=False,
172
+ cluster_type=ClusterType.OSD,
169
173
  pools=[
170
174
  ClusterMachinePoolV1(
171
175
  id="pool1",
@@ -190,7 +194,7 @@ def test_calculate_diff_noop(current_with_pool):
190
194
  desired = {
191
195
  "cluster1": DesiredMachinePool(
192
196
  cluster_name="cluster1",
193
- hypershift=False,
197
+ cluster_type=ClusterType.OSD,
194
198
  pools=[
195
199
  ClusterMachinePoolV1(
196
200
  id="pool1",
@@ -213,7 +217,7 @@ def test_calculate_diff_update(current_with_pool):
213
217
  desired = {
214
218
  "cluster1": DesiredMachinePool(
215
219
  cluster_name="cluster1",
216
- hypershift=False,
220
+ cluster_type=ClusterType.OSD,
217
221
  pools=[
218
222
  ClusterMachinePoolV1(
219
223
  id="pool1",
@@ -245,6 +249,7 @@ def current_with_2_pools() -> Mapping[str, list[AbstractPool]]:
245
249
  labels=None,
246
250
  taints=None,
247
251
  cluster="cluster1",
252
+ cluster_type=ClusterType.OSD,
248
253
  ),
249
254
  MachinePool(
250
255
  id="workers",
@@ -253,6 +258,7 @@ def current_with_2_pools() -> Mapping[str, list[AbstractPool]]:
253
258
  labels=None,
254
259
  taints=None,
255
260
  cluster="cluster1",
261
+ cluster_type=ClusterType.OSD,
256
262
  ),
257
263
  ]
258
264
  }
@@ -262,7 +268,7 @@ def test_calculate_diff_delete(current_with_2_pools):
262
268
  desired = {
263
269
  "cluster1": DesiredMachinePool(
264
270
  cluster_name="cluster1",
265
- hypershift=False,
271
+ cluster_type=ClusterType.OSD,
266
272
  pools=[
267
273
  ClusterMachinePoolV1(
268
274
  id="pool1",
@@ -287,7 +293,7 @@ def test_calculate_diff_delete_all_fail_validation(current_with_pool):
287
293
  desired = {
288
294
  "cluster1": DesiredMachinePool(
289
295
  cluster_name="cluster1",
290
- hypershift=False,
296
+ cluster_type=ClusterType.OSD,
291
297
  pools=[],
292
298
  ),
293
299
  }
@@ -450,7 +456,33 @@ def test_run_no_action(mocker: MockerFixture) -> None:
450
456
 
451
457
 
452
458
  @pytest.fixture
453
- def ocm_cluster_builder(
459
+ def osd_cluster_builder(
460
+ gql_class_factory: Callable[..., ClusterV1],
461
+ ) -> Callable[..., ClusterV1]:
462
+ def builder(machine_pools: list[dict]) -> ClusterV1:
463
+ return gql_class_factory(
464
+ ClusterV1,
465
+ {
466
+ "name": "ocm-cluster",
467
+ "auth": [],
468
+ "spec": {
469
+ "product": "osd",
470
+ },
471
+ "ocm": {
472
+ "name": "ocm-name",
473
+ "environment": {
474
+ "accessTokenClientSecret": {},
475
+ },
476
+ },
477
+ "machinePools": machine_pools,
478
+ },
479
+ )
480
+
481
+ return builder
482
+
483
+
484
+ @pytest.fixture
485
+ def rosa_cluster_builder(
454
486
  gql_class_factory: Callable[..., ClusterV1],
455
487
  ) -> Callable[..., ClusterV1]:
456
488
  def builder(machine_pools: list[dict]) -> ClusterV1:
@@ -459,6 +491,9 @@ def ocm_cluster_builder(
459
491
  {
460
492
  "name": "ocm-cluster",
461
493
  "auth": [],
494
+ "spec": {
495
+ "product": "rosa",
496
+ },
462
497
  "ocm": {
463
498
  "name": "ocm-name",
464
499
  "environment": {
@@ -482,11 +517,19 @@ def default_worker_machine_pool() -> dict:
482
517
 
483
518
 
484
519
  @pytest.fixture
485
- def ocm_cluster_with_default_machine_pool(
486
- ocm_cluster_builder: Callable[..., ClusterV1],
520
+ def osd_cluster_with_default_machine_pool(
521
+ osd_cluster_builder,
522
+ default_worker_machine_pool: dict,
523
+ ) -> ClusterV1:
524
+ return osd_cluster_builder([default_worker_machine_pool])
525
+
526
+
527
+ @pytest.fixture
528
+ def rosa_cluster_with_default_machine_pool(
529
+ rosa_cluster_builder,
487
530
  default_worker_machine_pool: dict,
488
531
  ) -> ClusterV1:
489
- return ocm_cluster_builder([default_worker_machine_pool])
532
+ return rosa_cluster_builder([default_worker_machine_pool])
490
533
 
491
534
 
492
535
  @pytest.fixture
@@ -499,12 +542,26 @@ def new_workers_machine_pool() -> dict:
499
542
 
500
543
 
501
544
  @pytest.fixture
502
- def ocm_cluster_with_default_and_new_machine_pools(
503
- ocm_cluster_builder: Callable[..., ClusterV1],
545
+ def osd_cluster_with_default_and_new_machine_pools(
546
+ osd_cluster_builder,
547
+ default_worker_machine_pool: dict,
548
+ new_workers_machine_pool: dict,
549
+ ) -> ClusterV1:
550
+ return osd_cluster_builder(
551
+ [
552
+ default_worker_machine_pool,
553
+ new_workers_machine_pool,
554
+ ]
555
+ )
556
+
557
+
558
+ @pytest.fixture
559
+ def rosa_cluster_with_default_and_new_machine_pools(
560
+ rosa_cluster_builder,
504
561
  default_worker_machine_pool: dict,
505
562
  new_workers_machine_pool: dict,
506
563
  ) -> ClusterV1:
507
- return ocm_cluster_builder(
564
+ return rosa_cluster_builder(
508
565
  [
509
566
  default_worker_machine_pool,
510
567
  new_workers_machine_pool,
@@ -525,22 +582,42 @@ def expected_ocm_machine_pool_create_payload() -> dict:
525
582
  }
526
583
 
527
584
 
528
- def test_run_create_machine_pool(
585
+ def test_run_create_machine_pool_for_osd_cluster(
529
586
  mocker: MockerFixture,
530
- ocm_cluster_with_default_and_new_machine_pools: ClusterV1,
587
+ osd_cluster_with_default_and_new_machine_pools: ClusterV1,
531
588
  default_worker_machine_pool: dict,
532
589
  expected_ocm_machine_pool_create_payload: dict,
533
590
  ) -> None:
534
591
  mocks = setup_mocks(
535
592
  mocker,
536
- clusters=[ocm_cluster_with_default_and_new_machine_pools],
593
+ clusters=[osd_cluster_with_default_and_new_machine_pools],
537
594
  machine_pools=[default_worker_machine_pool],
538
595
  )
539
596
 
540
597
  run(False)
541
598
 
542
599
  mocks["OCM"].create_machine_pool.assert_called_once_with(
543
- ocm_cluster_with_default_and_new_machine_pools.name,
600
+ osd_cluster_with_default_and_new_machine_pools.name,
601
+ expected_ocm_machine_pool_create_payload,
602
+ )
603
+
604
+
605
+ def test_run_create_machine_pool_for_rosa_cluster(
606
+ mocker: MockerFixture,
607
+ rosa_cluster_with_default_and_new_machine_pools: ClusterV1,
608
+ default_worker_machine_pool: dict,
609
+ expected_ocm_machine_pool_create_payload: dict,
610
+ ) -> None:
611
+ mocks = setup_mocks(
612
+ mocker,
613
+ clusters=[rosa_cluster_with_default_and_new_machine_pools],
614
+ machine_pools=[default_worker_machine_pool],
615
+ )
616
+
617
+ run(False)
618
+
619
+ mocks["OCM"].create_machine_pool.assert_called_once_with(
620
+ rosa_cluster_with_default_and_new_machine_pools.name,
544
621
  expected_ocm_machine_pool_create_payload,
545
622
  )
546
623
 
@@ -564,22 +641,42 @@ def expected_ocm_machine_pool_update_payload() -> dict:
564
641
  }
565
642
 
566
643
 
567
- def test_run_update_machine_pool(
644
+ def test_run_update_machine_pool_for_osd_cluster(
645
+ mocker: MockerFixture,
646
+ osd_cluster_with_default_machine_pool: ClusterV1,
647
+ existing_updated_default_machine_pool: dict,
648
+ expected_ocm_machine_pool_update_payload: dict,
649
+ ) -> None:
650
+ mocks = setup_mocks(
651
+ mocker,
652
+ clusters=[osd_cluster_with_default_machine_pool],
653
+ machine_pools=[existing_updated_default_machine_pool],
654
+ )
655
+
656
+ run(False)
657
+
658
+ mocks["OCM"].update_machine_pool.assert_called_once_with(
659
+ osd_cluster_with_default_machine_pool.name,
660
+ expected_ocm_machine_pool_update_payload,
661
+ )
662
+
663
+
664
+ def test_run_update_machine_pool_for_rosa_cluster(
568
665
  mocker: MockerFixture,
569
- ocm_cluster_with_default_machine_pool: ClusterV1,
666
+ rosa_cluster_with_default_machine_pool: ClusterV1,
570
667
  existing_updated_default_machine_pool: dict,
571
668
  expected_ocm_machine_pool_update_payload: dict,
572
669
  ) -> None:
573
670
  mocks = setup_mocks(
574
671
  mocker,
575
- clusters=[ocm_cluster_with_default_machine_pool],
672
+ clusters=[rosa_cluster_with_default_machine_pool],
576
673
  machine_pools=[existing_updated_default_machine_pool],
577
674
  )
578
675
 
579
676
  run(False)
580
677
 
581
678
  mocks["OCM"].update_machine_pool.assert_called_once_with(
582
- ocm_cluster_with_default_machine_pool.name,
679
+ rosa_cluster_with_default_machine_pool.name,
583
680
  expected_ocm_machine_pool_update_payload,
584
681
  )
585
682
 
@@ -593,14 +690,32 @@ def existing_default_machine_pool_with_different_instance_type() -> dict:
593
690
  }
594
691
 
595
692
 
596
- def test_run_update_machine_pool_error(
693
+ def test_run_update_machine_pool_error_for_osd_cluster(
597
694
  mocker: MockerFixture,
598
- ocm_cluster_with_default_machine_pool: ClusterV1,
695
+ osd_cluster_with_default_machine_pool: ClusterV1,
599
696
  existing_default_machine_pool_with_different_instance_type: dict,
600
697
  ) -> None:
601
698
  setup_mocks(
602
699
  mocker,
603
- clusters=[ocm_cluster_with_default_machine_pool],
700
+ clusters=[osd_cluster_with_default_machine_pool],
701
+ machine_pools=[existing_default_machine_pool_with_different_instance_type],
702
+ )
703
+
704
+ with pytest.raises(ExceptionGroup) as eg:
705
+ run(False)
706
+
707
+ assert len(eg.value.exceptions) == 1
708
+ assert isinstance(eg.value.exceptions[0], InvalidUpdateError)
709
+
710
+
711
+ def test_run_update_machine_pool_error_for_rosa_cluster(
712
+ mocker: MockerFixture,
713
+ rosa_cluster_with_default_machine_pool: ClusterV1,
714
+ existing_default_machine_pool_with_different_instance_type: dict,
715
+ ) -> None:
716
+ setup_mocks(
717
+ mocker,
718
+ clusters=[rosa_cluster_with_default_machine_pool],
604
719
  machine_pools=[existing_default_machine_pool_with_different_instance_type],
605
720
  )
606
721
 
@@ -624,42 +739,70 @@ def expected_ocm_machine_pool_delete_payload() -> dict:
624
739
  }
625
740
 
626
741
 
627
- def test_run_delete_machine_pool(
742
+ def test_run_delete_machine_pool_for_osd_cluster(
743
+ mocker: MockerFixture,
744
+ osd_cluster_with_default_machine_pool: ClusterV1,
745
+ default_worker_machine_pool: dict,
746
+ new_workers_machine_pool: dict,
747
+ expected_ocm_machine_pool_delete_payload: dict,
748
+ ) -> None:
749
+ mocks = setup_mocks(
750
+ mocker,
751
+ clusters=[osd_cluster_with_default_machine_pool],
752
+ machine_pools=[default_worker_machine_pool, new_workers_machine_pool],
753
+ )
754
+
755
+ run(False)
756
+
757
+ mocks["OCM"].delete_machine_pool.assert_called_once_with(
758
+ osd_cluster_with_default_machine_pool.name,
759
+ expected_ocm_machine_pool_delete_payload,
760
+ )
761
+
762
+
763
+ def test_run_delete_machine_pool_for_rosa_cluster(
628
764
  mocker: MockerFixture,
629
- ocm_cluster_with_default_machine_pool: ClusterV1,
630
- default_worker_machine_pool,
765
+ rosa_cluster_with_default_machine_pool: ClusterV1,
766
+ default_worker_machine_pool: dict,
631
767
  new_workers_machine_pool: dict,
632
768
  expected_ocm_machine_pool_delete_payload: dict,
633
769
  ) -> None:
634
770
  mocks = setup_mocks(
635
771
  mocker,
636
- clusters=[ocm_cluster_with_default_machine_pool],
772
+ clusters=[rosa_cluster_with_default_machine_pool],
637
773
  machine_pools=[default_worker_machine_pool, new_workers_machine_pool],
638
774
  )
639
775
 
640
776
  run(False)
641
777
 
642
778
  mocks["OCM"].delete_machine_pool.assert_called_once_with(
643
- ocm_cluster_with_default_machine_pool.name,
779
+ rosa_cluster_with_default_machine_pool.name,
644
780
  expected_ocm_machine_pool_delete_payload,
645
781
  )
646
782
 
647
783
 
648
784
  @pytest.fixture
649
- def ocm_cluster_without_machine_pools(
650
- ocm_cluster_builder: Callable[..., ClusterV1],
785
+ def osd_cluster_without_machine_pools(
786
+ osd_cluster_builder,
651
787
  ) -> ClusterV1:
652
- return ocm_cluster_builder([])
788
+ return osd_cluster_builder([])
653
789
 
654
790
 
655
- def test_run_delete_machine_pool_fail_validation(
791
+ @pytest.fixture
792
+ def rosa_cluster_without_machine_pools(
793
+ rosa_cluster_builder,
794
+ ) -> ClusterV1:
795
+ return rosa_cluster_builder([])
796
+
797
+
798
+ def test_run_delete_machine_pool_fail_validation_for_osd_cluster(
656
799
  mocker: MockerFixture,
657
- ocm_cluster_without_machine_pools: ClusterV1,
658
- default_worker_machine_pool,
800
+ osd_cluster_without_machine_pools: ClusterV1,
801
+ default_worker_machine_pool: dict,
659
802
  ) -> None:
660
803
  setup_mocks(
661
804
  mocker,
662
- clusters=[ocm_cluster_without_machine_pools],
805
+ clusters=[osd_cluster_without_machine_pools],
663
806
  machine_pools=[default_worker_machine_pool],
664
807
  )
665
808
 
@@ -670,6 +813,76 @@ def test_run_delete_machine_pool_fail_validation(
670
813
  assert isinstance(eg.value.exceptions[0], InvalidUpdateError)
671
814
 
672
815
 
816
+ def test_run_delete_machine_pool_fail_validation_for_rosa_cluster(
817
+ mocker: MockerFixture,
818
+ rosa_cluster_without_machine_pools: ClusterV1,
819
+ default_worker_machine_pool: dict,
820
+ ) -> None:
821
+ setup_mocks(
822
+ mocker,
823
+ clusters=[rosa_cluster_without_machine_pools],
824
+ machine_pools=[default_worker_machine_pool],
825
+ )
826
+
827
+ with pytest.raises(ExceptionGroup) as eg:
828
+ run(False)
829
+
830
+ assert len(eg.value.exceptions) == 1
831
+ assert isinstance(eg.value.exceptions[0], InvalidUpdateError)
832
+
833
+
834
+ @pytest.fixture
835
+ def osd_cluster_with_new_machine_pool(
836
+ osd_cluster_builder,
837
+ new_workers_machine_pool: dict,
838
+ ) -> ClusterV1:
839
+ return osd_cluster_builder([new_workers_machine_pool])
840
+
841
+
842
+ @pytest.fixture
843
+ def rosa_cluster_with_new_machine_pool(
844
+ rosa_cluster_builder,
845
+ new_workers_machine_pool: dict,
846
+ ) -> ClusterV1:
847
+ return rosa_cluster_builder([new_workers_machine_pool])
848
+
849
+
850
+ def test_run_delete_default_machine_pool_fail_validation_for_osd_cluster(
851
+ mocker: MockerFixture,
852
+ osd_cluster_with_new_machine_pool: ClusterV1,
853
+ default_worker_machine_pool: dict,
854
+ new_workers_machine_pool: dict,
855
+ ) -> None:
856
+ setup_mocks(
857
+ mocker,
858
+ clusters=[osd_cluster_with_new_machine_pool],
859
+ machine_pools=[default_worker_machine_pool, new_workers_machine_pool],
860
+ )
861
+
862
+ with pytest.raises(ExceptionGroup) as eg:
863
+ run(False)
864
+
865
+ assert len(eg.value.exceptions) == 1
866
+ assert isinstance(eg.value.exceptions[0], InvalidUpdateError)
867
+
868
+
869
+ def test_run_delete_default_machine_pool_success_for_rosa_cluster(
870
+ mocker: MockerFixture,
871
+ rosa_cluster_with_new_machine_pool: ClusterV1,
872
+ default_worker_machine_pool: dict,
873
+ new_workers_machine_pool: dict,
874
+ ) -> None:
875
+ mocks = setup_mocks(
876
+ mocker,
877
+ clusters=[rosa_cluster_with_new_machine_pool],
878
+ machine_pools=[default_worker_machine_pool, new_workers_machine_pool],
879
+ )
880
+
881
+ run(False)
882
+
883
+ mocks["OCM"].delete_machine_pool.assert_called_once()
884
+
885
+
673
886
  @pytest.fixture
674
887
  def hypershift_cluster_builder(
675
888
  gql_class_factory: Callable[..., ClusterV1],
@@ -687,6 +900,7 @@ def hypershift_cluster_builder(
687
900
  },
688
901
  },
689
902
  "spec": {
903
+ "product": "rosa",
690
904
  "hypershift": True,
691
905
  },
692
906
  "machinePools": machine_pools,
@@ -880,7 +1094,7 @@ def existing_multiple_hypershift_node_pools_with_defaults() -> list[dict]:
880
1094
  ]
881
1095
 
882
1096
 
883
- def test_run_delete_node_pool_skip_workers_ones(
1097
+ def test_run_delete_default_node_pool(
884
1098
  mocker: MockerFixture,
885
1099
  hypershift_cluster_without_default_worker_machine_pools: ClusterV1,
886
1100
  existing_multiple_hypershift_node_pools_with_defaults: list[dict],
@@ -893,4 +1107,4 @@ def test_run_delete_node_pool_skip_workers_ones(
893
1107
 
894
1108
  run(False)
895
1109
 
896
- mocks["OCM"].delete_node_pool.assert_not_called()
1110
+ mocks["OCM"].delete_node_pool.assert_called()
@@ -140,17 +140,13 @@ def aws_account_no_state(automation_token) -> AWSAccountV1:
140
140
 
141
141
  @pytest.fixture
142
142
  def int_params() -> TerraformRepoIntegrationParams:
143
- return TerraformRepoIntegrationParams(
144
- output_file=None, validate_git=False, ignore_state_errors=False
145
- )
143
+ return TerraformRepoIntegrationParams(output_file=None, validate_git=False)
146
144
 
147
145
 
148
146
  @pytest.fixture
149
147
  def int_params_print_to_tmp(tmp_path) -> TerraformRepoIntegrationParams:
150
148
  return TerraformRepoIntegrationParams(
151
- output_file=f"{tmp_path}/tf-repo.yaml",
152
- validate_git=False,
153
- ignore_state_errors=False,
149
+ output_file=f"{tmp_path}/tf-repo.yaml", validate_git=False
154
150
  )
155
151
 
156
152
 
@@ -165,7 +161,11 @@ def test_addition_to_existing_repo(existing_repo, new_repo, int_params, state_mo
165
161
 
166
162
  integration = TerraformRepoIntegration(params=int_params)
167
163
  diff = integration.calculate_diff(
168
- existing_state=existing, desired_state=desired, dry_run=False, state=state_mock
164
+ existing_state=existing,
165
+ desired_state=desired,
166
+ dry_run=False,
167
+ state=state_mock,
168
+ recreate_state=False,
169
169
  )
170
170
 
171
171
  assert diff == [new_repo]
@@ -187,6 +187,7 @@ def test_updating_repo_ref(existing_repo, int_params, state_mock):
187
187
  desired_state=[updated_repo],
188
188
  dry_run=False,
189
189
  state=state_mock,
190
+ recreate_state=False,
190
191
  )
191
192
 
192
193
  assert diff == [updated_repo]
@@ -213,6 +214,7 @@ def test_fail_on_update_invalid_repo_params(existing_repo, int_params):
213
214
  desired_state=[updated_repo],
214
215
  dry_run=True,
215
216
  state=None,
217
+ recreate_state=False,
216
218
  )
217
219
 
218
220
 
@@ -228,6 +230,7 @@ def test_delete_repo(existing_repo, int_params, state_mock):
228
230
  desired_state=[updated_repo],
229
231
  dry_run=False,
230
232
  state=state_mock,
233
+ recreate_state=False,
231
234
  )
232
235
 
233
236
  assert diff == [updated_repo]
@@ -242,7 +245,11 @@ def test_delete_repo_without_flag(existing_repo, int_params):
242
245
 
243
246
  with pytest.raises(ParameterError):
244
247
  integration.calculate_diff(
245
- existing_state=existing, desired_state=[], dry_run=True, state=None
248
+ existing_state=existing,
249
+ desired_state=[],
250
+ dry_run=True,
251
+ state=None,
252
+ recreate_state=False,
246
253
  )
247
254
 
248
255
 
@@ -303,6 +310,7 @@ def test_update_repo_state(int_params, existing_repo, state_mock):
303
310
  desired_state=desired_state,
304
311
  dry_run=False,
305
312
  state=state_mock,
313
+ recreate_state=False,
306
314
  )
307
315
 
308
316
  state_mock.add.assert_called_once_with(
@@ -327,6 +335,7 @@ def test_output_correct_statefile(
327
335
  desired_state=desired_state,
328
336
  dry_run=True,
329
337
  state=state_mock,
338
+ recreate_state=False,
330
339
  )
331
340
 
332
341
  assert diff
@@ -353,6 +362,7 @@ def test_output_correct_no_statefile(
353
362
  desired_state=desired_state,
354
363
  dry_run=True,
355
364
  state=state_mock,
365
+ recreate_state=False,
356
366
  )
357
367
 
358
368
  assert diff
@@ -371,7 +381,11 @@ def test_fail_on_multiple_repos_dry_run(int_params, existing_repo, new_repo):
371
381
 
372
382
  with pytest.raises(Exception):
373
383
  integration.calculate_diff(
374
- existing_state=[], desired_state=desired_state, dry_run=True, state=None
384
+ existing_state=[],
385
+ desired_state=desired_state,
386
+ dry_run=True,
387
+ state=None,
388
+ recreate_state=False,
375
389
  )
376
390
 
377
391
 
@@ -381,7 +395,11 @@ def test_succeed_on_multiple_repos_non_dry_run(int_params, existing_repo, new_re
381
395
  desired_state = [existing_repo, new_repo]
382
396
 
383
397
  diff = integration.calculate_diff(
384
- existing_state=[], desired_state=desired_state, dry_run=False, state=None
398
+ existing_state=[],
399
+ desired_state=desired_state,
400
+ dry_run=False,
401
+ state=None,
402
+ recreate_state=False,
385
403
  )
386
404
 
387
405
  assert diff
@@ -397,7 +415,11 @@ def test_no_op_succeeds(int_params, existing_repo):
397
415
  state = [existing_repo]
398
416
 
399
417
  diff = integration.calculate_diff(
400
- existing_state=state, desired_state=state, dry_run=True, state=None
418
+ existing_state=state,
419
+ desired_state=state,
420
+ dry_run=True,
421
+ state=None,
422
+ recreate_state=False,
401
423
  )
402
424
 
403
425
  assert diff is None
@@ -7,7 +7,16 @@ def make_semver(major: int, minor: int, patch: int) -> str:
7
7
  return str(semver.VersionInfo(major=major, minor=minor, patch=patch))
8
8
 
9
9
 
10
- def parse_semver(version: str) -> semver.VersionInfo:
10
+ def parse_semver(
11
+ version: str, optional_minor_and_patch: bool = False
12
+ ) -> semver.VersionInfo:
13
+ if optional_minor_and_patch:
14
+ # semver3 supports optional minor and patch.
15
+ # until we upgrade to semver3, we support this by adding a default minor and/or patch
16
+ if "." not in version:
17
+ version = f"{version}.0.0"
18
+ elif version.count(".") == 1:
19
+ version = f"{version}.0"
11
20
  return semver.VersionInfo.parse(version)
12
21
 
13
22