qontract-reconcile 0.10.1rc596__py3-none-any.whl → 0.10.1rc597__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.1rc596
3
+ Version: 0.10.1rc597
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
@@ -9,7 +9,7 @@ reconcile/aws_iam_password_reset.py,sha256=NwErtrqgBiXr7eGCAHdtGGOx0S7-4JnSc29Ie
9
9
  reconcile/aws_support_cases_sos.py,sha256=Jk6_XjDeJSYxgRGqcEAOcynt9qJF2r5HPIPcSKmoBv8,2974
10
10
  reconcile/blackbox_exporter_endpoint_monitoring.py,sha256=W_VJagnsJR1v5oqjlI3RJJE0_nhtJ0m81RS8zWA5u5c,3538
11
11
  reconcile/checkpoint.py,sha256=R2WFXUXLTB4sWMi4GeA4eegsuf_1-Q4vH8M0Toh3Ij4,5036
12
- reconcile/cli.py,sha256=3THrA_8GdYIWiB6lwq8WZ8udxS6WALPB61xZZ40-puQ,89871
12
+ reconcile/cli.py,sha256=eZfqiWuI1r_BEx_VzxTPYrqqs1PHiQPAnEt00qZ3vNo,91518
13
13
  reconcile/closedbox_endpoint_monitoring_base.py,sha256=SMhkcQqprWvThrIJa3U_3uh5w1h-alleW1QnCJFY4Qw,4909
14
14
  reconcile/cluster_deployment_mapper.py,sha256=2Ah-nu-Mdig0pjuiZl_XLrmVAjYzFjORR3dMlCgkmw0,2352
15
15
  reconcile/dashdotdb_base.py,sha256=a5aPLVxyqPSbjdB0Ty-uliOtxwvEbbEljHJKxdK3-Zk,4813
@@ -120,7 +120,7 @@ reconcile/vpc_peerings_validator.py,sha256=Kv22HJVlTW9l9GB2eXwjPWqdDbr_VuvQBNPtt
120
120
  reconcile/aus/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
121
121
  reconcile/aus/advanced_upgrade_service.py,sha256=PkVcXBMrveW5euvqjEBO4e5-9KDb_6hszLI2GrWpx2w,21378
122
122
  reconcile/aus/aus_label_source.py,sha256=qoP8Fgxuu1tCuhG6ixCWve7Ll-KD6a79E2uLAmC0ifw,4184
123
- reconcile/aus/base.py,sha256=Qdktz7W7J9TWVCYJZHwyKoIf4sVI7W9o_WwzsOBxLfQ,44912
123
+ reconcile/aus/base.py,sha256=ESycSlsxNGjpy1v2G2NXB7WB6NMYrXCaN4kSVMicRKk,44462
124
124
  reconcile/aus/cluster_version_data.py,sha256=j4UyEBi5mQuvPq5Lo7a_L_0blxvH790wJV07uAiikFU,7126
125
125
  reconcile/aus/metrics.py,sha256=fIew-rzi_kYuI5Gxn3-4bQVIr2oNibiKPyGnhB-xKU4,3538
126
126
  reconcile/aus/models.py,sha256=muBmbovxYtSNLFrTLVRcJYZ4dx6JLh8n3Q1-DjWJOHM,7098
@@ -128,6 +128,11 @@ reconcile/aus/ocm_addons_upgrade_scheduler_org.py,sha256=fshslI27hrqT40qrVsVOQaW
128
128
  reconcile/aus/ocm_upgrade_scheduler.py,sha256=7cK2SakCFkl5EdnqUEAYdUo4pUnnf-SsUR10uytAGyE,3058
129
129
  reconcile/aus/ocm_upgrade_scheduler_org.py,sha256=OBgE5mnVdQQV4tMH0AE2V_PDt9Gy6d-LyuPceqjORts,2331
130
130
  reconcile/aus/upgrades.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
131
+ reconcile/aus/version_gate_approver.py,sha256=VJ6Lrzkapr14QvulzIsE-sTfawIDcyn9UGOC3pIM1gs,6895
132
+ reconcile/aus/version_gates/__init__.py,sha256=fWx-IvS132Wpa4gWNIuoNvFwqhkuUuFWYWq5-xiLklI,362
133
+ reconcile/aus/version_gates/handler.py,sha256=S_isQPYHbG4DERiUEvQBZ6ngiFX3uMmATA-Q_eNKmFk,839
134
+ reconcile/aus/version_gates/ocp_gate_handler.py,sha256=RW1ppDaCZXVegV9AzzqYXxDUu_Z_7d43Z5h2Pk_piKc,716
135
+ reconcile/aus/version_gates/sts_version_gate_handler.py,sha256=PhJ7yBh2q-rv9CJcfFhc0H11nyDyG7NAryNS3F74xdY,3697
131
136
  reconcile/aws_ami_cleanup/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
132
137
  reconcile/aws_ami_cleanup/integration.py,sha256=IW95cpMj2P5ffs-AxsR_TDQCJnYFBhLIfP2de7dz_8A,10109
133
138
  reconcile/aws_cloudwatch_log_retention/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -535,7 +540,7 @@ reconcile/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
535
540
  reconcile/utils/aggregated_list.py,sha256=pkYoBj7WwmaNgEefETqEOFTnQMcUzHE3mdsVdzGYj60,3372
536
541
  reconcile/utils/amtool.py,sha256=JV5-to_e_FaIcvJWTKYA9d6L3LwzwijM0MjUWn83eD4,2204
537
542
  reconcile/utils/aws_api.py,sha256=Wy040GBQ3HrWmtxe1QAx3zl1I3phVDVjXEx_OITEcOw,69191
538
- reconcile/utils/aws_helper.py,sha256=E8NHkStoHRmvLVjRll2f5kGtU3i3f7ekp5V6nrn7B_M,1691
543
+ reconcile/utils/aws_helper.py,sha256=6Nfgsz0aQ97LBAJ0JBRdnPaFTAkEBSqXvCH6_pVIWdw,2006
539
544
  reconcile/utils/binary.py,sha256=3IBnwjKakHM367skPPvG6yVSQYjKt5muQlFNdoa63DU,2352
540
545
  reconcile/utils/config.py,sha256=aId5zrPjM_84u_T4yTRE_Psu3zo5-5_JCR6_7Wgv5UQ,990
541
546
  reconcile/utils/constants.py,sha256=pOUd97bqZdsAu5RWJ8NUs9cwCY7K9y0eW9VVeJ4fZIU,138
@@ -593,7 +598,7 @@ reconcile/utils/raw_github_api.py,sha256=ZHC-SZuAyRe1zaMoOU7Krt1-zecDxENd9c_NzQY
593
598
  reconcile/utils/repo_owners.py,sha256=j-pUjc9PuDzq7KpjNLpnhqfU8tUG4nj2WMhFp4ick7g,6629
594
599
  reconcile/utils/ruamel.py,sha256=FzL4_L0FnMOUZmgThrZSMJs5MTdXwiy-E9MZWfk8bh8,397
595
600
  reconcile/utils/secret_reader.py,sha256=2DeYAAQFjUULEKlLw3UDAUoND6gbqvCh9uKPtlc-0us,10403
596
- reconcile/utils/semver_helper.py,sha256=dp86KxjlOc8LHzawMvbxRfZamv7KU7b2SVnZQL-Xg6U,1142
601
+ reconcile/utils/semver_helper.py,sha256=-WfPOMSA2v1h7hT3PwVf-Htg7wOsoKlQC1JdmDX2Ars,1268
597
602
  reconcile/utils/sharding.py,sha256=gkYf0lD3IUKQPEmdRJZ70mdDT1c9qWjbdP7evRsUis4,839
598
603
  reconcile/utils/slack_api.py,sha256=OPmzU6L9rJx2XXDlZkMlxLjOWu17yC-fVCoUItzQrXw,16295
599
604
  reconcile/utils/smtp_client.py,sha256=gJNbBQJpAt5PX4t_TaeNHsXM8vt50bFgndml6yK2b5o,2800
@@ -642,7 +647,7 @@ reconcile/utils/mr/ocm_upgrade_scheduler_org_updates.py,sha256=RzEKRT_BhvB2ud9py
642
647
  reconcile/utils/mr/user_maintenance.py,sha256=cHPBn8zrReWLHalyk-EFdkFJe9zjVjRoZhT4t2zZfGE,3956
643
648
  reconcile/utils/ocm/__init__.py,sha256=5Pcf5cyftDWT5XRi1EzvNklOVxGplJi-v12HN3TDarc,57
644
649
  reconcile/utils/ocm/addons.py,sha256=8wVrt16i69KkXq1fQByVheSQRhrRELbuOHb7Tz9bKT0,1675
645
- reconcile/utils/ocm/base.py,sha256=7jm37vMoIIaSnOZe02_6VZFIh0IDa-VVIqfR1FoddZQ,11872
650
+ reconcile/utils/ocm/base.py,sha256=GclZtCrPkPJmGP9HHvqIlV-8VXSKuaQTQJkA2pklN60,13817
646
651
  reconcile/utils/ocm/cluster_groups.py,sha256=F8oqVqN_4QUnGL0K61zZhoYIzJeP57EcmZpwmoV0mr4,1751
647
652
  reconcile/utils/ocm/clusters.py,sha256=Q6g5kGSNfxZUZ56LPFAYjOz8xJ2c6QG76V78GvyLxB0,7448
648
653
  reconcile/utils/ocm/identity_providers.py,sha256=dKed09N8iWmn39tI_MpwgVe47x23eLsknGbjMUxtwr4,2175
@@ -693,8 +698,8 @@ tools/test/test_app_interface_metrics_exporter.py,sha256=SX7qL3D1SIRKFo95FoQztvf
693
698
  tools/test/test_qontract_cli.py,sha256=OvalpVRfY4pNmpMaWHHYqBjV68b1eGQjX8SCyTAXb1w,3501
694
699
  tools/test/test_sd_app_sre_alert_report.py,sha256=v363r9zM7__0kR5K6mvJoGFcM9BvE33fWAayrqkpojA,2116
695
700
  tools/test/test_sre_checkpoints.py,sha256=SKqPPTl9ua0RFdSSofnoQX-JZE6dFLO3LRhfQzqtfh8,2607
696
- qontract_reconcile-0.10.1rc596.dist-info/METADATA,sha256=48NeYqXGwOl1UWs78VYRsxQpABH6M_hfv5xHUEO3cFw,2349
697
- qontract_reconcile-0.10.1rc596.dist-info/WHEEL,sha256=oiQVh_5PnQM0E3gPdiz09WCNmwiHDMaGer_elqB3coM,92
698
- qontract_reconcile-0.10.1rc596.dist-info/entry_points.txt,sha256=rTjAv28I_CHLM8ID3OPqMI_suoQ9s7tFbim4aYjn9kk,376
699
- qontract_reconcile-0.10.1rc596.dist-info/top_level.txt,sha256=l5ISPoXzt0SdR4jVdkfa7RPSKNc8zAHYWAnR-Dw8Ey8,24
700
- qontract_reconcile-0.10.1rc596.dist-info/RECORD,,
701
+ qontract_reconcile-0.10.1rc597.dist-info/METADATA,sha256=d8mSQupiOAr8Iya1HYE2kxpuoqxKJv1EYTjBdA4WdOs,2349
702
+ qontract_reconcile-0.10.1rc597.dist-info/WHEEL,sha256=oiQVh_5PnQM0E3gPdiz09WCNmwiHDMaGer_elqB3coM,92
703
+ qontract_reconcile-0.10.1rc597.dist-info/entry_points.txt,sha256=rTjAv28I_CHLM8ID3OPqMI_suoQ9s7tFbim4aYjn9kk,376
704
+ qontract_reconcile-0.10.1rc597.dist-info/top_level.txt,sha256=l5ISPoXzt0SdR4jVdkfa7RPSKNc8zAHYWAnR-Dw8Ey8,24
705
+ qontract_reconcile-0.10.1rc597.dist-info/RECORD,,
reconcile/aus/base.py CHANGED
@@ -19,7 +19,7 @@ from typing import (
19
19
 
20
20
  import semver
21
21
  from croniter import croniter
22
- from pydantic import BaseModel
22
+ from pydantic import BaseModel, Extra
23
23
  from semver import VersionInfo
24
24
 
25
25
  from reconcile.aus.cluster_version_data import (
@@ -44,6 +44,7 @@ from reconcile.aus.models import (
44
44
  OrganizationUpgradeSpec,
45
45
  Sector,
46
46
  )
47
+ from reconcile.aus.version_gates import HANDLERS
47
48
  from reconcile.gql_definitions.advanced_upgrade_service.aus_organization import (
48
49
  query as aus_organizations_query,
49
50
  )
@@ -71,7 +72,6 @@ from reconcile.utils.ocm.upgrades import (
71
72
  create_control_plane_upgrade_policy,
72
73
  create_node_pool_upgrade_policy,
73
74
  create_upgrade_policy,
74
- create_version_agreement,
75
75
  delete_addon_upgrade_policy,
76
76
  delete_control_plane_upgrade_policy,
77
77
  delete_upgrade_policy,
@@ -88,6 +88,7 @@ from reconcile.utils.runtime.integration import (
88
88
  QontractReconcileIntegration,
89
89
  )
90
90
  from reconcile.utils.semver_helper import (
91
+ get_version_prefix,
91
92
  parse_semver,
92
93
  sort_versions,
93
94
  )
@@ -299,21 +300,6 @@ class AdvancedUpgradeSchedulerBaseIntegration(
299
300
  )
300
301
 
301
302
 
302
- class GateAgreement(BaseModel):
303
- gate: OCMVersionGate
304
-
305
- def create(self, ocm_api: OCMBaseClient, cluster: OCMCluster) -> None:
306
- logging.info(
307
- f"create agreement for gate {self.gate.id} on cluster {cluster.name} (id={cluster.id})"
308
- )
309
- agreement = create_version_agreement(ocm_api, self.gate.id, cluster.id)
310
- if agreement.get("version_gate") is None:
311
- logging.error(
312
- "Unexpected response while creating version "
313
- f"agreement with id {self.gate.id} for cluster {cluster.name} (id={cluster.id})"
314
- )
315
-
316
-
317
303
  class RemainingSoakDayMetricsBuilder(Protocol):
318
304
  def __call__(
319
305
  self, cluster_uuid: str, soaking_version: str
@@ -467,18 +453,12 @@ class NodePoolUpgradePolicy(AbstractUpgradePolicy):
467
453
  return f"node pool upgrade policy - {remove_none_values_from_dict(details)}"
468
454
 
469
455
 
470
- class UpgradePolicyHandler(BaseModel):
456
+ class UpgradePolicyHandler(BaseModel, extra=Extra.forbid):
471
457
  """Class to handle upgrade policy actions"""
472
458
 
473
459
  action: str
474
460
  policy: AbstractUpgradePolicy
475
461
 
476
- gates_to_agree: Optional[list[GateAgreement]]
477
-
478
- def _create_gate_agreements(self, ocm_api: OCMBaseClient) -> None:
479
- for gate in self.gates_to_agree or []:
480
- gate.create(ocm_api, self.policy.cluster)
481
-
482
462
  def act(self, dry_run: bool, ocm_api: OCMBaseClient) -> None:
483
463
  logging.info(f"{self.action} {self.policy.summarize()}")
484
464
  if dry_run:
@@ -489,7 +469,6 @@ class UpgradePolicyHandler(BaseModel):
489
469
  elif self.action == "delete":
490
470
  self.policy.delete(ocm_api)
491
471
  elif self.action == "create":
492
- self._create_gate_agreements(ocm_api)
493
472
  self.policy.create(ocm_api)
494
473
 
495
474
 
@@ -727,10 +706,34 @@ def gates_for_minor_version(
727
706
  return [g for g in gates if g.version_raw_id_prefix == target_version_prefix]
728
707
 
729
708
 
709
+ def is_gate_applicable_to_cluster(gate: OCMVersionGate, cluster: OCMCluster) -> bool:
710
+ # check that the cluster has an upgrade path that crosses the gate version
711
+ minor_version_upgrade_paths = {
712
+ get_version_prefix(version) for version in cluster.available_upgrades()
713
+ }
714
+ if gate.version_raw_id_prefix not in minor_version_upgrade_paths:
715
+ return False
716
+
717
+ # consider only gates after the clusters current minor version
718
+ # OCM onls supports creating gate agreements for later minor versions than the
719
+ # current cluster version
720
+ if not semver.match(
721
+ f"{cluster.minor_version()}.0", f"<{gate.version_raw_id_prefix}.0"
722
+ ):
723
+ return False
724
+
725
+ # check the handler for the gate type if it is responsible for this kind
726
+ # of cluster
727
+ handler = HANDLERS.get(gate.label)
728
+ if handler:
729
+ return handler.gate_applicable_to_cluster(cluster)
730
+ return False
731
+
732
+
730
733
  def gates_to_agree(
731
734
  gates: list[OCMVersionGate],
732
735
  cluster: OCMCluster,
733
- ocm_api: OCMBaseClient,
736
+ acked_gate_ids: set[str],
734
737
  ) -> list[OCMVersionGate]:
735
738
  """Check via OCM if a version is agreed
736
739
 
@@ -742,36 +745,13 @@ def gates_to_agree(
742
745
  Returns:
743
746
  list[OCMVersionGate]: list of gates a cluster has not agreed on yet
744
747
  """
745
- applicable_gates = [
746
- g
747
- for g in gates
748
- # todo: sts version gates need special handling - https://issues.redhat.com/browse/APPSRE-7949
749
- # until this is solved, we can't do automated upgrades for STS clusters that cross a version gate
750
- # once we have proper and secure handling get gate agreements for STS clusters, we can use this condition:
751
- # `and (not g.sts_only or g.sts_only == cluster.is_sts())`
752
- if not g.sts_only
753
- # consider only gates after the clusters current minor version
754
- # OCM onls supports creating gate agreements for later minor versions than the
755
- # current cluster version
756
- and semver.match(
757
- f"{cluster.minor_version()}.0", f"<{g.version_raw_id_prefix}.0"
758
- )
759
- ]
748
+ applicable_gates = [g for g in gates if is_gate_applicable_to_cluster(g, cluster)]
760
749
 
761
750
  if applicable_gates:
762
- current_agreements = {
763
- agreement["version_gate"]["id"]
764
- for agreement in get_version_agreement(ocm_api, cluster.id)
765
- }
766
- return [gate for gate in applicable_gates if gate.id not in current_agreements]
751
+ return [gate for gate in applicable_gates if gate.id not in acked_gate_ids]
767
752
  return []
768
753
 
769
754
 
770
- def get_version_prefix(version: str) -> str:
771
- semver = parse_semver(version)
772
- return f"{semver.major}.{semver.minor}"
773
-
774
-
775
755
  def upgradeable_version(
776
756
  spec: ClusterUpgradeSpec,
777
757
  version_data: VersionData,
@@ -1024,18 +1004,24 @@ def calculate_diff(
1024
1004
  "Skip creation of an upgrade policy."
1025
1005
  )
1026
1006
  continue
1007
+ gates = gates_to_agree(
1008
+ gates=minor_version_gates,
1009
+ cluster=spec.cluster,
1010
+ acked_gate_ids={
1011
+ agreement["version_gate"]["id"]
1012
+ for agreement in get_version_agreement(ocm_api, spec.cluster.id)
1013
+ },
1014
+ )
1015
+ if gates:
1016
+ gate_ids = [gate.id for gate in gates]
1017
+ logging.debug(
1018
+ f"[{spec.org.org_id}/{spec.org.name}/{spec.cluster.name}] found gates for {target_version_prefix} - {gate_ids} "
1019
+ "Skip creation of an upgrade policy until all of them have been acked by the version-gate-approver integration or a user."
1020
+ )
1027
1021
  diffs.append(
1028
1022
  UpgradePolicyHandler(
1029
1023
  action="create",
1030
1024
  policy=_create_upgrade_policy(next_schedule, spec, version),
1031
- gates_to_agree=[
1032
- GateAgreement(gate=g)
1033
- for g in gates_to_agree(
1034
- minor_version_gates,
1035
- spec.cluster,
1036
- ocm_api,
1037
- )
1038
- ],
1039
1025
  )
1040
1026
  )
1041
1027
  set_mutex(locked, spec.cluster.id, spec.upgrade_policy.conditions.mutexes)
@@ -0,0 +1,204 @@
1
+ import logging
2
+ from typing import (
3
+ Callable,
4
+ Iterable,
5
+ Optional,
6
+ )
7
+
8
+ import semver
9
+
10
+ from reconcile.aus.advanced_upgrade_service import aus_label_key
11
+ from reconcile.aus.base import gates_to_agree, get_orgs_for_environment
12
+ from reconcile.aus.version_gates import ocp_gate_handler, sts_version_gate_handler
13
+ from reconcile.aus.version_gates.handler import GateHandler
14
+ from reconcile.gql_definitions.common.ocm_environments import (
15
+ query as ocm_environment_query,
16
+ )
17
+ from reconcile.utils import gql
18
+ from reconcile.utils.grouping import group_by
19
+ from reconcile.utils.jobcontroller.controller import (
20
+ build_job_controller,
21
+ )
22
+ from reconcile.utils.ocm.base import (
23
+ ClusterDetails,
24
+ LabelContainer,
25
+ OCMCluster,
26
+ OCMVersionGate,
27
+ )
28
+ from reconcile.utils.ocm.clusters import discover_clusters_by_labels
29
+ from reconcile.utils.ocm.search_filters import Filter
30
+ from reconcile.utils.ocm.upgrades import (
31
+ create_version_agreement,
32
+ get_version_agreement,
33
+ get_version_gates,
34
+ )
35
+ from reconcile.utils.ocm_base_client import (
36
+ OCMBaseClient,
37
+ init_ocm_base_client,
38
+ init_ocm_base_client_for_org,
39
+ )
40
+ from reconcile.utils.runtime.integration import (
41
+ PydanticRunParams,
42
+ QontractReconcileIntegration,
43
+ )
44
+
45
+ QONTRACT_INTEGRATION = "version-gate-approver"
46
+ QONTRACT_INTEGRATION_VERSION = semver.format_version(0, 1, 0)
47
+
48
+
49
+ class VersionGateApproverParams(PydanticRunParams):
50
+ job_controller_cluster: str
51
+ job_controller_namespace: str
52
+ rosa_job_service_account: str
53
+ rosa_role: str
54
+ rosa_job_image: Optional[str] = None
55
+
56
+
57
+ class VersionGateApprover(QontractReconcileIntegration[VersionGateApproverParams]):
58
+ @property
59
+ def name(self) -> str:
60
+ return QONTRACT_INTEGRATION
61
+
62
+ def initialize_handlers(self, query_func: Callable) -> None:
63
+ self.handlers: dict[str, GateHandler] = {
64
+ sts_version_gate_handler.GATE_LABEL: sts_version_gate_handler.STSGateHandler(
65
+ job_controller=build_job_controller(
66
+ integration=QONTRACT_INTEGRATION,
67
+ integration_version=QONTRACT_INTEGRATION_VERSION,
68
+ cluster=self.params.job_controller_cluster,
69
+ namespace=self.params.job_controller_namespace,
70
+ secret_reader=self.secret_reader,
71
+ dry_run=False,
72
+ ),
73
+ aws_iam_role=self.params.rosa_role,
74
+ rosa_job_service_account=self.params.rosa_job_service_account,
75
+ rosa_job_image=self.params.rosa_job_image,
76
+ ),
77
+ ocp_gate_handler.GATE_LABEL: ocp_gate_handler.OCPGateHandler(),
78
+ }
79
+
80
+ def run(self, dry_run: bool) -> None:
81
+ gql_api = gql.get_api()
82
+ self.initialize_handlers(gql_api.query)
83
+ environments = ocm_environment_query(gql_api.query).environments
84
+ ocm_apis = {
85
+ env.name: init_ocm_base_client(env, self.secret_reader)
86
+ for env in environments
87
+ }
88
+ for env in environments:
89
+ self.process_environment(
90
+ ocm_env_name=env.name,
91
+ ocm_api=ocm_apis[env.name],
92
+ query_func=gql_api.query,
93
+ dry_run=dry_run,
94
+ )
95
+
96
+ def process_environment(
97
+ self,
98
+ ocm_env_name: str,
99
+ ocm_api: OCMBaseClient,
100
+ query_func: Callable,
101
+ dry_run: bool,
102
+ ) -> None:
103
+ """
104
+ Find all clusters with AUS labels in the organization and process them
105
+ org by org.
106
+ """
107
+ # lookup clusters
108
+ clusters = discover_clusters_by_labels(
109
+ ocm_api=ocm_api,
110
+ label_filter=Filter().like("key", aus_label_key("%")),
111
+ )
112
+ clusters_by_org_id = group_by(clusters, lambda c: c.organization_id)
113
+
114
+ # lookup version gates
115
+ gates = get_version_gates(ocm_api)
116
+
117
+ # lookup organization metadata
118
+ organizations = get_orgs_for_environment(
119
+ integration=QONTRACT_INTEGRATION,
120
+ ocm_env_name=ocm_env_name,
121
+ query_func=query_func,
122
+ ocm_organization_ids=set(clusters_by_org_id.keys()),
123
+ )
124
+
125
+ for org in organizations:
126
+ ocm_org_api = init_ocm_base_client_for_org(org, self.secret_reader)
127
+ self.process_organization(
128
+ clusters=clusters_by_org_id.get(org.org_id, []),
129
+ gates=gates,
130
+ ocm_api=ocm_org_api,
131
+ dry_run=dry_run,
132
+ )
133
+
134
+ def process_organization(
135
+ self,
136
+ clusters: Iterable[ClusterDetails],
137
+ gates: list[OCMVersionGate],
138
+ ocm_api: OCMBaseClient,
139
+ dry_run: bool,
140
+ ) -> None:
141
+ """
142
+ Process all clusters in an organization.
143
+ """
144
+ for cluster in clusters:
145
+ unacked_gates = gates_to_agree(
146
+ cluster=cluster.ocm_cluster,
147
+ gates=gates,
148
+ acked_gate_ids={
149
+ agreement["version_gate"]["id"]
150
+ for agreement in get_version_agreement(
151
+ ocm_api, cluster.ocm_cluster.id
152
+ )
153
+ },
154
+ )
155
+ if not unacked_gates:
156
+ continue
157
+ self.process_cluster(
158
+ cluster=cluster.ocm_cluster,
159
+ enabled_gate_handlers=get_enabled_gate_handlers(cluster.labels),
160
+ gates=unacked_gates,
161
+ ocm_api=ocm_api,
162
+ ocm_org_id=cluster.organization_id,
163
+ dry_run=dry_run,
164
+ )
165
+
166
+ def process_cluster(
167
+ self,
168
+ cluster: OCMCluster,
169
+ enabled_gate_handlers: set[str],
170
+ gates: list[OCMVersionGate],
171
+ ocm_api: OCMBaseClient,
172
+ ocm_org_id: str,
173
+ dry_run: bool,
174
+ ) -> None:
175
+ """
176
+ Process all unacknowledged gates for a cluster.
177
+ """
178
+ for gate in gates:
179
+ if gate.label in self.handlers and gate.label not in enabled_gate_handlers:
180
+ continue
181
+ success = self.handlers[gate.label].handle(
182
+ ocm_api=ocm_api,
183
+ ocm_org_id=ocm_org_id,
184
+ cluster=cluster,
185
+ gate=gate,
186
+ dry_run=dry_run,
187
+ )
188
+ if success and not dry_run:
189
+ create_version_agreement(ocm_api, gate.id, cluster.id)
190
+ elif not success:
191
+ logging.error(
192
+ f"Failed to handle gate {gate.id} for cluster {cluster.name}"
193
+ )
194
+
195
+
196
+ def get_enabled_gate_handlers(labels: LabelContainer) -> set[str]:
197
+ """
198
+ Get the set of enabled gate handlers from the labels. Default to the OCP
199
+ gate to keep backwards compatibility (for now).
200
+ """
201
+ handler_csv = labels.get_label_value(aus_label_key("version-gate-approvals"))
202
+ if not handler_csv:
203
+ return {ocp_gate_handler.GATE_LABEL}
204
+ return set(handler_csv.split(","))
@@ -0,0 +1,9 @@
1
+ from typing import Type
2
+
3
+ from reconcile.aus.version_gates import ocp_gate_handler, sts_version_gate_handler
4
+ from reconcile.aus.version_gates.handler import GateHandler
5
+
6
+ HANDLERS: dict[str, Type[GateHandler]] = {
7
+ ocp_gate_handler.GATE_LABEL: ocp_gate_handler.OCPGateHandler,
8
+ sts_version_gate_handler.GATE_LABEL: sts_version_gate_handler.STSGateHandler,
9
+ }
@@ -0,0 +1,33 @@
1
+ from abc import ABC, abstractmethod
2
+
3
+ from reconcile.utils.ocm.base import OCMCluster, OCMVersionGate
4
+ from reconcile.utils.ocm_base_client import OCMBaseClient
5
+
6
+
7
+ class GateHandler(ABC):
8
+ """
9
+ A protocol for version gate handlers.
10
+ """
11
+
12
+ @staticmethod
13
+ @abstractmethod
14
+ def gate_applicable_to_cluster(cluster: OCMCluster) -> bool:
15
+ """
16
+ Check if the gate represented by this handler is applicable for the given cluster.
17
+ """
18
+ ...
19
+
20
+ @abstractmethod
21
+ def handle(
22
+ self,
23
+ ocm_api: OCMBaseClient,
24
+ ocm_org_id: str,
25
+ cluster: OCMCluster,
26
+ gate: OCMVersionGate,
27
+ dry_run: bool,
28
+ ) -> bool:
29
+ """
30
+ Take all necessary actions required by a version gate.
31
+ If successful, return True. Otherwise, return False.
32
+ """
33
+ ...
@@ -0,0 +1,26 @@
1
+ from reconcile.aus.version_gates.handler import GateHandler
2
+ from reconcile.utils.ocm.base import OCMCluster, OCMVersionGate
3
+ from reconcile.utils.ocm_base_client import OCMBaseClient
4
+
5
+ GATE_LABEL = "api.openshift.com/gate-ocp"
6
+
7
+
8
+ class OCPGateHandler(GateHandler):
9
+ """
10
+ Right now we just ack all gate-ocp gates...
11
+ We could do better in the future, e.g. inspecting insights findings on the cluster
12
+ """
13
+
14
+ @staticmethod
15
+ def gate_applicable_to_cluster(_: OCMCluster) -> bool:
16
+ return True
17
+
18
+ def handle(
19
+ self,
20
+ ocm_api: OCMBaseClient,
21
+ ocm_org_id: str,
22
+ cluster: OCMCluster,
23
+ gate: OCMVersionGate,
24
+ dry_run: bool,
25
+ ) -> bool:
26
+ return True
@@ -0,0 +1,101 @@
1
+ import logging
2
+ from typing import Optional
3
+
4
+ from reconcile.aus.version_gates.handler import GateHandler
5
+ from reconcile.utils.jobcontroller.controller import K8sJobController
6
+ from reconcile.utils.ocm.base import OCMCluster, OCMVersionGate
7
+ from reconcile.utils.ocm_base_client import OCMBaseClient
8
+ from reconcile.utils.rosa.rosa_cli import RosaCliException
9
+ from reconcile.utils.rosa.session import RosaSession
10
+
11
+ GATE_LABEL = "api.openshift.com/gate-sts"
12
+
13
+
14
+ class STSGateHandler(GateHandler):
15
+ def __init__(
16
+ self,
17
+ job_controller: K8sJobController,
18
+ aws_iam_role: str,
19
+ rosa_job_service_account: Optional[str] = None,
20
+ rosa_job_image: Optional[str] = None,
21
+ ) -> None:
22
+ self.job_controller = job_controller
23
+ self.aws_iam_role = aws_iam_role
24
+ self.rosa_job_image = rosa_job_image
25
+ self.rosa_job_service_account = rosa_job_service_account
26
+
27
+ @staticmethod
28
+ def gate_applicable_to_cluster(cluster: OCMCluster) -> bool:
29
+ """
30
+ The STS Gate is applicable to all clusters with STS enabled.
31
+ This could potentially also be OSD STS clusters. While this handler
32
+ does not handle OSD clusters as of now, it is still important that
33
+ we report the STS gate to be applicable to OSD STS clusters.
34
+ """
35
+ return cluster.is_sts()
36
+
37
+ def handle(
38
+ self,
39
+ ocm_api: OCMBaseClient,
40
+ ocm_org_id: str,
41
+ cluster: OCMCluster,
42
+ gate: OCMVersionGate,
43
+ dry_run: bool,
44
+ ) -> bool:
45
+ if (
46
+ not cluster.id
47
+ or not cluster.aws
48
+ or not cluster.aws.sts
49
+ or not cluster.is_sts()
50
+ ):
51
+ # checked already but mypy :/
52
+ return False
53
+
54
+ if cluster.is_rosa_hypershift():
55
+ # thanks to hypershift managed policies, there is nothing to do for us here
56
+ # returning True will ack the version gate
57
+ return True
58
+ if not cluster.is_rosa_classic():
59
+ # we manage roels only for rosa classic clusters
60
+ # returning here will prevent OSD STS clusters to be handled right now
61
+ logging.error(
62
+ f"Cluster {cluster.id} is not a ROSA cluster. "
63
+ "STS version gates are only handled for ROSA classic clusters."
64
+ )
65
+ return False
66
+
67
+ rosa = RosaSession(
68
+ aws_account_id=cluster.aws.aws_account_id,
69
+ aws_region=cluster.region.id,
70
+ aws_iam_role=self.aws_iam_role,
71
+ ocm_org_id=ocm_org_id,
72
+ ocm_api=ocm_api,
73
+ job_controller=self.job_controller,
74
+ image=self.rosa_job_image,
75
+ service_account=self.rosa_job_service_account,
76
+ )
77
+
78
+ try:
79
+ # account role handling
80
+ account_role_prefix = cluster.aws.account_role_prefix
81
+ if not account_role_prefix:
82
+ raise Exception(
83
+ f"Can't upgrade account roles. Cluster {cluster.name} does not define spec.aws.account_role_prefix"
84
+ )
85
+ rosa.upgrade_account_roles(
86
+ role_prefix=account_role_prefix,
87
+ minor_version=gate.version_raw_id_prefix,
88
+ channel_group=cluster.version.channel_group,
89
+ dry_run=dry_run,
90
+ )
91
+
92
+ # operator role handling
93
+ rosa.upgrade_operator_roles(
94
+ cluster_id=cluster.id,
95
+ dry_run=dry_run,
96
+ )
97
+ except RosaCliException as e:
98
+ logging.error(f"Failed to upgrade roles for cluster {cluster.name}: {e}")
99
+ e.write_logs_to_logger(logging.error)
100
+ return False
101
+ return True
reconcile/cli.py CHANGED
@@ -2448,6 +2448,65 @@ def advanced_upgrade_scheduler(
2448
2448
  )
2449
2449
 
2450
2450
 
2451
+ @integration.command(short_help="Approves OCM cluster upgrade version gates.")
2452
+ @click.option(
2453
+ "--job-controller-cluster",
2454
+ help="The cluster holding the job-controller namepsace",
2455
+ required=True,
2456
+ envvar="JOB_CONTROLLER_CLUSTER",
2457
+ )
2458
+ @click.option(
2459
+ "--job-controller-namespace",
2460
+ help="The namespace used for ROSA jobs",
2461
+ required=True,
2462
+ envvar="JOB_CONTROLLER_NAMESPACE",
2463
+ )
2464
+ @click.option(
2465
+ "--rosa-job-service-account",
2466
+ help="The service-account used for ROSA jobs",
2467
+ required=True,
2468
+ envvar="ROSA_JOB_SERVICE_ACCOUNT",
2469
+ )
2470
+ @click.option(
2471
+ "--rosa-job-image",
2472
+ help="The container image to use to run ROSA cli command jobs",
2473
+ required=False,
2474
+ envvar="ROSA_JOB_IMAGE",
2475
+ )
2476
+ @click.option(
2477
+ "--rosa-role",
2478
+ help="The role to assume in the ROSA cluster account",
2479
+ required=True,
2480
+ envvar="ROSA_ROLE",
2481
+ )
2482
+ @click.pass_context
2483
+ def version_gate_approver(
2484
+ ctx,
2485
+ job_controller_cluster: str,
2486
+ job_controller_namespace: str,
2487
+ rosa_job_service_account: str,
2488
+ rosa_role: str,
2489
+ rosa_job_image: Optional[str],
2490
+ ) -> None:
2491
+ from reconcile.aus.version_gate_approver import (
2492
+ VersionGateApprover,
2493
+ VersionGateApproverParams,
2494
+ )
2495
+
2496
+ run_class_integration(
2497
+ integration=VersionGateApprover(
2498
+ VersionGateApproverParams(
2499
+ job_controller_cluster=job_controller_cluster,
2500
+ job_controller_namespace=job_controller_namespace,
2501
+ rosa_job_service_account=rosa_job_service_account,
2502
+ rosa_job_image=rosa_job_image,
2503
+ rosa_role=rosa_role,
2504
+ )
2505
+ ),
2506
+ ctx=ctx.obj,
2507
+ )
2508
+
2509
+
2451
2510
  @integration.command(short_help="Manage Databases and Database Users.")
2452
2511
  @vault_output_path
2453
2512
  @click.pass_context
@@ -21,6 +21,17 @@ def get_account_uid_from_arn(arn):
21
21
  return arn.split(":")[4]
22
22
 
23
23
 
24
+ def get_role_name_from_arn(arn: str) -> str:
25
+ # arn:aws:iam::12345:role/role-1 --> role-1
26
+ return arn.split("/")[-1]
27
+
28
+
29
+ def is_aws_managed_resource(arn: str) -> bool:
30
+ # arn:aws:iam::aws:role/role-1 --> True
31
+ # arn:aws:iam::12345:role/role-1 --> False
32
+ return get_account_uid_from_arn(arn) == "aws"
33
+
34
+
24
35
  def get_details_from_role_link(role_link):
25
36
  # https://signin.aws.amazon.com/switchrole?
26
37
  # account=<uid>&roleName=<role_name> -->
@@ -14,6 +14,8 @@ from pydantic import (
14
14
  Field,
15
15
  )
16
16
 
17
+ from reconcile.utils.aws_helper import get_account_uid_from_arn, get_role_name_from_arn
18
+
17
19
  LabelSetTypeVar = TypeVar("LabelSetTypeVar", bound=BaseModel)
18
20
  ACTIVE_SUBSCRIPTION_STATES = {"Active", "Reserved"}
19
21
  CAPABILITY_MANAGE_CLUSTER_ADMIN = "capability.cluster.manage_cluster_admin"
@@ -47,6 +49,12 @@ class OCMVersionGate(BaseModel):
47
49
  id: str
48
50
  version_raw_id_prefix: str
49
51
  sts_only: bool
52
+ label: str
53
+ """
54
+ the label field holds a readable name for a verion gate, e.g.
55
+ - api.openshift.com/gate-sts
56
+ - api.openshift.com/gate-ocp
57
+ """
50
58
 
51
59
 
52
60
  class OCMClusterGroupId(Enum):
@@ -118,13 +126,62 @@ class OCMClusterFlag(BaseModel):
118
126
  enabled: bool
119
127
 
120
128
 
129
+ class OCMClusterAWSOperatorRole(BaseModel):
130
+ id: str
131
+ name: str
132
+ namespace: str
133
+ role_arn: str
134
+ service_account: str
135
+
136
+
137
+ class OCMAWSSTS(OCMClusterFlag):
138
+ role_arn: Optional[str]
139
+ support_role_arn: Optional[str]
140
+ oidc_endpoint_url: Optional[str]
141
+ operator_iam_roles: Optional[list[OCMClusterAWSOperatorRole]]
142
+ instance_iam_roles: Optional[dict[str, str]]
143
+ operator_role_prefix: Optional[str]
144
+
145
+
121
146
  class OCMClusterAWSSettings(BaseModel):
122
- sts: Optional[OCMClusterFlag]
147
+ sts: Optional[OCMAWSSTS]
123
148
 
124
149
  @property
125
150
  def sts_enabled(self) -> bool:
126
151
  return self.sts is not None and self.sts.enabled
127
152
 
153
+ @property
154
+ def aws_account_id(self) -> str:
155
+ return get_account_uid_from_arn(self.account_roles[0])
156
+
157
+ @property
158
+ def account_roles(self) -> list[str]:
159
+ if not self.sts or not self.sts.enabled:
160
+ return []
161
+ roles = []
162
+ if self.sts.role_arn:
163
+ roles.append(self.sts.role_arn)
164
+ if self.sts.support_role_arn:
165
+ roles.append(self.sts.support_role_arn)
166
+ for instance_iam_role in (self.sts.instance_iam_roles or {}).values():
167
+ roles.append(instance_iam_role)
168
+ return roles
169
+
170
+ @property
171
+ def account_role_prefix(self) -> Optional[str]:
172
+ INSTALLER_ROLE_BASE_NAME = "-Installer-Role"
173
+ installer_role_arn = self.sts.role_arn if self.sts else None
174
+ if installer_role_arn and installer_role_arn.endswith(INSTALLER_ROLE_BASE_NAME):
175
+ installer_role_name = get_role_name_from_arn(installer_role_arn)
176
+ return installer_role_name.removesuffix(INSTALLER_ROLE_BASE_NAME)
177
+ return None
178
+
179
+ @property
180
+ def operator_roles(self) -> list[str]:
181
+ if not self.sts:
182
+ return []
183
+ return [role.role_arn for role in self.sts.operator_iam_roles or []]
184
+
128
185
 
129
186
  class OCMClusterVersion(BaseModel):
130
187
  id: str
@@ -147,7 +204,6 @@ class OCMClusterDns(BaseModel):
147
204
 
148
205
 
149
206
  class OCMExternalConfiguration(BaseModel):
150
- kind: str
151
207
  syncsets: dict
152
208
 
153
209
 
@@ -199,6 +255,9 @@ class OCMCluster(BaseModel):
199
255
  def is_osd(self) -> bool:
200
256
  return self.product.id == PRODUCT_ID_OSD
201
257
 
258
+ def is_rosa(self) -> bool:
259
+ return self.product.id == PRODUCT_ID_ROSA
260
+
202
261
  def is_rosa_classic(self) -> bool:
203
262
  return self.product.id == PRODUCT_ID_ROSA and not self.hypershift.enabled
204
263
 
@@ -35,3 +35,8 @@ def sort_versions(versions: Iterable[str]) -> list[str]:
35
35
 
36
36
  def is_version_bumped(current_version: str, previous_version: str) -> bool:
37
37
  return parse_semver(current_version) > parse_semver(previous_version)
38
+
39
+
40
+ def get_version_prefix(version: str) -> str:
41
+ semver = parse_semver(version)
42
+ return f"{semver.major}.{semver.minor}"