qontract-reconcile 0.10.1rc612__py3-none-any.whl → 0.10.1rc614__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {qontract_reconcile-0.10.1rc612.dist-info → qontract_reconcile-0.10.1rc614.dist-info}/METADATA +1 -1
- {qontract_reconcile-0.10.1rc612.dist-info → qontract_reconcile-0.10.1rc614.dist-info}/RECORD +19 -17
- reconcile/cli.py +57 -4
- reconcile/ocm/types.py +1 -1
- reconcile/ocm_clusters.py +171 -103
- reconcile/saas_auto_promotions_manager/merge_request_manager/merge_request_manager_v2.py +4 -1
- reconcile/saas_auto_promotions_manager/merge_request_manager/renderer.py +5 -3
- reconcile/templates/rosa-hcp-cluster-creation.sh.j2 +54 -0
- reconcile/test/test_ocm_clusters.py +119 -44
- reconcile/test/test_ocm_upgrade_scheduler_org_updater.py +1 -5
- reconcile/utils/jobcontroller/controller.py +1 -1
- reconcile/utils/ocm/__init__.py +29 -0
- reconcile/utils/ocm/clusters.py +7 -0
- reconcile/utils/ocm/ocm.py +58 -665
- reconcile/utils/ocm/products.py +743 -0
- reconcile/utils/rosa/session.py +53 -0
- {qontract_reconcile-0.10.1rc612.dist-info → qontract_reconcile-0.10.1rc614.dist-info}/WHEEL +0 -0
- {qontract_reconcile-0.10.1rc612.dist-info → qontract_reconcile-0.10.1rc614.dist-info}/entry_points.txt +0 -0
- {qontract_reconcile-0.10.1rc612.dist-info → qontract_reconcile-0.10.1rc614.dist-info}/top_level.txt +0 -0
reconcile/utils/ocm/ocm.py
CHANGED
@@ -1,11 +1,7 @@
|
|
1
1
|
from __future__ import annotations
|
2
2
|
|
3
3
|
import functools
|
4
|
-
import logging
|
5
|
-
import random
|
6
4
|
import re
|
7
|
-
import string
|
8
|
-
from abc import abstractmethod
|
9
5
|
from collections.abc import Mapping
|
10
6
|
from typing import (
|
11
7
|
Any,
|
@@ -17,18 +13,14 @@ from sretoolbox.utils import retry
|
|
17
13
|
import reconcile.utils.aws_helper as awsh
|
18
14
|
from reconcile.gql_definitions.fragments.vault_secret import VaultSecret
|
19
15
|
from reconcile.ocm.types import (
|
20
|
-
ClusterMachinePool,
|
21
|
-
OCMClusterNetwork,
|
22
|
-
OCMClusterSpec,
|
23
16
|
OCMSpec,
|
24
|
-
OSDClusterSpec,
|
25
|
-
ROSAClusterAWSAccount,
|
26
|
-
ROSAClusterSpec,
|
27
|
-
ROSAOcmAwsAttrs,
|
28
|
-
ROSAOcmAwsStsAttrs,
|
29
17
|
)
|
30
|
-
from reconcile.utils.exceptions import ParameterError
|
31
18
|
from reconcile.utils.ocm.clusters import get_node_pools
|
19
|
+
from reconcile.utils.ocm.products import (
|
20
|
+
OCMProduct,
|
21
|
+
OCMProductPortfolio,
|
22
|
+
build_product_portfolio,
|
23
|
+
)
|
32
24
|
from reconcile.utils.ocm_base_client import (
|
33
25
|
OCMAPIClientConfiguration,
|
34
26
|
OCMBaseClient,
|
@@ -74,623 +66,8 @@ CLUSTER_ADDON_DESIRED_KEYS = {"id", "parameters"}
|
|
74
66
|
|
75
67
|
DISABLE_UWM_ATTR = "disable_user_workload_monitoring"
|
76
68
|
CLUSTER_ADMIN_LABEL_KEY = "capability.cluster.manage_cluster_admin"
|
77
|
-
BYTES_IN_GIGABYTE = 1024**3
|
78
69
|
REQUEST_TIMEOUT_SEC = 60
|
79
70
|
|
80
|
-
SPEC_ATTR_ACCOUNT = "account"
|
81
|
-
SPEC_ATTR_DISABLE_UWM = "disable_user_workload_monitoring"
|
82
|
-
SPEC_ATTR_PRIVATE = "private"
|
83
|
-
SPEC_ATTR_CHANNEL = "channel"
|
84
|
-
SPEC_ATTR_LOAD_BALANCERS = "load_balancers"
|
85
|
-
SPEC_ATTR_STORAGE = "storage"
|
86
|
-
SPEC_ATTR_ID = "id"
|
87
|
-
SPEC_ATTR_EXTERNAL_ID = "external_id"
|
88
|
-
SPEC_ATTR_OIDC_ENDPONT_URL = "oidc_endpoint_url"
|
89
|
-
SPEC_ATTR_PROVISION_SHARD_ID = "provision_shard_id"
|
90
|
-
SPEC_ATTR_VERSION = "version"
|
91
|
-
SPEC_ATTR_INITIAL_VERSION = "initial_version"
|
92
|
-
SPEC_ATTR_MULTI_AZ = "multi_az"
|
93
|
-
SPEC_ATTR_HYPERSHIFT = "hypershift"
|
94
|
-
SPEC_ATTR_SUBNET_IDS = "subnet_ids"
|
95
|
-
SPEC_ATTR_AVAILABILITY_ZONES = "availability_zones"
|
96
|
-
|
97
|
-
SPEC_ATTR_NETWORK = "network"
|
98
|
-
|
99
|
-
SPEC_ATTR_CONSOLE_URL = "consoleUrl"
|
100
|
-
SPEC_ATTR_SERVER_URL = "serverUrl"
|
101
|
-
SPEC_ATTR_ELBFQDN = "elbFQDN"
|
102
|
-
SPEC_ATTR_PATH = "path"
|
103
|
-
|
104
|
-
OCM_PRODUCT_OSD = "osd"
|
105
|
-
OCM_PRODUCT_ROSA = "rosa"
|
106
|
-
OCM_PRODUCT_HYPERSHIFT = "hypershift"
|
107
|
-
|
108
|
-
DEFAULT_OCM_MACHINE_POOL_ID = "worker"
|
109
|
-
|
110
|
-
|
111
|
-
class OCMValidationException(Exception):
|
112
|
-
pass
|
113
|
-
|
114
|
-
|
115
|
-
class OCMProduct:
|
116
|
-
ALLOWED_SPEC_UPDATE_FIELDS: set[str]
|
117
|
-
EXCLUDED_SPEC_FIELDS: set[str]
|
118
|
-
|
119
|
-
@staticmethod
|
120
|
-
@abstractmethod
|
121
|
-
def create_cluster(ocm: OCM, name: str, cluster: OCMSpec, dry_run: bool):
|
122
|
-
pass
|
123
|
-
|
124
|
-
@staticmethod
|
125
|
-
@abstractmethod
|
126
|
-
def update_cluster(
|
127
|
-
ocm: OCM, cluster_name: str, update_spec: Mapping[str, Any], dry_run: bool
|
128
|
-
):
|
129
|
-
pass
|
130
|
-
|
131
|
-
@staticmethod
|
132
|
-
@abstractmethod
|
133
|
-
def get_ocm_spec(
|
134
|
-
ocm: OCM, cluster: Mapping[str, Any], init_provision_shards: bool
|
135
|
-
) -> OCMSpec:
|
136
|
-
pass
|
137
|
-
|
138
|
-
|
139
|
-
class OCMProductOsd(OCMProduct):
|
140
|
-
ALLOWED_SPEC_UPDATE_FIELDS = {
|
141
|
-
SPEC_ATTR_STORAGE,
|
142
|
-
SPEC_ATTR_LOAD_BALANCERS,
|
143
|
-
SPEC_ATTR_PRIVATE,
|
144
|
-
SPEC_ATTR_CHANNEL,
|
145
|
-
SPEC_ATTR_DISABLE_UWM,
|
146
|
-
}
|
147
|
-
|
148
|
-
EXCLUDED_SPEC_FIELDS = {
|
149
|
-
SPEC_ATTR_ID,
|
150
|
-
SPEC_ATTR_EXTERNAL_ID,
|
151
|
-
SPEC_ATTR_PROVISION_SHARD_ID,
|
152
|
-
SPEC_ATTR_VERSION,
|
153
|
-
SPEC_ATTR_INITIAL_VERSION,
|
154
|
-
SPEC_ATTR_HYPERSHIFT,
|
155
|
-
}
|
156
|
-
|
157
|
-
@staticmethod
|
158
|
-
def create_cluster(ocm: OCM, name: str, cluster: OCMSpec, dry_run: bool):
|
159
|
-
ocm_spec = OCMProductOsd._get_create_cluster_spec(name, cluster)
|
160
|
-
api = f"{CS_API_BASE}/v1/clusters"
|
161
|
-
params = {}
|
162
|
-
if dry_run:
|
163
|
-
params["dryRun"] = "true"
|
164
|
-
|
165
|
-
ocm._post(api, ocm_spec, params)
|
166
|
-
|
167
|
-
@staticmethod
|
168
|
-
def update_cluster(
|
169
|
-
ocm: OCM, cluster_name: str, update_spec: Mapping[str, Any], dry_run: bool
|
170
|
-
):
|
171
|
-
ocm_spec = OCMProductOsd._get_update_cluster_spec(update_spec)
|
172
|
-
cluster_id = ocm.cluster_ids.get(cluster_name)
|
173
|
-
api = f"{CS_API_BASE}/v1/clusters/{cluster_id}"
|
174
|
-
params: dict[str, Any] = {}
|
175
|
-
if dry_run:
|
176
|
-
params["dryRun"] = "true"
|
177
|
-
ocm._patch(api, ocm_spec, params)
|
178
|
-
|
179
|
-
@staticmethod
|
180
|
-
def get_ocm_spec(
|
181
|
-
ocm: OCM, cluster: Mapping[str, Any], init_provision_shards: bool
|
182
|
-
) -> OCMSpec:
|
183
|
-
if init_provision_shards:
|
184
|
-
provision_shard_id = ocm.get_provision_shard(cluster["id"])["id"]
|
185
|
-
else:
|
186
|
-
provision_shard_id = None
|
187
|
-
|
188
|
-
spec = OCMClusterSpec(
|
189
|
-
product=cluster["product"]["id"],
|
190
|
-
id=cluster["id"],
|
191
|
-
external_id=cluster["external_id"],
|
192
|
-
provider=cluster["cloud_provider"]["id"],
|
193
|
-
region=cluster["region"]["id"],
|
194
|
-
channel=cluster["version"]["channel_group"],
|
195
|
-
version=cluster["version"]["raw_id"],
|
196
|
-
multi_az=cluster["multi_az"],
|
197
|
-
private=cluster["api"]["listening"] == "internal",
|
198
|
-
disable_user_workload_monitoring=cluster[
|
199
|
-
"disable_user_workload_monitoring"
|
200
|
-
],
|
201
|
-
provision_shard_id=provision_shard_id,
|
202
|
-
hypershift=cluster["hypershift"]["enabled"],
|
203
|
-
)
|
204
|
-
|
205
|
-
if not cluster["ccs"]["enabled"]:
|
206
|
-
cluster_spec_data = spec.dict()
|
207
|
-
cluster_spec_data["storage"] = (
|
208
|
-
cluster["storage_quota"]["value"] // BYTES_IN_GIGABYTE
|
209
|
-
)
|
210
|
-
cluster_spec_data["load_balancers"] = cluster["load_balancer_quota"]
|
211
|
-
spec = OSDClusterSpec(**cluster_spec_data)
|
212
|
-
|
213
|
-
machine_pools = [
|
214
|
-
ClusterMachinePool(**p) for p in cluster.get("machinePools") or []
|
215
|
-
]
|
216
|
-
|
217
|
-
network = OCMClusterNetwork(
|
218
|
-
type=cluster["network"].get("type") or "OVNKubernetes",
|
219
|
-
vpc=cluster["network"]["machine_cidr"],
|
220
|
-
service=cluster["network"]["service_cidr"],
|
221
|
-
pod=cluster["network"]["pod_cidr"],
|
222
|
-
)
|
223
|
-
|
224
|
-
ocm_spec = OCMSpec(
|
225
|
-
console_url=cluster["console"]["url"],
|
226
|
-
server_url=cluster["api"]["url"],
|
227
|
-
domain=cluster["dns"]["base_domain"],
|
228
|
-
spec=spec,
|
229
|
-
machine_pools=machine_pools,
|
230
|
-
network=network,
|
231
|
-
)
|
232
|
-
|
233
|
-
return ocm_spec
|
234
|
-
|
235
|
-
@staticmethod
|
236
|
-
def _get_nodes_spec(cluster: OCMSpec) -> dict[str, Any]:
|
237
|
-
default_machine_pool = next(
|
238
|
-
(
|
239
|
-
mp
|
240
|
-
for mp in cluster.machine_pools
|
241
|
-
if mp.id == DEFAULT_OCM_MACHINE_POOL_ID
|
242
|
-
),
|
243
|
-
None,
|
244
|
-
)
|
245
|
-
if default_machine_pool is None:
|
246
|
-
raise OCMValidationException(
|
247
|
-
f"No default machine pool found, id: {DEFAULT_OCM_MACHINE_POOL_ID}"
|
248
|
-
)
|
249
|
-
|
250
|
-
spec: dict[str, Any] = {
|
251
|
-
"compute_machine_type": {"id": default_machine_pool.instance_type},
|
252
|
-
}
|
253
|
-
if default_machine_pool.autoscale is not None:
|
254
|
-
spec["autoscale_compute"] = default_machine_pool.autoscale.dict()
|
255
|
-
else:
|
256
|
-
spec["compute"] = default_machine_pool.replicas
|
257
|
-
return spec
|
258
|
-
|
259
|
-
@staticmethod
|
260
|
-
def _get_create_cluster_spec(cluster_name: str, cluster: OCMSpec) -> dict[str, Any]:
|
261
|
-
ocm_spec: dict[str, Any] = {
|
262
|
-
"name": cluster_name,
|
263
|
-
"cloud_provider": {"id": cluster.spec.provider},
|
264
|
-
"region": {"id": cluster.spec.region},
|
265
|
-
"version": {
|
266
|
-
"id": f"openshift-v{cluster.spec.initial_version}",
|
267
|
-
"channel_group": cluster.spec.channel,
|
268
|
-
},
|
269
|
-
"multi_az": cluster.spec.multi_az,
|
270
|
-
"nodes": OCMProductOsd._get_nodes_spec(cluster),
|
271
|
-
"network": {
|
272
|
-
"type": cluster.network.type or "OVNKubernetes",
|
273
|
-
"machine_cidr": cluster.network.vpc,
|
274
|
-
"service_cidr": cluster.network.service,
|
275
|
-
"pod_cidr": cluster.network.pod,
|
276
|
-
},
|
277
|
-
"api": {"listening": "internal" if cluster.spec.private else "external"},
|
278
|
-
"disable_user_workload_monitoring": (
|
279
|
-
duwm
|
280
|
-
if (duwm := cluster.spec.disable_user_workload_monitoring) is not None
|
281
|
-
else True
|
282
|
-
),
|
283
|
-
}
|
284
|
-
|
285
|
-
# Workaround to enable type checks.
|
286
|
-
# cluster.spec is a Union of pydantic models Union[OSDClusterSpec, RosaClusterSpec].
|
287
|
-
# In this case, cluster.spec will always be an OSDClusterSpec because the type
|
288
|
-
# assignment is managed by pydantic, however, mypy complains if OSD attributes are set
|
289
|
-
# outside the isinstance check because it checks all the types set in the Union.
|
290
|
-
if isinstance(cluster.spec, OSDClusterSpec):
|
291
|
-
ocm_spec["storage_quota"] = {
|
292
|
-
"value": float(cluster.spec.storage * BYTES_IN_GIGABYTE),
|
293
|
-
}
|
294
|
-
ocm_spec["load_balancer_quota"] = cluster.spec.load_balancers
|
295
|
-
|
296
|
-
provision_shard_id = cluster.spec.provision_shard_id
|
297
|
-
if provision_shard_id:
|
298
|
-
ocm_spec.setdefault("properties", {})
|
299
|
-
ocm_spec["properties"]["provision_shard_id"] = provision_shard_id
|
300
|
-
return ocm_spec
|
301
|
-
|
302
|
-
@staticmethod
|
303
|
-
def _get_update_cluster_spec(update_spec: Mapping[str, Any]) -> dict[str, Any]:
|
304
|
-
ocm_spec: dict[str, Any] = {}
|
305
|
-
|
306
|
-
storage = update_spec.get("storage")
|
307
|
-
if storage is not None:
|
308
|
-
ocm_spec["storage_quota"] = {"value": float(storage * 1073741824)} # 1024^3
|
309
|
-
|
310
|
-
load_balancers = update_spec.get("load_balancers")
|
311
|
-
if load_balancers is not None:
|
312
|
-
ocm_spec["load_balancer_quota"] = load_balancers
|
313
|
-
|
314
|
-
private = update_spec.get("private")
|
315
|
-
if private is not None:
|
316
|
-
ocm_spec["api"] = {"listening": "internal" if private else "external"}
|
317
|
-
|
318
|
-
channel = update_spec.get("channel")
|
319
|
-
if channel is not None:
|
320
|
-
ocm_spec["version"] = {"channel_group": channel}
|
321
|
-
|
322
|
-
disable_uwm = update_spec.get("disable_user_workload_monitoring")
|
323
|
-
if disable_uwm is not None:
|
324
|
-
ocm_spec["disable_user_workload_monitoring"] = disable_uwm
|
325
|
-
|
326
|
-
return ocm_spec
|
327
|
-
|
328
|
-
|
329
|
-
class OCMProductRosa(OCMProduct):
|
330
|
-
ALLOWED_SPEC_UPDATE_FIELDS = {
|
331
|
-
SPEC_ATTR_CHANNEL,
|
332
|
-
SPEC_ATTR_DISABLE_UWM,
|
333
|
-
}
|
334
|
-
|
335
|
-
EXCLUDED_SPEC_FIELDS = {
|
336
|
-
SPEC_ATTR_ID,
|
337
|
-
SPEC_ATTR_EXTERNAL_ID,
|
338
|
-
SPEC_ATTR_PROVISION_SHARD_ID,
|
339
|
-
SPEC_ATTR_VERSION,
|
340
|
-
SPEC_ATTR_INITIAL_VERSION,
|
341
|
-
SPEC_ATTR_ACCOUNT,
|
342
|
-
SPEC_ATTR_HYPERSHIFT,
|
343
|
-
SPEC_ATTR_SUBNET_IDS,
|
344
|
-
SPEC_ATTR_AVAILABILITY_ZONES,
|
345
|
-
SPEC_ATTR_OIDC_ENDPONT_URL,
|
346
|
-
}
|
347
|
-
|
348
|
-
@staticmethod
|
349
|
-
def create_cluster(ocm: OCM, name: str, cluster: OCMSpec, dry_run: bool):
|
350
|
-
ocm_spec = OCMProductRosa._get_create_cluster_spec(name, cluster)
|
351
|
-
api = f"{CS_API_BASE}/v1/clusters"
|
352
|
-
params = {}
|
353
|
-
if dry_run:
|
354
|
-
params["dryRun"] = "true"
|
355
|
-
if cluster.spec.hypershift:
|
356
|
-
logging.info(
|
357
|
-
"Dry-Run is not yet implemented for Hosted clusters. Here is the payload:"
|
358
|
-
)
|
359
|
-
logging.info(ocm_spec)
|
360
|
-
return
|
361
|
-
ocm._post(api, ocm_spec, params)
|
362
|
-
|
363
|
-
@staticmethod
|
364
|
-
def update_cluster(
|
365
|
-
ocm: OCM, cluster_name: str, update_spec: Mapping[str, Any], dry_run: bool
|
366
|
-
):
|
367
|
-
ocm_spec = OCMProductRosa._get_update_cluster_spec(update_spec)
|
368
|
-
cluster_id = ocm.cluster_ids.get(cluster_name)
|
369
|
-
api = f"{CS_API_BASE}/v1/clusters/{cluster_id}"
|
370
|
-
params: dict[str, Any] = {}
|
371
|
-
if dry_run:
|
372
|
-
params["dryRun"] = "true"
|
373
|
-
ocm._patch(api, ocm_spec, params)
|
374
|
-
|
375
|
-
@staticmethod
|
376
|
-
def get_ocm_spec(
|
377
|
-
ocm: OCM, cluster: Mapping[str, Any], init_provision_shards: bool
|
378
|
-
) -> OCMSpec:
|
379
|
-
if init_provision_shards:
|
380
|
-
provision_shard_id = ocm.get_provision_shard(cluster["id"])["id"]
|
381
|
-
else:
|
382
|
-
provision_shard_id = None
|
383
|
-
|
384
|
-
sts = None
|
385
|
-
oidc_endpoint_url = None
|
386
|
-
if cluster["aws"].get("sts", None):
|
387
|
-
sts = ROSAOcmAwsStsAttrs(
|
388
|
-
installer_role_arn=cluster["aws"]["sts"]["role_arn"],
|
389
|
-
support_role_arn=cluster["aws"]["sts"]["support_role_arn"],
|
390
|
-
controlplane_role_arn=cluster["aws"]["sts"]["instance_iam_roles"].get(
|
391
|
-
"master_role_arn"
|
392
|
-
),
|
393
|
-
worker_role_arn=cluster["aws"]["sts"]["instance_iam_roles"][
|
394
|
-
"worker_role_arn"
|
395
|
-
],
|
396
|
-
)
|
397
|
-
oidc_endpoint_url = cluster["aws"]["sts"]["oidc_endpoint_url"]
|
398
|
-
account = ROSAClusterAWSAccount(
|
399
|
-
uid=cluster["properties"]["rosa_creator_arn"].split(":")[4],
|
400
|
-
rosa=ROSAOcmAwsAttrs(
|
401
|
-
creator_role_arn=cluster["properties"]["rosa_creator_arn"],
|
402
|
-
sts=sts,
|
403
|
-
),
|
404
|
-
)
|
405
|
-
|
406
|
-
spec = ROSAClusterSpec(
|
407
|
-
product=cluster["product"]["id"],
|
408
|
-
account=account,
|
409
|
-
id=cluster["id"],
|
410
|
-
external_id=cluster.get("external_id"),
|
411
|
-
provider=cluster["cloud_provider"]["id"],
|
412
|
-
region=cluster["region"]["id"],
|
413
|
-
channel=cluster["version"]["channel_group"],
|
414
|
-
version=cluster["version"]["raw_id"],
|
415
|
-
multi_az=cluster["multi_az"],
|
416
|
-
private=cluster["api"]["listening"] == "internal",
|
417
|
-
disable_user_workload_monitoring=cluster[
|
418
|
-
"disable_user_workload_monitoring"
|
419
|
-
],
|
420
|
-
provision_shard_id=provision_shard_id,
|
421
|
-
hypershift=cluster["hypershift"]["enabled"],
|
422
|
-
subnet_ids=cluster["aws"].get("subnet_ids"),
|
423
|
-
availability_zones=cluster["nodes"].get("availability_zones"),
|
424
|
-
oidc_endpoint_url=oidc_endpoint_url,
|
425
|
-
)
|
426
|
-
|
427
|
-
machine_pools = [
|
428
|
-
ClusterMachinePool(**p) for p in cluster.get("machinePools") or []
|
429
|
-
]
|
430
|
-
|
431
|
-
network = OCMClusterNetwork(
|
432
|
-
type=cluster["network"].get("type") or "OVNKubernetes",
|
433
|
-
vpc=cluster["network"]["machine_cidr"],
|
434
|
-
service=cluster["network"]["service_cidr"],
|
435
|
-
pod=cluster["network"]["pod_cidr"],
|
436
|
-
)
|
437
|
-
|
438
|
-
ocm_spec = OCMSpec(
|
439
|
-
# Hosted control plane clusters can reach a Ready State without having the console
|
440
|
-
# Endpoint
|
441
|
-
console_url=cluster.get("console", {}).get("url", ""),
|
442
|
-
server_url=cluster["api"]["url"],
|
443
|
-
domain=cluster["dns"]["base_domain"],
|
444
|
-
spec=spec,
|
445
|
-
machine_pools=machine_pools,
|
446
|
-
network=network,
|
447
|
-
)
|
448
|
-
|
449
|
-
return ocm_spec
|
450
|
-
|
451
|
-
@staticmethod
|
452
|
-
def _get_nodes_spec(cluster: OCMSpec) -> dict[str, Any]:
|
453
|
-
default_machine_pool = next(
|
454
|
-
(
|
455
|
-
mp
|
456
|
-
for mp in cluster.machine_pools
|
457
|
-
if mp.id == DEFAULT_OCM_MACHINE_POOL_ID
|
458
|
-
),
|
459
|
-
None,
|
460
|
-
)
|
461
|
-
if default_machine_pool is None:
|
462
|
-
raise OCMValidationException(
|
463
|
-
f"No default machine pool found, id: {DEFAULT_OCM_MACHINE_POOL_ID}"
|
464
|
-
)
|
465
|
-
|
466
|
-
spec: dict[str, Any] = {
|
467
|
-
"compute_machine_type": {"id": default_machine_pool.instance_type},
|
468
|
-
}
|
469
|
-
if default_machine_pool.autoscale is not None:
|
470
|
-
spec["autoscale_compute"] = default_machine_pool.autoscale.dict()
|
471
|
-
else:
|
472
|
-
spec["compute"] = default_machine_pool.replicas
|
473
|
-
return spec
|
474
|
-
|
475
|
-
@staticmethod
|
476
|
-
def _get_create_cluster_spec(cluster_name: str, cluster: OCMSpec) -> dict[str, Any]:
|
477
|
-
operator_roles_prefix = "".join(
|
478
|
-
random.choices(string.ascii_lowercase + string.digits, k=4)
|
479
|
-
)
|
480
|
-
|
481
|
-
ocm_spec: dict[str, Any] = {
|
482
|
-
"api": {"listening": "internal" if cluster.spec.private else "external"},
|
483
|
-
"name": cluster_name,
|
484
|
-
"cloud_provider": {"id": cluster.spec.provider},
|
485
|
-
"region": {"id": cluster.spec.region},
|
486
|
-
"version": {
|
487
|
-
"id": f"openshift-v{cluster.spec.initial_version}",
|
488
|
-
"channel_group": cluster.spec.channel,
|
489
|
-
},
|
490
|
-
"hypershift": {"enabled": cluster.spec.hypershift},
|
491
|
-
"multi_az": cluster.spec.multi_az,
|
492
|
-
"nodes": OCMProductRosa._get_nodes_spec(cluster),
|
493
|
-
"network": {
|
494
|
-
"type": cluster.network.type or "OVNKubernetes",
|
495
|
-
"machine_cidr": cluster.network.vpc,
|
496
|
-
"service_cidr": cluster.network.service,
|
497
|
-
"pod_cidr": cluster.network.pod,
|
498
|
-
},
|
499
|
-
"disable_user_workload_monitoring": (
|
500
|
-
duwm
|
501
|
-
if (duwm := cluster.spec.disable_user_workload_monitoring) is not None
|
502
|
-
else True
|
503
|
-
),
|
504
|
-
}
|
505
|
-
|
506
|
-
provision_shard_id = cluster.spec.provision_shard_id
|
507
|
-
if provision_shard_id:
|
508
|
-
ocm_spec.setdefault("properties", {})
|
509
|
-
ocm_spec["properties"]["provision_shard_id"] = provision_shard_id
|
510
|
-
|
511
|
-
if isinstance(cluster.spec, ROSAClusterSpec):
|
512
|
-
ocm_spec.setdefault("properties", {})
|
513
|
-
ocm_spec["properties"]["rosa_creator_arn"] = (
|
514
|
-
cluster.spec.account.rosa.creator_role_arn
|
515
|
-
)
|
516
|
-
|
517
|
-
if not cluster.spec.account.rosa.sts:
|
518
|
-
raise ParameterError("STS is required for ROSA clusters")
|
519
|
-
|
520
|
-
rosa_spec: dict[str, Any] = {
|
521
|
-
"product": {"id": "rosa"},
|
522
|
-
"ccs": {"enabled": True},
|
523
|
-
"aws": {
|
524
|
-
"account_id": cluster.spec.account.uid,
|
525
|
-
"sts": {
|
526
|
-
"enabled": True,
|
527
|
-
"auto_mode": True,
|
528
|
-
"role_arn": cluster.spec.account.rosa.sts.installer_role_arn,
|
529
|
-
"support_role_arn": cluster.spec.account.rosa.sts.support_role_arn,
|
530
|
-
"instance_iam_roles": {
|
531
|
-
"worker_role_arn": cluster.spec.account.rosa.sts.worker_role_arn,
|
532
|
-
},
|
533
|
-
"operator_role_prefix": f"{cluster_name}-{operator_roles_prefix}",
|
534
|
-
},
|
535
|
-
},
|
536
|
-
}
|
537
|
-
|
538
|
-
if cluster.spec.account.rosa.sts.controlplane_role_arn:
|
539
|
-
rosa_spec["aws"]["sts"]["instance_iam_roles"]["master_role_arn"] = (
|
540
|
-
cluster.spec.account.rosa.sts.controlplane_role_arn
|
541
|
-
)
|
542
|
-
|
543
|
-
if cluster.spec.hypershift:
|
544
|
-
ocm_spec["nodes"]["availability_zones"] = (
|
545
|
-
cluster.spec.availability_zones
|
546
|
-
)
|
547
|
-
rosa_spec["aws"]["subnet_ids"] = cluster.spec.subnet_ids
|
548
|
-
|
549
|
-
ocm_spec.update(rosa_spec)
|
550
|
-
return ocm_spec
|
551
|
-
|
552
|
-
@staticmethod
|
553
|
-
def _get_update_cluster_spec(update_spec: Mapping[str, Any]) -> dict[str, Any]:
|
554
|
-
ocm_spec: dict[str, Any] = {}
|
555
|
-
|
556
|
-
channel = update_spec.get(SPEC_ATTR_CHANNEL)
|
557
|
-
if channel is not None:
|
558
|
-
ocm_spec["version"] = {"channel_group": channel}
|
559
|
-
|
560
|
-
disable_uwm = update_spec.get(SPEC_ATTR_DISABLE_UWM)
|
561
|
-
if disable_uwm is not None:
|
562
|
-
ocm_spec["disable_user_workload_monitoring"] = disable_uwm
|
563
|
-
|
564
|
-
return ocm_spec
|
565
|
-
|
566
|
-
|
567
|
-
class OCMProductHypershift(OCMProduct):
|
568
|
-
# Not a real product, but a way to represent the Hypershift specialties
|
569
|
-
ALLOWED_SPEC_UPDATE_FIELDS = {
|
570
|
-
# needs implementation, see: https://issues.redhat.com/browse/OCM-2144
|
571
|
-
# SPEC_ATTR_CHANNEL,
|
572
|
-
# needs implementation, see: https://issues.redhat.com/browse/OCM-915
|
573
|
-
# SPEC_ATTR_PRIVATE,
|
574
|
-
SPEC_ATTR_DISABLE_UWM,
|
575
|
-
}
|
576
|
-
|
577
|
-
EXCLUDED_SPEC_FIELDS = {
|
578
|
-
SPEC_ATTR_ID,
|
579
|
-
SPEC_ATTR_EXTERNAL_ID,
|
580
|
-
SPEC_ATTR_PROVISION_SHARD_ID,
|
581
|
-
SPEC_ATTR_VERSION,
|
582
|
-
SPEC_ATTR_INITIAL_VERSION,
|
583
|
-
SPEC_ATTR_ACCOUNT,
|
584
|
-
SPEC_ATTR_HYPERSHIFT,
|
585
|
-
SPEC_ATTR_SUBNET_IDS,
|
586
|
-
SPEC_ATTR_AVAILABILITY_ZONES,
|
587
|
-
SPEC_ATTR_OIDC_ENDPONT_URL,
|
588
|
-
}
|
589
|
-
|
590
|
-
@staticmethod
|
591
|
-
def create_cluster(ocm: OCM, name: str, cluster: OCMSpec, dry_run: bool):
|
592
|
-
logging.error("Please use rosa cli to create new clusters")
|
593
|
-
|
594
|
-
@staticmethod
|
595
|
-
def update_cluster(
|
596
|
-
ocm: OCM, cluster_name: str, update_spec: Mapping[str, Any], dry_run: bool
|
597
|
-
):
|
598
|
-
ocm_spec = OCMProductRosa._get_update_cluster_spec(update_spec)
|
599
|
-
cluster_id = ocm.cluster_ids.get(cluster_name)
|
600
|
-
api = f"{CS_API_BASE}/v1/clusters/{cluster_id}"
|
601
|
-
params: dict[str, Any] = {}
|
602
|
-
if dry_run:
|
603
|
-
params["dryRun"] = "true"
|
604
|
-
ocm._patch(api, ocm_spec, params)
|
605
|
-
|
606
|
-
@staticmethod
|
607
|
-
def get_ocm_spec(
|
608
|
-
ocm: OCM, cluster: Mapping[str, Any], init_provision_shards: bool
|
609
|
-
) -> OCMSpec:
|
610
|
-
if init_provision_shards:
|
611
|
-
provision_shard_id = ocm.get_provision_shard(cluster["id"])["id"]
|
612
|
-
else:
|
613
|
-
provision_shard_id = None
|
614
|
-
|
615
|
-
sts = None
|
616
|
-
oidc_endpoint_url = None
|
617
|
-
if cluster["aws"].get("sts", None):
|
618
|
-
sts = ROSAOcmAwsStsAttrs(
|
619
|
-
installer_role_arn=cluster["aws"]["sts"]["role_arn"],
|
620
|
-
support_role_arn=cluster["aws"]["sts"]["support_role_arn"],
|
621
|
-
controlplane_role_arn=cluster["aws"]["sts"]["instance_iam_roles"].get(
|
622
|
-
"master_role_arn"
|
623
|
-
),
|
624
|
-
worker_role_arn=cluster["aws"]["sts"]["instance_iam_roles"][
|
625
|
-
"worker_role_arn"
|
626
|
-
],
|
627
|
-
)
|
628
|
-
oidc_endpoint_url = cluster["aws"]["sts"]["oidc_endpoint_url"]
|
629
|
-
account = ROSAClusterAWSAccount(
|
630
|
-
uid=cluster["properties"]["rosa_creator_arn"].split(":")[4],
|
631
|
-
rosa=ROSAOcmAwsAttrs(
|
632
|
-
creator_role_arn=cluster["properties"]["rosa_creator_arn"],
|
633
|
-
sts=sts,
|
634
|
-
),
|
635
|
-
)
|
636
|
-
|
637
|
-
spec = ROSAClusterSpec(
|
638
|
-
product=cluster["product"]["id"],
|
639
|
-
account=account,
|
640
|
-
id=cluster["id"],
|
641
|
-
external_id=cluster.get("external_id"),
|
642
|
-
provider=cluster["cloud_provider"]["id"],
|
643
|
-
region=cluster["region"]["id"],
|
644
|
-
channel=cluster["version"]["channel_group"],
|
645
|
-
version=cluster["version"]["raw_id"],
|
646
|
-
multi_az=cluster["multi_az"],
|
647
|
-
private=cluster["api"]["listening"] == "internal",
|
648
|
-
disable_user_workload_monitoring=cluster[
|
649
|
-
"disable_user_workload_monitoring"
|
650
|
-
],
|
651
|
-
provision_shard_id=provision_shard_id,
|
652
|
-
subnet_ids=cluster["aws"].get("subnet_ids"),
|
653
|
-
availability_zones=cluster["nodes"].get("availability_zones"),
|
654
|
-
hypershift=cluster["hypershift"]["enabled"],
|
655
|
-
oidc_endpoint_url=oidc_endpoint_url,
|
656
|
-
)
|
657
|
-
|
658
|
-
network = OCMClusterNetwork(
|
659
|
-
type=cluster["network"].get("type") or "OVNKubernetes",
|
660
|
-
vpc=cluster["network"]["machine_cidr"],
|
661
|
-
service=cluster["network"]["service_cidr"],
|
662
|
-
pod=cluster["network"]["pod_cidr"],
|
663
|
-
)
|
664
|
-
|
665
|
-
ocm_spec = OCMSpec(
|
666
|
-
# Hosted control plane clusters can reach a Ready State without having the console
|
667
|
-
# Endpoint
|
668
|
-
console_url=cluster.get("console", {}).get("url", ""),
|
669
|
-
server_url=cluster["api"]["url"],
|
670
|
-
domain=cluster["dns"]["base_domain"],
|
671
|
-
spec=spec,
|
672
|
-
network=network,
|
673
|
-
)
|
674
|
-
|
675
|
-
return ocm_spec
|
676
|
-
|
677
|
-
@staticmethod
|
678
|
-
def _get_update_cluster_spec(update_spec: Mapping[str, Any]) -> dict[str, Any]:
|
679
|
-
ocm_spec: dict[str, Any] = {}
|
680
|
-
|
681
|
-
disable_uwm = update_spec.get(SPEC_ATTR_DISABLE_UWM)
|
682
|
-
if disable_uwm is not None:
|
683
|
-
ocm_spec["disable_user_workload_monitoring"] = disable_uwm
|
684
|
-
|
685
|
-
return ocm_spec
|
686
|
-
|
687
|
-
|
688
|
-
OCM_PRODUCTS_IMPL = {
|
689
|
-
OCM_PRODUCT_OSD: OCMProductOsd,
|
690
|
-
OCM_PRODUCT_ROSA: OCMProductRosa,
|
691
|
-
OCM_PRODUCT_HYPERSHIFT: OCMProductHypershift,
|
692
|
-
}
|
693
|
-
|
694
71
|
|
695
72
|
class OCM: # pylint: disable=too-many-public-methods
|
696
73
|
"""
|
@@ -720,12 +97,17 @@ class OCM: # pylint: disable=too-many-public-methods
|
|
720
97
|
init_version_gates=False,
|
721
98
|
blocked_versions=None,
|
722
99
|
inheritVersionData: Optional[list[dict[str, Any]]] = None,
|
100
|
+
product_portfolio: Optional[OCMProductPortfolio] = None,
|
723
101
|
):
|
724
102
|
"""Initiates access token and gets clusters information."""
|
725
103
|
self.name = name
|
726
104
|
self._ocm_client = ocm_client
|
727
105
|
self.org_id = org_id
|
728
106
|
self.ocm_env = ocm_env
|
107
|
+
if product_portfolio is None:
|
108
|
+
self.product_portfolio = build_product_portfolio()
|
109
|
+
else:
|
110
|
+
self.product_portfolio = product_portfolio
|
729
111
|
self._init_clusters(init_provision_shards=init_provision_shards)
|
730
112
|
|
731
113
|
if init_addons:
|
@@ -746,22 +128,25 @@ class OCM: # pylint: disable=too-many-public-methods
|
|
746
128
|
self.get_aws_infrastructure_access_role_grants
|
747
129
|
)
|
748
130
|
|
749
|
-
@
|
750
|
-
def
|
131
|
+
@property
|
132
|
+
def ocm_api(self) -> OCMBaseClient:
|
133
|
+
return self._ocm_client
|
134
|
+
|
135
|
+
def _ready_for_app_interface(self, cluster: dict[str, Any]) -> bool:
|
751
136
|
return (
|
752
137
|
cluster["managed"]
|
753
138
|
and cluster["state"] == STATUS_READY
|
754
|
-
and cluster["product"]["id"] in
|
139
|
+
and cluster["product"]["id"] in self.product_portfolio.product_names
|
755
140
|
)
|
756
141
|
|
757
142
|
def _init_clusters(self, init_provision_shards: bool):
|
758
143
|
api = f"{CS_API_BASE}/v1/clusters"
|
759
|
-
product_csv = ",".join([f"'{p}'" for p in
|
144
|
+
product_csv = ",".join([f"'{p}'" for p in self.product_portfolio.product_names])
|
760
145
|
params = {
|
761
146
|
"search": f"organization.id='{self.org_id}' and managed='true' and product.id in ({product_csv})"
|
762
147
|
}
|
763
148
|
clusters = self._get_json(api, params=params).get("items", [])
|
764
|
-
self.cluster_ids = {c["name"]: c["id"] for c in clusters}
|
149
|
+
self.cluster_ids: dict[str, str] = {c["name"]: c["id"] for c in clusters}
|
765
150
|
|
766
151
|
self.clusters: dict[str, OCMSpec] = {}
|
767
152
|
self.available_cluster_upgrades: dict[str, list[str]] = {}
|
@@ -788,32 +173,31 @@ class OCM: # pylint: disable=too-many-public-methods
|
|
788
173
|
def is_ready(self, cluster):
|
789
174
|
return cluster in self.clusters
|
790
175
|
|
791
|
-
def
|
176
|
+
def get_product_impl(
|
792
177
|
self, product: str, hypershift: Optional[bool] = False
|
793
|
-
) ->
|
794
|
-
|
795
|
-
return OCM_PRODUCTS_IMPL[OCM_PRODUCT_HYPERSHIFT]
|
796
|
-
return OCM_PRODUCTS_IMPL[product]
|
178
|
+
) -> OCMProduct:
|
179
|
+
return self.product_portfolio.get_product_impl(product, hypershift)
|
797
180
|
|
798
181
|
def _get_cluster_ocm_spec(
|
799
182
|
self, cluster: Mapping[str, Any], init_provision_shards: bool
|
800
183
|
) -> OCMSpec:
|
801
|
-
impl = self.
|
184
|
+
impl = self.get_product_impl(
|
802
185
|
cluster["product"]["id"], cluster["hypershift"]["enabled"]
|
803
186
|
)
|
804
|
-
spec = impl.get_ocm_spec(self, cluster, init_provision_shards)
|
187
|
+
spec = impl.get_ocm_spec(self.ocm_api, cluster, init_provision_shards)
|
805
188
|
return spec
|
806
189
|
|
807
190
|
def create_cluster(self, name: str, cluster: OCMSpec, dry_run: bool):
|
808
|
-
impl = self.
|
809
|
-
impl.create_cluster(self, name, cluster, dry_run)
|
191
|
+
impl = self.get_product_impl(cluster.spec.product, cluster.spec.hypershift)
|
192
|
+
impl.create_cluster(self.ocm_api, self.org_id, name, cluster, dry_run)
|
810
193
|
|
811
194
|
def update_cluster(
|
812
195
|
self, cluster_name: str, update_spec: Mapping[str, Any], dry_run=False
|
813
196
|
):
|
814
197
|
cluster = self.clusters[cluster_name]
|
815
|
-
|
816
|
-
impl.
|
198
|
+
cluster_id = self.cluster_ids[cluster_name]
|
199
|
+
impl = self.get_product_impl(cluster.spec.product, cluster.spec.hypershift)
|
200
|
+
impl.update_cluster(self.ocm_api, cluster_id, update_spec, dry_run)
|
817
201
|
|
818
202
|
def get_group_if_exists(self, cluster, group_id):
|
819
203
|
"""Returns a list of users in a group in a cluster.
|
@@ -1325,16 +709,6 @@ class OCM: # pylint: disable=too-many-public-methods
|
|
1325
709
|
api = f"{CS_API_BASE}/v1/clusters/{cluster_id}/" + f"ingresses/{router_id}"
|
1326
710
|
self._delete(api)
|
1327
711
|
|
1328
|
-
def get_provision_shard(self, cluster_id):
|
1329
|
-
"""Returns details of the provision shard
|
1330
|
-
|
1331
|
-
:param cluster: cluster id
|
1332
|
-
|
1333
|
-
:type cluster: string
|
1334
|
-
"""
|
1335
|
-
api = f"{CS_API_BASE}/v1/clusters/{cluster_id}/provision_shard"
|
1336
|
-
return self._get_json(api)
|
1337
|
-
|
1338
712
|
@staticmethod
|
1339
713
|
def _get_autoscale(cluster):
|
1340
714
|
autoscale = cluster["nodes"].get("autoscale_compute", None)
|
@@ -1559,10 +933,11 @@ class OCMMap: # pylint: disable=too-many-public-methods
|
|
1559
933
|
init_provision_shards=False,
|
1560
934
|
init_addons=False,
|
1561
935
|
init_version_gates=False,
|
1562
|
-
|
936
|
+
product_portfolio: Optional[OCMProductPortfolio] = None,
|
937
|
+
) -> None:
|
1563
938
|
"""Initiates OCM instances for each OCM referenced in a cluster."""
|
1564
|
-
self.clusters_map = {}
|
1565
|
-
self.ocm_map = {}
|
939
|
+
self.clusters_map: dict[str, str] = {}
|
940
|
+
self.ocm_map: dict[str, OCM] = {}
|
1566
941
|
self.calling_integration = integration
|
1567
942
|
self.settings = settings
|
1568
943
|
|
@@ -1576,6 +951,7 @@ class OCMMap: # pylint: disable=too-many-public-methods
|
|
1576
951
|
init_provision_shards,
|
1577
952
|
init_addons,
|
1578
953
|
init_version_gates=init_version_gates,
|
954
|
+
product_portfolio=product_portfolio,
|
1579
955
|
)
|
1580
956
|
elif namespaces:
|
1581
957
|
for namespace_info in namespaces:
|
@@ -1585,6 +961,7 @@ class OCMMap: # pylint: disable=too-many-public-methods
|
|
1585
961
|
init_provision_shards,
|
1586
962
|
init_addons,
|
1587
963
|
init_version_gates=init_version_gates,
|
964
|
+
product_portfolio=product_portfolio,
|
1588
965
|
)
|
1589
966
|
elif ocms:
|
1590
967
|
for ocm in ocms:
|
@@ -1593,15 +970,21 @@ class OCMMap: # pylint: disable=too-many-public-methods
|
|
1593
970
|
init_provision_shards,
|
1594
971
|
init_addons,
|
1595
972
|
init_version_gates=init_version_gates,
|
973
|
+
product_portfolio=product_portfolio,
|
1596
974
|
)
|
1597
975
|
else:
|
1598
976
|
raise KeyError("expected one of clusters, namespaces or ocm.")
|
1599
977
|
|
1600
|
-
def __getitem__(self, ocm_name) -> OCM:
|
978
|
+
def __getitem__(self, ocm_name: str) -> OCM:
|
1601
979
|
return self.ocm_map[ocm_name]
|
1602
980
|
|
1603
981
|
def init_ocm_client_from_cluster(
|
1604
|
-
self,
|
982
|
+
self,
|
983
|
+
cluster_info,
|
984
|
+
init_provision_shards,
|
985
|
+
init_addons,
|
986
|
+
init_version_gates,
|
987
|
+
product_portfolio: Optional[OCMProductPortfolio] = None,
|
1605
988
|
):
|
1606
989
|
if self.cluster_disabled(cluster_info):
|
1607
990
|
return
|
@@ -1613,11 +996,20 @@ class OCMMap: # pylint: disable=too-many-public-methods
|
|
1613
996
|
|
1614
997
|
if ocm_name not in self.ocm_map:
|
1615
998
|
self.init_ocm_client(
|
1616
|
-
ocm_info,
|
999
|
+
ocm_info,
|
1000
|
+
init_provision_shards,
|
1001
|
+
init_addons,
|
1002
|
+
init_version_gates,
|
1003
|
+
product_portfolio,
|
1617
1004
|
)
|
1618
1005
|
|
1619
1006
|
def init_ocm_client(
|
1620
|
-
self,
|
1007
|
+
self,
|
1008
|
+
ocm_info,
|
1009
|
+
init_provision_shards,
|
1010
|
+
init_addons,
|
1011
|
+
init_version_gates,
|
1012
|
+
product_portfolio: Optional[OCMProductPortfolio] = None,
|
1621
1013
|
):
|
1622
1014
|
"""
|
1623
1015
|
Initiate OCM client.
|
@@ -1667,11 +1059,12 @@ class OCMMap: # pylint: disable=too-many-public-methods
|
|
1667
1059
|
blocked_versions=ocm_info.get("blockedVersions"),
|
1668
1060
|
init_version_gates=init_version_gates,
|
1669
1061
|
inheritVersionData=ocm_info.get("inheritVersionData"),
|
1062
|
+
product_portfolio=product_portfolio,
|
1670
1063
|
)
|
1671
1064
|
|
1672
1065
|
def instances(self) -> list[str]:
|
1673
1066
|
"""Get list of OCM instance names initiated in the OCM map."""
|
1674
|
-
return self.ocm_map.keys()
|
1067
|
+
return list(self.ocm_map.keys())
|
1675
1068
|
|
1676
1069
|
def cluster_disabled(self, cluster_info):
|
1677
1070
|
"""
|
@@ -1699,19 +1092,19 @@ class OCMMap: # pylint: disable=too-many-public-methods
|
|
1699
1092
|
:type cluster: string
|
1700
1093
|
"""
|
1701
1094
|
ocm = self.clusters_map[cluster]
|
1702
|
-
return self.ocm_map
|
1095
|
+
return self.ocm_map[ocm]
|
1703
1096
|
|
1704
1097
|
def clusters(self) -> list[str]:
|
1705
1098
|
"""Get list of cluster names initiated in the OCM map."""
|
1706
1099
|
return [k for k, v in self.clusters_map.items() if v]
|
1707
1100
|
|
1708
|
-
def cluster_specs(self) -> tuple[dict[str, OCMSpec], list]:
|
1101
|
+
def cluster_specs(self) -> tuple[dict[str, OCMSpec], list[str]]:
|
1709
1102
|
"""Get dictionary of cluster names and specs in the OCM map."""
|
1710
1103
|
cluster_specs = {}
|
1711
1104
|
for v in self.ocm_map.values():
|
1712
1105
|
cluster_specs.update(v.clusters)
|
1713
1106
|
|
1714
|
-
not_ready_cluster_names = []
|
1107
|
+
not_ready_cluster_names: list[str] = []
|
1715
1108
|
for v in self.ocm_map.values():
|
1716
1109
|
not_ready_cluster_names.extend(v.not_ready_clusters)
|
1717
1110
|
return cluster_specs, not_ready_cluster_names
|