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.
@@ -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
- @staticmethod
750
- def _ready_for_app_interface(cluster: dict[str, Any]) -> bool:
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 OCM_PRODUCTS_IMPL
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 OCM_PRODUCTS_IMPL])
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 _get_ocm_impl(
176
+ def get_product_impl(
792
177
  self, product: str, hypershift: Optional[bool] = False
793
- ) -> type[OCMProduct]:
794
- if hypershift:
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._get_ocm_impl(
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._get_ocm_impl(cluster.spec.product, cluster.spec.hypershift)
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
- impl = self._get_ocm_impl(cluster.spec.product, cluster.spec.hypershift)
816
- impl.update_cluster(self, cluster_name, update_spec, dry_run)
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, cluster_info, init_provision_shards, init_addons, init_version_gates
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, init_provision_shards, init_addons, init_version_gates
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, ocm_info, init_provision_shards, init_addons, init_version_gates
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.get(ocm, None)
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