diff --git a/AGENTS.md b/AGENTS.md index 5d7e6b529b..582ef7f624 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -218,6 +218,51 @@ When reviewing quarantine PRs, verify the **quarantine mechanism matches the fai - **Use identity for None** - `if x is None:` NOT `if x == None:` - **NEVER compare to True/False** - `if flag:` NOT `if flag == True:` +### HCO v1 API Usage (MANDATORY) + +All HCO spec patches and reads MUST use the v1 grouped API structure. Use `HCOv1Spec` builders from `utilities.constants.hco` to construct patches. + +**Available builders:** +- `HCOv1Spec.virtualization(field=value)` — virtualization fields (live migration, CPU, etc.) +- `HCOv1Spec.security(field=value)` — security fields (TLS profiles, etc.) +- `HCOv1Spec.storage(field=value)` — storage fields +- `HCOv1Spec.deployment(field=value)` — deployment fields +- `HCOv1Spec.workload_sources(field=value)` — workload source fields +- `HCOv1Spec.networking(field=value)` — networking fields +- `HCOv1Spec.node_placements(infra=..., workload=...)` — node placement +- `HCOv1Spec.vm_options(field=value)` — virtualMachineOptions +- `HCOv1Spec.aaq_config(field=value)` — applicationAwareConfig + +**Rules:** + +1. **HCO spec patches MUST use v1 grouped structure** — use `HCOv1Spec` builders, NEVER construct raw nested dicts manually +2. **Feature gates MUST use v1 list format** — use `HCOv1Spec.feature_gates(name=True/False)`, NEVER dict format `{"featureGates": {"name": true}}` +3. **NEVER use v1beta1 flat spec paths** — `spec.liveMigrationConfig` is wrong, `spec.virtualization.liveMigrationConfig` is correct. See `HCOv1Spec` group builders in `utilities/constants/hco.py` for the complete mapping +4. **FG state reads MUST use `HCOv1Spec.is_fg_enabled()`** with the `hco_fg_phases` fixture, not direct list searching. For deprecated FGs, read the dedicated spec field instead +5. **`spec.workloads` is renamed to `workload` (singular)** in v1 under `spec.deployment.nodePlacements.workload` + +**Before (v1beta1 -- WRONG):** +```python +# Flat spec path -- WRONG +patch = {"spec": {"liveMigrationConfig": {"parallelOutboundMigrationsPerNode": 5}}} +hco_resource.update(resource_dict=patch) + +# Dict feature gate -- WRONG +fg_patch = {"spec": {"featureGates": {"withHostPassthroughCPU": True}}} +``` + +**After (v1 -- CORRECT):** +```python +from utilities.constants.hco import HCOv1Spec + +# Grouped spec path +patch = HCOv1Spec.virtualization(liveMigrationConfig={"parallelOutboundMigrationsPerNode": 5}) +hco_resource.update(resource_dict=patch) + +# List feature gate +fg_patch = HCOv1Spec.feature_gates(withHostPassthroughCPU=True) +``` + ### Tests Directory Organization - **Feature subdirectories REQUIRED** - each feature MUST have its own subdirectory under component (e.g., `tests/network/ipv6/`) diff --git a/pyproject.toml b/pyproject.toml index 445acea19c..bace5949e6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -118,7 +118,7 @@ dependencies = [ "xmltodict>=0.14.2", "python-simple-logger>=2.0.13", "pytest-html>=4.1.1", - "openshift-python-wrapper>=11.0.132", + "openshift-python-wrapper>=11.0.139", "cachetools>=6.2.2", "dacite>=1.9.2", "python-dotenv>=1.2.1", diff --git a/tests/conftest.py b/tests/conftest.py index e11d4414ec..b61547a26a 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -110,6 +110,7 @@ HCO_SUBSCRIPTION, HOTFIX_STR, SSP_CR_COMMON_TEMPLATES_LIST_KEY_NAME, + HCOv1Spec, UpgradeStreams, ) from utilities.constants.images import OS_FLAVOR_RHEL @@ -1235,6 +1236,11 @@ def hyperconverged_resource_scope_session(admin_client, hco_namespace, installin return get_hyperconverged_resource(client=admin_client, hco_ns_name=hco_namespace.name) +@pytest.fixture(scope="session") +def hco_fg_phases(admin_client): + return utilities.hco.parse_hco_fg_phases(admin_client=admin_client) + + @pytest.fixture() def kubevirt_hyperconverged_spec_scope_function(admin_client, hco_namespace, installing_cnv): if not installing_cnv: @@ -1304,8 +1310,10 @@ def hyperconverged_with_node_placement(request, admin_client, hco_namespace, hyp workloads_placement = request.param["workloads"] LOGGER.info("Fetching HCO to save its initial node placement configuration ") - initial_infra = hyperconverged_resource_scope_class.instance.to_dict()["spec"].get("infra", {}) - initial_workloads = hyperconverged_resource_scope_class.instance.to_dict()["spec"].get("workloads", {}) + spec = hyperconverged_resource_scope_class.instance.to_dict()["spec"] + node_placements = HCOv1Spec.node_placements.read(spec=spec, default={}) + initial_infra = node_placements.get("infra", {}) + initial_workloads = node_placements.get("workload", {}) yield utilities.hco.apply_np_changes( admin_client=admin_client, hco=hyperconverged_resource_scope_class, diff --git a/tests/infrastructure/golden_images/update_boot_source/conftest.py b/tests/infrastructure/golden_images/update_boot_source/conftest.py index 1b04e0f57d..64dc061649 100644 --- a/tests/infrastructure/golden_images/update_boot_source/conftest.py +++ b/tests/infrastructure/golden_images/update_boot_source/conftest.py @@ -17,6 +17,7 @@ get_all_release_versions_from_docs, ) from utilities.constants import Images +from utilities.constants.hco import HCOv1Spec from utilities.constants.images import DEFAULT_FEDORA_REGISTRY_URL from utilities.constants.storage import BIND_IMMEDIATE_ANNOTATION from utilities.constants.timeouts import ( @@ -82,7 +83,9 @@ def updated_hco_with_custom_data_import_cron_scope_function( with ResourceEditorValidateHCOReconcile( admin_client=admin_client, patches={ - hyperconverged_resource_scope_function: {"spec": {"dataImportCronTemplates": [data_import_cron_dict]}} + hyperconverged_resource_scope_function: HCOv1Spec.workload_sources( + dataImportCronTemplates=[data_import_cron_dict] + ) }, list_resource_reconcile=[SSP, CDI], ): @@ -170,9 +173,9 @@ def updated_data_import_cron( ) with ResourceEditor( patches={ - hyperconverged_resource_scope_function: { - "spec": {"dataImportCronTemplates": [updated_hco_with_custom_data_import_cron_scope_function]} - } + hyperconverged_resource_scope_function: HCOv1Spec.workload_sources( + dataImportCronTemplates=[updated_hco_with_custom_data_import_cron_scope_function], + ) } ): yield diff --git a/tests/infrastructure/vhostmd/test_downwardmetrics_virtio.py b/tests/infrastructure/vhostmd/test_downwardmetrics_virtio.py index acfbb57205..40535bd186 100644 --- a/tests/infrastructure/vhostmd/test_downwardmetrics_virtio.py +++ b/tests/infrastructure/vhostmd/test_downwardmetrics_virtio.py @@ -19,6 +19,7 @@ ) from pyhelper_utils.shell import run_ssh_commands +from utilities.constants.hco import HCOv1Spec from utilities.constants.images import OS_FLAVOR_RHEL from utilities.hco import ResourceEditorValidateHCOReconcile from utilities.virt import VirtualMachineForTests, wait_for_running_vm @@ -98,7 +99,7 @@ def enabled_feature_gate_for_downward_metrics_scope_function( ): with ResourceEditorValidateHCOReconcile( admin_client=admin_client, - patches={hyperconverged_resource_scope_function: {"spec": {"featureGates": {"downwardMetrics": True}}}}, + patches={hyperconverged_resource_scope_function: HCOv1Spec.feature_gates(downwardMetrics=True)}, list_resource_reconcile=[KubeVirt], wait_for_reconcile_post_update=True, ): diff --git a/tests/infrastructure/vhostmd/test_vhostmd.py b/tests/infrastructure/vhostmd/test_vhostmd.py index 4c0a4b4ec3..79cd3fa2b3 100644 --- a/tests/infrastructure/vhostmd/test_vhostmd.py +++ b/tests/infrastructure/vhostmd/test_vhostmd.py @@ -14,6 +14,7 @@ from tests.os_params import RHEL_LATEST_LABELS from utilities.artifactory import get_artifactory_header from utilities.constants.architecture import S390X +from utilities.constants.hco import HCOv1Spec from utilities.constants.timeouts import ( TIMEOUT_3MIN, TIMEOUT_5SEC, @@ -57,7 +58,7 @@ def download_and_install_vm_dump_metrics(vm, rpm_file_name): def enabled_downward_metrics_hco_featuregate(admin_client, hyperconverged_resource_scope_module): with ResourceEditorValidateHCOReconcile( admin_client=admin_client, - patches={hyperconverged_resource_scope_module: {"spec": {"featureGates": {"downwardMetrics": True}}}}, + patches={hyperconverged_resource_scope_module: HCOv1Spec.feature_gates(downwardMetrics=True)}, list_resource_reconcile=[KubeVirt], wait_for_reconcile_post_update=True, ): diff --git a/tests/install_upgrade_operators/conftest.py b/tests/install_upgrade_operators/conftest.py index 34a640102d..6ea09c78e6 100644 --- a/tests/install_upgrade_operators/conftest.py +++ b/tests/install_upgrade_operators/conftest.py @@ -13,10 +13,7 @@ from pytest_testconfig import py_config from tests.install_upgrade_operators.constants import ( - ENABLE_MULTI_ARCH_BOOT_IMAGE_IMPORT, EXPECTED_KUBEVIRT_HARDCODED_FEATUREGATES, - FG_ENABLED, - HCO_DEFAULT_FEATUREGATES, RESOURCE_NAME_STR, RESOURCE_NAMESPACE_STR, RESOURCE_TYPE_STR, @@ -27,7 +24,6 @@ get_resource_by_name, get_resource_from_module_name, ) -from utilities.constants.architecture import MULTIARCH from utilities.constants.components import ( HCO_OPERATOR, HOSTPATH_PROVISIONER_CSI, @@ -305,9 +301,6 @@ def expected_value(request, is_s390x_cluster): expected = request.param.copy() if expected == EXPECTED_KUBEVIRT_HARDCODED_FEATUREGATES and is_s390x_cluster: expected |= S390X_SPECIFIC_KUBEVIRT_FEATUREGATES - if expected == HCO_DEFAULT_FEATUREGATES: - if py_config["cluster_type"] == MULTIARCH: - expected[ENABLE_MULTI_ARCH_BOOT_IMAGE_IMPORT] = FG_ENABLED return expected diff --git a/tests/install_upgrade_operators/constants.py b/tests/install_upgrade_operators/constants.py index b239691f6d..5706b0968b 100644 --- a/tests/install_upgrade_operators/constants.py +++ b/tests/install_upgrade_operators/constants.py @@ -12,10 +12,6 @@ DEVELOPER_CONFIGURATION = "developerConfiguration" MEDIATED_DEVICES_CONFIGURATION = "mediatedDevicesConfiguration" # featuregates: -DEPLOY_KUBE_SECONDARY_DNS = "deployKubeSecondaryDNS" -ENABLE_MULTI_ARCH_BOOT_IMAGE_IMPORT = "enableMultiArchBootImageImport" -PERSISTENT_RESERVATION = "persistentReservation" -FG_DISABLED = False FG_ENABLED = True FEATUREGATES = "featureGates" @@ -38,18 +34,6 @@ "HonorWaitForFirstConsumer", "WebhookPvcRendering", } -HCO_DEFAULT_FEATUREGATES = { - DEPLOY_KUBE_SECONDARY_DNS: FG_DISABLED, - PERSISTENT_RESERVATION: FG_DISABLED, - "alignCPUs": FG_DISABLED, - "downwardMetrics": FG_DISABLED, - ENABLE_MULTI_ARCH_BOOT_IMAGE_IMPORT: FG_DISABLED, - "decentralizedLiveMigration": FG_ENABLED, - "declarativeHotplugVolumes": FG_ENABLED, - "objectGraph": FG_DISABLED, - "incrementalBackup": FG_DISABLED, - "containerPathVolumes": FG_DISABLED, -} CUSTOM_DATASOURCE_NAME = "custom-datasource" WORKLOAD_UPDATE_STRATEGY_KEY_NAME = "workloadUpdateStrategy" KUBEMACPOOL_SERVICE = "kubemacpool-service" diff --git a/tests/install_upgrade_operators/crypto_policy/test_crypto_policy_default.py b/tests/install_upgrade_operators/crypto_policy/test_crypto_policy_default.py index 140892df3b..d000025165 100644 --- a/tests/install_upgrade_operators/crypto_policy/test_crypto_policy_default.py +++ b/tests/install_upgrade_operators/crypto_policy/test_crypto_policy_default.py @@ -11,6 +11,7 @@ from tests.install_upgrade_operators.constants import ( KEY_NAME_STR, + KEY_PATH_SEPARATOR, RESOURCE_NAME_STR, RESOURCE_NAMESPACE_STR, RESOURCE_TYPE_STR, @@ -54,7 +55,7 @@ RESOURCE_TYPE_STR: HyperConverged, RESOURCE_NAME_STR: py_config["hco_cr_name"], RESOURCE_NAMESPACE_STR: py_config["hco_namespace"], - KEY_NAME_STR: TLS_SECURITY_PROFILE, + KEY_NAME_STR: f"security{KEY_PATH_SEPARATOR}{TLS_SECURITY_PROFILE}", }, HyperConverged, marks=(pytest.mark.polarion("CNV-9464")), diff --git a/tests/install_upgrade_operators/crypto_policy/test_hco_crypto_policy_propagation.py b/tests/install_upgrade_operators/crypto_policy/test_hco_crypto_policy_propagation.py index 3344edc30f..c6a0e434a1 100644 --- a/tests/install_upgrade_operators/crypto_policy/test_hco_crypto_policy_propagation.py +++ b/tests/install_upgrade_operators/crypto_policy/test_hco_crypto_policy_propagation.py @@ -12,7 +12,7 @@ assert_crypto_policy_propagated_to_components, set_hco_crypto_policy, ) -from utilities.constants.hco import TLS_SECURITY_PROFILE +from utilities.constants.hco import TLS_SECURITY_PROFILE, HCOv1Spec LOGGER = logging.getLogger(__name__) pytestmark = [pytest.mark.post_upgrade, pytest.mark.sno, pytest.mark.s390x] @@ -22,13 +22,13 @@ def hco_crypto_policy( hyperconverged_resource_scope_function, updated_hco_crypto_policy, cnv_crypto_policy_matrix__function__ ): - tls_profile = hyperconverged_resource_scope_function.instance.spec.get(TLS_SECURITY_PROFILE) + spec = hyperconverged_resource_scope_function.instance.to_dict()["spec"] + tls_profile = HCOv1Spec.security.read(spec=spec, default={}).get(TLS_SECURITY_PROFILE) if not tls_profile: return None - tls_dict = tls_profile.to_dict() # OCP 4.22+ API adds empty profile-type keys (e.g. old: {}, custom: {}) as CRD defaults expected = CRYPTO_POLICY_SPEC_DICT[cnv_crypto_policy_matrix__function__] - return {policy_key: policy_value for policy_key, policy_value in tls_dict.items() if policy_key in expected} + return {policy_key: policy_value for policy_key, policy_value in tls_profile.items() if policy_key in expected} @pytest.fixture() diff --git a/tests/install_upgrade_operators/crypto_policy/test_hco_custom_profile_negative.py b/tests/install_upgrade_operators/crypto_policy/test_hco_custom_profile_negative.py index 6ead794716..cd17a37551 100644 --- a/tests/install_upgrade_operators/crypto_policy/test_hco_custom_profile_negative.py +++ b/tests/install_upgrade_operators/crypto_policy/test_hco_custom_profile_negative.py @@ -10,7 +10,7 @@ ) from utilities.constants.hco import ( TLS_CUSTOM_POLICY, - TLS_SECURITY_PROFILE, + HCOv1Spec, ) from utilities.hco import ResourceEditorValidateHCOReconcile @@ -33,7 +33,7 @@ def test_set_hco_crypto_failed_without_required_cipher( "ECDHE-ECDSA-AES256-GCM-SHA384", "ECDHE-RSA-AES256-GCM-SHA384", ] - tls_spec = {"spec": {TLS_SECURITY_PROFILE: tls_custom_profile}} + tls_spec = HCOv1Spec.security(tlsSecurityProfile=tls_custom_profile) with pytest.raises(ForbiddenError, match=r"missing an HTTP/2-required"): with ResourceEditorValidateHCOReconcile( admin_client=admin_client, @@ -54,7 +54,7 @@ def test_set_ciphers_for_tlsv13(admin_client, hyperconverged_resource_scope_func with pytest.raises(ForbiddenError, match=error_string): with ResourceEditorValidateHCOReconcile( admin_client=admin_client, - patches={hyperconverged_resource_scope_function: {"spec": {TLS_SECURITY_PROFILE: tls_custom_profile}}}, + patches={hyperconverged_resource_scope_function: HCOv1Spec.security(tlsSecurityProfile=tls_custom_profile)}, ): LOGGER.error( "Setting HCO with custom tlsSecurityProfile with TLS Version 1.3 " diff --git a/tests/install_upgrade_operators/crypto_policy/utils.py b/tests/install_upgrade_operators/crypto_policy/utils.py index 9502ac4cd1..703a393f3d 100644 --- a/tests/install_upgrade_operators/crypto_policy/utils.py +++ b/tests/install_upgrade_operators/crypto_policy/utils.py @@ -31,7 +31,7 @@ get_resource_key_value, ) from utilities.constants.components import CLUSTER -from utilities.constants.hco import TLS_SECURITY_PROFILE +from utilities.constants.hco import TLS_SECURITY_PROFILE, HCOv1Spec from utilities.constants.timeouts import ( TIMEOUT_2MIN, TIMEOUT_60MIN, @@ -266,7 +266,7 @@ def assert_tls_ciphers_blocked(utility_pods, node, services, tls_version, allowe def set_hco_crypto_policy(admin_client, hco_resource, tls_spec): with ResourceEditorValidateHCOReconcile( admin_client=admin_client, - patches={hco_resource: {"spec": {TLS_SECURITY_PROFILE: tls_spec}}}, + patches={hco_resource: HCOv1Spec.security(tlsSecurityProfile=tls_spec)}, wait_for_reconcile_post_update=True, list_resource_reconcile=MANAGED_CRS_LIST, ): diff --git a/tests/install_upgrade_operators/csv/test_hco_api_version.py b/tests/install_upgrade_operators/csv/test_hco_api_version.py index 00778c747a..42b744d486 100644 --- a/tests/install_upgrade_operators/csv/test_hco_api_version.py +++ b/tests/install_upgrade_operators/csv/test_hco_api_version.py @@ -7,6 +7,6 @@ @pytest.mark.polarion("CNV-5832") def test_hyperconverged_cr_api_version(hyperconverged_resource_scope_function): """ - This test will check the Hyperconverged CR's api_version for v1beta1 + This test will check the Hyperconverged CR's api_version for v1 """ - assert Resource.ApiVersion.V1BETA1 in hyperconverged_resource_scope_function.instance.apiVersion + assert hyperconverged_resource_scope_function.instance.apiVersion.endswith(f"/{Resource.ApiVersion.V1}") diff --git a/tests/install_upgrade_operators/feature_gates/test_update_featuregate_hco.py b/tests/install_upgrade_operators/feature_gates/test_update_featuregate_hco.py index 6a16c1a0fa..61384ad723 100644 --- a/tests/install_upgrade_operators/feature_gates/test_update_featuregate_hco.py +++ b/tests/install_upgrade_operators/feature_gates/test_update_featuregate_hco.py @@ -6,7 +6,7 @@ FG_ENABLED, MEDIATED_DEVICES_CONFIGURATION, ) -from utilities.constants.hco import DISABLE_MDEV_CONFIGURATION +from utilities.constants.hco import DISABLE_MDEV_CONFIGURATION, HCOv1Spec from utilities.hco import ResourceEditorValidateHCOReconcile pytestmark = [pytest.mark.s390x, pytest.mark.skip_must_gather_collection] @@ -20,7 +20,7 @@ def updated_fg_hco( ): with ResourceEditorValidateHCOReconcile( admin_client=admin_client, - patches={hyperconverged_resource_scope_function: {"spec": {FEATUREGATES: request.param["featuregate"]}}}, + patches={hyperconverged_resource_scope_function: request.param["patch"]}, list_resource_reconcile=[KubeVirt], wait_for_reconcile_post_update=True, ): @@ -31,7 +31,7 @@ def updated_fg_hco( "updated_fg_hco", [ pytest.param( - {"featuregate": {DISABLE_MDEV_CONFIGURATION: FG_ENABLED}}, + {"patch": HCOv1Spec.feature_gates(disableMDevConfiguration=FG_ENABLED)}, marks=pytest.mark.polarion("CNV-10091"), id="test_enable_fg_disable_mdev_config_hco", ), @@ -41,10 +41,12 @@ def updated_fg_hco( def test_enable_fg_hco( updated_fg_hco, hco_spec, + hco_fg_phases, kubevirt_resource, ): - assert hco_spec[FEATUREGATES][DISABLE_MDEV_CONFIGURATION] is True, ( - f"HCO featureGates.{DISABLE_MDEV_CONFIGURATION} is not True: {hco_spec[FEATUREGATES]}" + fg_list = hco_spec[FEATUREGATES] + assert HCOv1Spec.is_fg_enabled(feature_gates=fg_list, name=DISABLE_MDEV_CONFIGURATION, fg_phases=hco_fg_phases), ( + f"HCO featureGates.{DISABLE_MDEV_CONFIGURATION} is not enabled: {fg_list}" ) kubevirt_mdev_enabled = kubevirt_resource.instance.spec["configuration"][MEDIATED_DEVICES_CONFIGURATION]["enabled"] diff --git a/tests/install_upgrade_operators/hco_enablement_golden_image_updates/test_enable_common_boot_image_import.py b/tests/install_upgrade_operators/hco_enablement_golden_image_updates/test_enable_common_boot_image_import.py index 6209eb4970..50a9d1ab81 100644 --- a/tests/install_upgrade_operators/hco_enablement_golden_image_updates/test_enable_common_boot_image_import.py +++ b/tests/install_upgrade_operators/hco_enablement_golden_image_updates/test_enable_common_boot_image_import.py @@ -6,6 +6,7 @@ COMMON_TEMPLATES_KEY_NAME, ENABLE_COMMON_BOOT_IMAGE_IMPORT, SSP_CR_COMMON_TEMPLATES_LIST_KEY_NAME, + HCOv1Spec, ) from utilities.hco import wait_for_auto_boot_config_stabilization @@ -33,6 +34,7 @@ def test_enable_and_delete_spec_enable_common_boot_image_import_hco_cr( hyperconverged_resource_scope_function, ): wait_for_auto_boot_config_stabilization(admin_client=admin_client, hco_namespace=hco_namespace) - assert not hyperconverged_resource_scope_function.instance.spec[ENABLE_COMMON_BOOT_IMAGE_IMPORT], ( + spec = hyperconverged_resource_scope_function.instance.to_dict()["spec"] + assert not HCOv1Spec.workload_sources.read(spec=spec, default={}).get(ENABLE_COMMON_BOOT_IMAGE_IMPORT, True), ( f"Spec {ENABLE_COMMON_BOOT_IMAGE_IMPORT} was not disabled in HCO." ) diff --git a/tests/install_upgrade_operators/launcher_updates/constants.py b/tests/install_upgrade_operators/launcher_updates/constants.py index 86a8730b59..ca680c0fc8 100644 --- a/tests/install_upgrade_operators/launcher_updates/constants.py +++ b/tests/install_upgrade_operators/launcher_updates/constants.py @@ -45,4 +45,4 @@ MOD_CUST_DEFAULT_WORKLOAD_UPDATE_METHOD[WORKLOADUPDATEMETHODS] = DEFAULT_WORKLOAD_UPDATE_METHODS CUSTOM_STRATEGY = {WORKLOAD_UPDATE_STRATEGY_KEY_NAME: CUSTOM_WORKLOAD_UPDATE_STRATEGY} -CUSTOM_WORKLOAD_STRATEGY_SPEC = {"spec": CUSTOM_STRATEGY} +CUSTOM_WORKLOAD_STRATEGY_SPEC = {"spec": {"virtualization": CUSTOM_STRATEGY}} diff --git a/tests/install_upgrade_operators/launcher_updates/test_negative_kubevirt_update.py b/tests/install_upgrade_operators/launcher_updates/test_negative_kubevirt_update.py index 3a1c4fcb08..3b9c4ba1e9 100644 --- a/tests/install_upgrade_operators/launcher_updates/test_negative_kubevirt_update.py +++ b/tests/install_upgrade_operators/launcher_updates/test_negative_kubevirt_update.py @@ -76,7 +76,7 @@ def test_hyperconverged_reset_custom_workload_update_strategy( wait_for_spec_change( expected=CUSTOM_WORKLOAD_UPDATE_STRATEGY, get_spec_func=lambda: get_hco_spec(admin_client=admin_client, hco_namespace=hco_namespace), - base_path=[WORKLOAD_UPDATE_STRATEGY_KEY_NAME], + base_path=["virtualization", WORKLOAD_UPDATE_STRATEGY_KEY_NAME], ) wait_for_spec_change( expected=CUSTOM_WORKLOAD_UPDATE_STRATEGY, diff --git a/tests/install_upgrade_operators/launcher_updates/test_reset_custom_values.py b/tests/install_upgrade_operators/launcher_updates/test_reset_custom_values.py index 351816a587..f04128f320 100644 --- a/tests/install_upgrade_operators/launcher_updates/test_reset_custom_values.py +++ b/tests/install_upgrade_operators/launcher_updates/test_reset_custom_values.py @@ -20,14 +20,16 @@ class TestLauncherUpdateResetFields: [ pytest.param( { - "patch": {"spec": {WORKLOAD_UPDATE_STRATEGY_KEY_NAME: None}}, + "patch": {"spec": {"virtualization": {WORKLOAD_UPDATE_STRATEGY_KEY_NAME: None}}}, }, DEFAULT_WORKLOAD_UPDATE_STRATEGY, marks=(pytest.mark.polarion("CNV-6928"),), ), pytest.param( { - "patch": {"spec": {WORKLOAD_UPDATE_STRATEGY_KEY_NAME: {"batchEvictionInterval": None}}}, + "patch": { + "spec": {"virtualization": {WORKLOAD_UPDATE_STRATEGY_KEY_NAME: {"batchEvictionInterval": None}}} + }, }, MOD_CUST_DEFAULT_BATCH_EVICTION_INTERVAL, marks=pytest.mark.polarion("CNV-6929"), @@ -35,7 +37,9 @@ class TestLauncherUpdateResetFields: ), pytest.param( { - "patch": {"spec": {WORKLOAD_UPDATE_STRATEGY_KEY_NAME: {"batchEvictionSize": None}}}, + "patch": { + "spec": {"virtualization": {WORKLOAD_UPDATE_STRATEGY_KEY_NAME: {"batchEvictionSize": None}}} + }, }, MOD_CUST_DEFAULT_BATCH_EVICTION_SIZE, marks=pytest.mark.polarion("CNV-6930"), @@ -43,7 +47,9 @@ class TestLauncherUpdateResetFields: ), pytest.param( { - "patch": {"spec": {WORKLOAD_UPDATE_STRATEGY_KEY_NAME: {WORKLOADUPDATEMETHODS: None}}}, + "patch": { + "spec": {"virtualization": {WORKLOAD_UPDATE_STRATEGY_KEY_NAME: {WORKLOADUPDATEMETHODS: None}}} + }, }, MOD_CUST_DEFAULT_WORKLOAD_UPDATE_METHOD, marks=pytest.mark.polarion("CNV-6931"), @@ -63,7 +69,7 @@ def test_hyperconverged_reset_custom_workload_update_strategy( wait_for_spec_change( expected=expected, get_spec_func=lambda: get_hco_spec(admin_client=admin_client, hco_namespace=hco_namespace), - base_path=[WORKLOAD_UPDATE_STRATEGY_KEY_NAME], + base_path=["virtualization", WORKLOAD_UPDATE_STRATEGY_KEY_NAME], ) wait_for_spec_change( expected=expected, diff --git a/tests/install_upgrade_operators/must_gather/conftest.py b/tests/install_upgrade_operators/must_gather/conftest.py index ebdbf7bef5..3a99277cda 100644 --- a/tests/install_upgrade_operators/must_gather/conftest.py +++ b/tests/install_upgrade_operators/must_gather/conftest.py @@ -21,6 +21,7 @@ get_must_gather_dir, ) from tests.utils import create_vms +from utilities.constants.hco import HCOv1Spec from utilities.constants.networking import LINUX_BRIDGE from utilities.constants.timeouts import TIMEOUT_40MIN from utilities.exceptions import MissingResourceException @@ -639,14 +640,10 @@ def must_gather_vm_files_path(collected_vm_details_must_gather, vm_for_migration @pytest.fixture(scope="class") def updated_disable_serial_console_log_false(admin_client, hyperconverged_resource_scope_class): - if hyperconverged_resource_scope_class.instance.spec.virtualMachineOptions.disableSerialConsoleLog: + if hyperconverged_resource_scope_class.instance.spec.virtualization.virtualMachineOptions.disableSerialConsoleLog: with ResourceEditorValidateHCOReconcile( admin_client=admin_client, - patches={ - hyperconverged_resource_scope_class: { - "spec": {"virtualMachineOptions": {"disableSerialConsoleLog": False}} - } - }, + patches={hyperconverged_resource_scope_class: HCOv1Spec.vm_options(disableSerialConsoleLog=False)}, ): yield else: diff --git a/tests/install_upgrade_operators/node_component/conftest.py b/tests/install_upgrade_operators/node_component/conftest.py index 293be0f383..1664370665 100644 --- a/tests/install_upgrade_operators/node_component/conftest.py +++ b/tests/install_upgrade_operators/node_component/conftest.py @@ -29,6 +29,7 @@ from utilities.constants.hco import ( HCO_SUBSCRIPTION, IMAGE_CRON_STR, + HCOv1Spec, ) from utilities.constants.timeouts import TIMEOUT_2MIN, TIMEOUT_5MIN, TIMEOUT_10SEC from utilities.hco import add_labels_to_nodes, apply_np_changes, wait_for_hco_conditions @@ -216,8 +217,10 @@ def hyperconverged_resource_before_np(admin_client, hco_namespace, hyperconverge Update HCO CR with infrastructure and workloads spec. """ LOGGER.info("Fetching HCO to save its initial node placement configuration ") - initial_infra = hyperconverged_resource_scope_class.instance.to_dict()["spec"].get("infra", {}) - initial_workloads = hyperconverged_resource_scope_class.instance.to_dict()["spec"].get("workloads", {}) + hco_spec = hyperconverged_resource_scope_class.instance.to_dict()["spec"] + node_placements = HCOv1Spec.node_placements.read(spec=hco_spec, default={}) + initial_infra = node_placements.get("infra", {}) + initial_workloads = node_placements.get("workload", {}) yield hyperconverged_resource_scope_class LOGGER.info("Revert to initial HCO node placement configuration ") apply_np_changes( diff --git a/tests/install_upgrade_operators/node_component/test_deploy_cnv_on_subset_of_nodes_sanity.py b/tests/install_upgrade_operators/node_component/test_deploy_cnv_on_subset_of_nodes_sanity.py index 5d80ec8b73..3697255abc 100644 --- a/tests/install_upgrade_operators/node_component/test_deploy_cnv_on_subset_of_nodes_sanity.py +++ b/tests/install_upgrade_operators/node_component/test_deploy_cnv_on_subset_of_nodes_sanity.py @@ -20,6 +20,7 @@ verify_all_components_on_node, verify_no_components_on_nodes, ) +from utilities.constants.hco import HCOv1Spec from utilities.hco import ResourceEditorValidateHCOReconcile pytestmark = [pytest.mark.post_upgrade, pytest.mark.gating, pytest.mark.arm64, pytest.mark.s390x] @@ -165,7 +166,7 @@ def test_workload_components_selection_change_denied_with_workloads( try: with ResourceEditorValidateHCOReconcile( admin_client=admin_client, - patches={hyperconverged_resource_scope_function: {"spec": {"workloads": WORK_LABEL_1}}}, + patches={hyperconverged_resource_scope_function: HCOv1Spec.node_placements(workload=WORK_LABEL_1)}, ): LOGGER.info("Expected ability to change workloads label {WORK_LABEL_1} while VM/Workload is present.") except ForbiddenError: diff --git a/tests/install_upgrade_operators/product_uninstall/test_remove_hco.py b/tests/install_upgrade_operators/product_uninstall/test_remove_hco.py index 8f145952db..1ed4873d1c 100644 --- a/tests/install_upgrade_operators/product_uninstall/test_remove_hco.py +++ b/tests/install_upgrade_operators/product_uninstall/test_remove_hco.py @@ -32,11 +32,14 @@ def assert_expected_strategy(resource_objects, expected_strategy): - incorrect_components = { - component: resource_obj.instance.spec.uninstallStrategy - for component, resource_obj in resource_objects.items() - if resource_obj.instance.spec.uninstallStrategy != expected_strategy - } + incorrect_components = {} + for component, resource_obj in resource_objects.items(): + if isinstance(resource_obj, HyperConverged): + strategy = resource_obj.instance.spec.deployment.uninstallStrategy + else: + strategy = resource_obj.instance.spec.uninstallStrategy + if strategy != expected_strategy: + incorrect_components[component] = strategy assert not incorrect_components, ( f"Incorrect uninstallStrategy found for following component(s) {incorrect_components}" @@ -136,7 +139,9 @@ def hco_uninstall_strategy_remove_workloads( ): with ResourceEditorValidateHCOReconcile( admin_client=admin_client, - patches={hyperconverged_resource_scope_function: {"spec": {"uninstallStrategy": REMOVE_STRATEGY}}}, + patches={ + hyperconverged_resource_scope_function: {"spec": {"deployment": {"uninstallStrategy": REMOVE_STRATEGY}}} + }, ): wait_for_hco_conditions( admin_client=admin_client, diff --git a/tests/install_upgrade_operators/product_uninstall/test_remove_kubevirt.py b/tests/install_upgrade_operators/product_uninstall/test_remove_kubevirt.py index e725bde2db..2753e52f7e 100644 --- a/tests/install_upgrade_operators/product_uninstall/test_remove_kubevirt.py +++ b/tests/install_upgrade_operators/product_uninstall/test_remove_kubevirt.py @@ -17,7 +17,9 @@ def set_uninstall_strategy_remove_workloads(admin_client, hyperconverged_resource_scope_function): with ResourceEditorValidateHCOReconcile( admin_client=admin_client, - patches={hyperconverged_resource_scope_function: {"spec": {"uninstallStrategy": "RemoveWorkloads"}}}, + patches={ + hyperconverged_resource_scope_function: {"spec": {"deployment": {"uninstallStrategy": "RemoveWorkloads"}}} + }, list_resource_reconcile=[CDI, KubeVirt], wait_for_reconcile_post_update=True, ) as edits: diff --git a/tests/install_upgrade_operators/strict_reconciliation/conftest.py b/tests/install_upgrade_operators/strict_reconciliation/conftest.py index 7462c2a844..4fbb62d84b 100644 --- a/tests/install_upgrade_operators/strict_reconciliation/conftest.py +++ b/tests/install_upgrade_operators/strict_reconciliation/conftest.py @@ -124,10 +124,12 @@ def hco_with_non_default_feature_gates( hyperconverged_resource_scope_function, ): new_fgs = request.param["fgs"] - hco_fgs = hyperconverged_resource_scope_function.instance.to_dict()["spec"]["featureGates"] + hco_fgs = list(hyperconverged_resource_scope_function.instance.to_dict()["spec"].get("featureGates", [])) + existing_names = {fg["name"] for fg in hco_fgs} - for fg in new_fgs: - hco_fgs[fg] = True + for fg_name in new_fgs: + if fg_name not in existing_names: + hco_fgs.append({"name": fg_name}) with ResourceEditorValidateHCOReconcile( admin_client=admin_client, patches={hyperconverged_resource_scope_function: {"spec": {"featureGates": hco_fgs}}}, diff --git a/tests/install_upgrade_operators/strict_reconciliation/constants.py b/tests/install_upgrade_operators/strict_reconciliation/constants.py index 751203c0dc..f727d2effd 100644 --- a/tests/install_upgrade_operators/strict_reconciliation/constants.py +++ b/tests/install_upgrade_operators/strict_reconciliation/constants.py @@ -121,8 +121,8 @@ CUSTOM_HCO_CR_SPEC = { "spec": { - LIVE_MIGRATION_CONFIG_KEY: EXPCT_LM_CUSTOM, - HCO_CR_CERT_CONFIG_KEY: EXPCT_CERTC_CUSTOM, + "virtualization": {LIVE_MIGRATION_CONFIG_KEY: EXPCT_LM_CUSTOM}, + "security": {HCO_CR_CERT_CONFIG_KEY: EXPCT_CERTC_CUSTOM}, } } KUBEVIRT_DEFAULT = {KUBEVIRT_CR_CERT_CONFIG_SELF_SIGNED_KEY: EXPCT_CERTC_DEFAULTS} @@ -252,7 +252,7 @@ NP_INFRA_VALUE_HCO_CR = { "nodePlacement": NP_INFRA_VALUE_CDI_CR, } -NP_WORKLOADS_KEY_HCO_CR = "workloads" +NP_WORKLOADS_KEY_HCO_CR = "workload" NP_WORKLOADS_KEY_CDI_CR = "workload" NP_WORKLOADS_VALUE_HCO_CR = { "nodePlacement": { diff --git a/tests/install_upgrade_operators/strict_reconciliation/test_hco_cr_modify_defaults.py b/tests/install_upgrade_operators/strict_reconciliation/test_hco_cr_modify_defaults.py index 734f7c53c3..e7e455246f 100644 --- a/tests/install_upgrade_operators/strict_reconciliation/test_hco_cr_modify_defaults.py +++ b/tests/install_upgrade_operators/strict_reconciliation/test_hco_cr_modify_defaults.py @@ -65,12 +65,12 @@ class TestOperatorsModify: [ pytest.param( { - "patch": {"spec": {HCO_CR_CERT_CONFIG_KEY: EXPCT_CERTC_DEFAULTS}}, + "patch": {"spec": {"security": {HCO_CR_CERT_CONFIG_KEY: EXPCT_CERTC_DEFAULTS}}}, }, { "hco_spec": { "expected": EXPCT_CERTC_DEFAULTS, - "base_path": [HCO_CR_CERT_CONFIG_KEY], + "base_path": ["security", HCO_CR_CERT_CONFIG_KEY], }, "kubevirt_spec": { "expected": KUBEVIRT_DEFAULT, @@ -86,10 +86,12 @@ class TestOperatorsModify: { "patch": { "spec": { - HCO_CR_CERT_CONFIG_KEY: { - HCO_CR_CERT_CONFIG_CA_KEY: { - HCO_CR_CERT_CONFIG_DURATION_KEY: CERTC_DEFAULT_48H, - }, + "security": { + HCO_CR_CERT_CONFIG_KEY: { + HCO_CR_CERT_CONFIG_CA_KEY: { + HCO_CR_CERT_CONFIG_DURATION_KEY: CERTC_DEFAULT_48H, + }, + } } } }, @@ -97,7 +99,7 @@ class TestOperatorsModify: { "hco_spec": { "expected": HCO_MOD_DEFAULT_CA_DUR, - "base_path": [HCO_CR_CERT_CONFIG_KEY], + "base_path": ["security", HCO_CR_CERT_CONFIG_KEY], }, "kubevirt_spec": { "expected": KV_MOD_DEFAULT_CA_DUR, @@ -113,10 +115,12 @@ class TestOperatorsModify: { "patch": { "spec": { - HCO_CR_CERT_CONFIG_KEY: { - HCO_CR_CERT_CONFIG_CA_KEY: { - HCO_CR_CERT_CONFIG_RENEW_BEFORE_KEY: CERTC_DEFAULT_24H, - }, + "security": { + HCO_CR_CERT_CONFIG_KEY: { + HCO_CR_CERT_CONFIG_CA_KEY: { + HCO_CR_CERT_CONFIG_RENEW_BEFORE_KEY: CERTC_DEFAULT_24H, + }, + } } } }, @@ -124,7 +128,7 @@ class TestOperatorsModify: { "hco_spec": { "expected": HCO_MOD_DEFAULT_CA_RB, - "base_path": HCO_CR_CERT_CONFIG_KEY, + "base_path": ["security", HCO_CR_CERT_CONFIG_KEY], }, "kubevirt_spec": { "expected": KV_MOD_DEFAULT_CA_RB, @@ -140,10 +144,12 @@ class TestOperatorsModify: { "patch": { "spec": { - HCO_CR_CERT_CONFIG_KEY: { - HCO_CR_CERT_CONFIG_SERVER_KEY: { - HCO_CR_CERT_CONFIG_DURATION_KEY: CERTC_DEFAULT_24H, - }, + "security": { + HCO_CR_CERT_CONFIG_KEY: { + HCO_CR_CERT_CONFIG_SERVER_KEY: { + HCO_CR_CERT_CONFIG_DURATION_KEY: CERTC_DEFAULT_24H, + }, + } } } }, @@ -151,7 +157,7 @@ class TestOperatorsModify: { "hco_spec": { "expected": HCO_MOD_DEFAULT_SER_DUR, - "base_path": HCO_CR_CERT_CONFIG_KEY, + "base_path": ["security", HCO_CR_CERT_CONFIG_KEY], }, "kubevirt_spec": { "expected": KV_MOD_DEFAULT_SER_DUR, @@ -167,10 +173,12 @@ class TestOperatorsModify: { "patch": { "spec": { - HCO_CR_CERT_CONFIG_KEY: { - HCO_CR_CERT_CONFIG_SERVER_KEY: { - HCO_CR_CERT_CONFIG_RENEW_BEFORE_KEY: CERTC_DEFAULT_12H, - }, + "security": { + HCO_CR_CERT_CONFIG_KEY: { + HCO_CR_CERT_CONFIG_SERVER_KEY: { + HCO_CR_CERT_CONFIG_RENEW_BEFORE_KEY: CERTC_DEFAULT_12H, + }, + } } } }, @@ -178,7 +186,7 @@ class TestOperatorsModify: { "hco_spec": { "expected": HCO_MOD_DEFAULT_SER_RB, - "base_path": HCO_CR_CERT_CONFIG_KEY, + "base_path": ["security", HCO_CR_CERT_CONFIG_KEY], }, "kubevirt_spec": { "expected": KV_MOD_DEFAULT_SER_RB, @@ -191,11 +199,11 @@ class TestOperatorsModify: id="Test_Modify_HCO_CR_CertConfig_server_renewBefore", ), pytest.param( - {"patch": {"spec": {LIVE_MIGRATION_CONFIG_KEY: EXPCT_LM_DEFAULTS}}}, + {"patch": {"spec": {"virtualization": {LIVE_MIGRATION_CONFIG_KEY: EXPCT_LM_DEFAULTS}}}}, { "hco_spec": { "expected": EXPCT_LM_DEFAULTS, - "base_path": LIVE_MIGRATION_CONFIG_KEY, + "base_path": ["virtualization", LIVE_MIGRATION_CONFIG_KEY], }, "kubevirt_spec": { "expected": EXPCT_LM_DEFAULTS, @@ -214,8 +222,10 @@ class TestOperatorsModify: { "patch": { "spec": { - LIVE_MIGRATION_CONFIG_KEY: { - COMPLETION_TIMEOUT_PER_GIB_KEY: LM_COMPLETIONTIMEOUTPERGIB_DEFAULT, + "virtualization": { + LIVE_MIGRATION_CONFIG_KEY: { + COMPLETION_TIMEOUT_PER_GIB_KEY: LM_COMPLETIONTIMEOUTPERGIB_DEFAULT, + } } } } @@ -223,7 +233,7 @@ class TestOperatorsModify: { "hco_spec": { "expected": LM_CUST_DEFAULT_C, - "base_path": LIVE_MIGRATION_CONFIG_KEY, + "base_path": ["virtualization", LIVE_MIGRATION_CONFIG_KEY], }, "kubevirt_spec": { "expected": LM_CUST_DEFAULT_C, @@ -242,8 +252,10 @@ class TestOperatorsModify: { "patch": { "spec": { - LIVE_MIGRATION_CONFIG_KEY: { - PARALLEL_MIGRATIONS_PER_CLUSTER_KEY: LM_PARALLELMIGRATIONSPERCLUSTER_DEFAULT, + "virtualization": { + LIVE_MIGRATION_CONFIG_KEY: { + PARALLEL_MIGRATIONS_PER_CLUSTER_KEY: LM_PARALLELMIGRATIONSPERCLUSTER_DEFAULT, + } } } } @@ -251,7 +263,7 @@ class TestOperatorsModify: { "hco_spec": { "expected": LM_CUST_DEFAULT_PM, - "base_path": LIVE_MIGRATION_CONFIG_KEY, + "base_path": ["virtualization", LIVE_MIGRATION_CONFIG_KEY], }, "kubevirt_spec": { "expected": LM_CUST_DEFAULT_PM, @@ -267,11 +279,11 @@ class TestOperatorsModify: id="Test_Modify_HCO_CR_liveMigrationConfig_parallelMigrationsPerCluster", ), pytest.param( - {"patch": {"spec": {LIVE_MIGRATION_CONFIG_KEY: LM_PO_DEFAULT}}}, + {"patch": {"spec": {"virtualization": {LIVE_MIGRATION_CONFIG_KEY: LM_PO_DEFAULT}}}}, { "hco_spec": { "expected": LM_CUST_DEFAULT_PO, - "base_path": LIVE_MIGRATION_CONFIG_KEY, + "base_path": ["virtualization", LIVE_MIGRATION_CONFIG_KEY], }, "kubevirt_spec": { "expected": LM_CUST_DEFAULT_PO, @@ -290,8 +302,10 @@ class TestOperatorsModify: { "patch": { "spec": { - LIVE_MIGRATION_CONFIG_KEY: { - PROGRESS_TIMEOUT_KEY: LM_PROGRESSTIMEOUT_DEFAULT, + "virtualization": { + LIVE_MIGRATION_CONFIG_KEY: { + PROGRESS_TIMEOUT_KEY: LM_PROGRESSTIMEOUT_DEFAULT, + } } } } @@ -299,7 +313,7 @@ class TestOperatorsModify: { "hco_spec": { "expected": LM_CUST_DEFAULT_PT, - "base_path": LIVE_MIGRATION_CONFIG_KEY, + "base_path": ["virtualization", LIVE_MIGRATION_CONFIG_KEY], }, "kubevirt_spec": { "expected": LM_CUST_DEFAULT_PT, diff --git a/tests/install_upgrade_operators/strict_reconciliation/test_hco_cr_nondefault_fields.py b/tests/install_upgrade_operators/strict_reconciliation/test_hco_cr_nondefault_fields.py index 0d56a7b938..103dfcfe04 100644 --- a/tests/install_upgrade_operators/strict_reconciliation/test_hco_cr_nondefault_fields.py +++ b/tests/install_upgrade_operators/strict_reconciliation/test_hco_cr_nondefault_fields.py @@ -28,7 +28,7 @@ CDI_KUBEVIRT_HYPERCONVERGED, KUBEVIRT_HCO_NAME, ) -from utilities.constants.hco import RESOURCE_REQUIREMENTS_KEY_HCO_CR +from utilities.constants.hco import RESOURCE_REQUIREMENTS_KEY_HCO_CR, HCOv1Spec pytestmark = [pytest.mark.post_upgrade, pytest.mark.sno, pytest.mark.arm64, pytest.mark.s390x] @@ -78,11 +78,9 @@ class TestHCONonDefaultFields: ), pytest.param( { - "rpatch": { - "spec": { - SCRATCH_SPACE_STORAGE_CLASS_KEY: SCRATCH_SPACE_STORAGE_CLASS_VALUE, - } - }, + "rpatch": HCOv1Spec.storage( + scratchSpaceStorageClass=SCRATCH_SPACE_STORAGE_CLASS_VALUE, + ), "list_resource_reconcile": [CDI], }, {"resource_class": CDI, "resource_name": CDI_KUBEVIRT_HYPERCONVERGED}, @@ -93,11 +91,9 @@ class TestHCONonDefaultFields: ), pytest.param( { - "rpatch": { - "spec": { - OBSOLETE_CPUS_KEY: OBSOLETE_CPUS_VALUE_HCO_CR, - } - }, + "rpatch": HCOv1Spec.virtualization( + obsoleteCPUs=OBSOLETE_CPUS_VALUE_HCO_CR, + ), "list_resource_reconcile": [KubeVirt], }, {"resource_class": KubeVirt, "resource_name": KUBEVIRT_HCO_NAME}, @@ -108,11 +104,9 @@ class TestHCONonDefaultFields: ), pytest.param( { - "rpatch": { - "spec": { - STORAGE_IMPORT_KEY_HCO_CR: STORAGE_IMPORT_VALUE, - } - }, + "rpatch": HCOv1Spec.storage( + storageImport=STORAGE_IMPORT_VALUE, + ), "list_resource_reconcile": [CDI], }, {"resource_class": CDI, "resource_name": CDI_KUBEVIRT_HYPERCONVERGED}, @@ -123,11 +117,9 @@ class TestHCONonDefaultFields: ), pytest.param( { - "rpatch": { - "spec": { - NP_INFRA_KEY: NP_INFRA_VALUE_HCO_CR, - } - }, + "rpatch": HCOv1Spec.node_placements( + infra=NP_INFRA_VALUE_HCO_CR, + ), "list_resource_reconcile": [CDI, KubeVirt], "wait_for_reconcile": False, }, @@ -139,11 +131,9 @@ class TestHCONonDefaultFields: ), pytest.param( { - "rpatch": { - "spec": { - NP_WORKLOADS_KEY_HCO_CR: NP_WORKLOADS_VALUE_HCO_CR, - } - }, + "rpatch": HCOv1Spec.node_placements( + workload=NP_WORKLOADS_VALUE_HCO_CR, + ), "list_resource_reconcile": [CDI, KubeVirt], "wait_for_reconcile": False, }, diff --git a/tests/install_upgrade_operators/strict_reconciliation/test_hco_default_cpu_model.py b/tests/install_upgrade_operators/strict_reconciliation/test_hco_default_cpu_model.py index 209600b9b3..78ba418e70 100644 --- a/tests/install_upgrade_operators/strict_reconciliation/test_hco_default_cpu_model.py +++ b/tests/install_upgrade_operators/strict_reconciliation/test_hco_default_cpu_model.py @@ -3,7 +3,7 @@ from ocp_resources.virtual_machine import VirtualMachine from utilities.constants.architecture import ARM_64 -from utilities.constants.hco import HCO_DEFAULT_CPU_MODEL_KEY +from utilities.constants.hco import HCO_DEFAULT_CPU_MODEL_KEY, HCOv1Spec from utilities.hco import ResourceEditorValidateHCOReconcile from utilities.virt import VirtualMachineForTests, fedora_vm_body, running_vm @@ -15,7 +15,9 @@ def assert_updated_hco_default_cpu_model(hco_resource, expected_cpu_model): - hco_cpu_model = hco_resource.instance.spec.get(HCO_DEFAULT_CPU_MODEL_KEY) + spec = hco_resource.instance.to_dict()["spec"] + vm_options = HCOv1Spec.vm_options.read(spec=spec, default={}) + hco_cpu_model = vm_options.get(HCO_DEFAULT_CPU_MODEL_KEY) assert hco_cpu_model == expected_cpu_model, ( f"HCO CPU model: '{hco_cpu_model}' doesn't match with expected CPU model: '{expected_cpu_model}" ) @@ -29,7 +31,9 @@ def assert_vmi_cpu_model(vmi_resource, expected_cpu_model): def assert_kubevirt_cpu_model(kubevirt_resource, hco_resource): - hco_cpu_model = hco_resource.instance.spec.get(HCO_DEFAULT_CPU_MODEL_KEY) + spec = hco_resource.instance.to_dict()["spec"] + vm_options = HCOv1Spec.vm_options.read(spec=spec, default={}) + hco_cpu_model = vm_options.get(HCO_DEFAULT_CPU_MODEL_KEY) kubevirt_cpu_model = kubevirt_resource.instance.spec.configuration.get(KUBEVIRT_CPU_MODEL_KEY) assert kubevirt_cpu_model == hco_cpu_model, ( f"Kubevirt CPU model '{kubevirt_cpu_model}' doesn't match with the expected CPU model '{hco_cpu_model}'" @@ -82,11 +86,9 @@ def hco_with_default_cpu_model_set( with ResourceEditorValidateHCOReconcile( admin_client=admin_client, patches={ - hyperconverged_resource_scope_function: { - "spec": { - HCO_DEFAULT_CPU_MODEL_KEY: cluster_common_node_cpu, - }, - } + hyperconverged_resource_scope_function: HCOv1Spec.vm_options( + defaultCPUModel=cluster_common_node_cpu, + ), }, wait_for_reconcile_post_update=True, list_resource_reconcile=[KubeVirt], @@ -107,9 +109,10 @@ def test_default_value_for_cpu_model( and for VMI should be 'host-model' for AMD64 cluster and 'host-passthrough' for ARM64 cluster """ - assert HCO_DEFAULT_CPU_MODEL_KEY not in hco_spec_scope_module, ( + hco_vm_options = HCOv1Spec.vm_options.read(spec=hco_spec_scope_module, default={}) + assert HCO_DEFAULT_CPU_MODEL_KEY not in hco_vm_options, ( f"HCO is not expected to contain default value for '{HCO_DEFAULT_CPU_MODEL_KEY}', " - f"HCO spec values are: {hco_spec_scope_module}" + f"HCO virtualMachineOptions: {hco_vm_options}" ) assert KUBEVIRT_CPU_MODEL_KEY not in kubevirt_hyperconverged_spec_scope_module["configuration"], ( f"Kubevirt is not expected to default value for '{KUBEVIRT_CPU_MODEL_KEY}', " diff --git a/tests/install_upgrade_operators/strict_reconciliation/test_livemigration_config_update.py b/tests/install_upgrade_operators/strict_reconciliation/test_livemigration_config_update.py index ef5e25f7e4..1684150c0c 100644 --- a/tests/install_upgrade_operators/strict_reconciliation/test_livemigration_config_update.py +++ b/tests/install_upgrade_operators/strict_reconciliation/test_livemigration_config_update.py @@ -7,6 +7,7 @@ KUBEVIRT_CR_MIGRATIONS_KEY, LIVE_MIGRATION_CONFIG_KEY, ) +from utilities.constants.hco import HCOv1Spec pytestmark = [pytest.mark.post_upgrade, pytest.mark.sno, pytest.mark.gating, pytest.mark.arm64, pytest.mark.s390x] EXPECTED_VALUE = True @@ -20,7 +21,9 @@ class TestLiveMigrationConfigUpdate: [ pytest.param( { - PATCH_STR: {SPEC_STR: {LIVE_MIGRATION_CONFIG_KEY: {ALLOW_AUTO_CONVERGE: EXPECTED_VALUE}}}, + PATCH_STR: { + SPEC_STR: {"virtualization": {LIVE_MIGRATION_CONFIG_KEY: {ALLOW_AUTO_CONVERGE: EXPECTED_VALUE}}} + }, }, ALLOW_AUTO_CONVERGE, marks=pytest.mark.polarion("CNV-9674"), @@ -28,7 +31,9 @@ class TestLiveMigrationConfigUpdate: ), pytest.param( { - PATCH_STR: {SPEC_STR: {LIVE_MIGRATION_CONFIG_KEY: {ALLOW_POST_COPY: EXPECTED_VALUE}}}, + PATCH_STR: { + SPEC_STR: {"virtualization": {LIVE_MIGRATION_CONFIG_KEY: {ALLOW_POST_COPY: EXPECTED_VALUE}}} + }, }, ALLOW_POST_COPY, marks=pytest.mark.polarion("CNV-9675"), @@ -44,7 +49,7 @@ def test_modify_hco_cr( hco_spec, kubevirt_hyperconverged_spec_scope_function, ): - hco_value = hco_spec[LIVE_MIGRATION_CONFIG_KEY].get(expected) + hco_value = HCOv1Spec.virtualization.read(spec=hco_spec, default={})[LIVE_MIGRATION_CONFIG_KEY].get(expected) kubevirt_value = kubevirt_hyperconverged_spec_scope_function[KUBEVIRT_CR_CONFIGURATION_KEY][ KUBEVIRT_CR_MIGRATIONS_KEY ].get(expected) diff --git a/tests/network/kubemacpool/explicit_range/conftest.py b/tests/network/kubemacpool/explicit_range/conftest.py index c15bad6f34..5060fa1838 100644 --- a/tests/network/kubemacpool/explicit_range/conftest.py +++ b/tests/network/kubemacpool/explicit_range/conftest.py @@ -11,6 +11,7 @@ from libs.vm.spec import Interface, Multus, Network from libs.vm.vm import BaseVirtualMachine from tests.network.libs.mac import random_mac_range +from utilities.constants.hco import HCOv1Spec from utilities.hco import ResourceEditorValidateHCOReconcile from utilities.network import MacPool @@ -54,14 +55,12 @@ def kubemacpool_random_range_config_hco( with ResourceEditorValidateHCOReconcile( patches={ - hyperconverged_resource_scope_function: { - "spec": { - "kubeMacPoolConfiguration": { - "rangeStart": rand_range_start, - "rangeEnd": rand_range_end, - } + hyperconverged_resource_scope_function: HCOv1Spec.networking( + kubeMacPoolConfiguration={ + "rangeStart": rand_range_start, + "rangeEnd": rand_range_end, } - } + ) }, list_resource_reconcile=[NetworkAddonsConfig], wait_for_reconcile_post_update=True, @@ -76,9 +75,10 @@ def custom_range_hco_mac_pool( hyperconverged_resource_scope_function: HyperConverged, ) -> MacPool: hco_instance = hyperconverged_resource_scope_function.instance + kmp_config = hco_instance.spec.networking.kubeMacPoolConfiguration kmp_range_from_hco = { - "RANGE_START": hco_instance.spec.kubeMacPoolConfiguration.rangeStart, - "RANGE_END": hco_instance.spec.kubeMacPoolConfiguration.rangeEnd, + "RANGE_START": kmp_config.rangeStart, + "RANGE_END": kmp_config.rangeEnd, } return MacPool(kmp_range=kmp_range_from_hco) diff --git a/tests/storage/cbt/conftest.py b/tests/storage/cbt/conftest.py index 00c8f213cd..bca80b218c 100644 --- a/tests/storage/cbt/conftest.py +++ b/tests/storage/cbt/conftest.py @@ -25,6 +25,7 @@ wait_for_pull_backup_export_ready, wait_for_vm_cbt_enabled, ) +from utilities.constants.hco import HCOv1Spec from utilities.constants.images import OS_FLAVOR_RHEL from utilities.constants.instance_types import RHEL9_PREFERENCE, U1_SMALL from utilities.hco import ResourceEditorValidateHCOReconcile @@ -46,17 +47,13 @@ def cbt_hco_configured( Yields while both settings remain configured. """ + fg_patch = HCOv1Spec.feature_gates(incrementalBackup=True) + cbt_patch = HCOv1Spec.virtualization( + changedBlockTrackingLabelSelectors={"virtualMachineLabelSelector": {"matchLabels": CBT_ENABLED_LABEL}}, + ) + merged_spec = {**fg_patch["spec"], **cbt_patch["spec"]} with ResourceEditorValidateHCOReconcile( - patches={ - hyperconverged_resource_scope_module: { - "spec": { - "featureGates": {"incrementalBackup": True}, - "changedBlockTrackingLabelSelectors": { - "virtualMachineLabelSelector": {"matchLabels": CBT_ENABLED_LABEL}, - }, - }, - }, - }, + patches={hyperconverged_resource_scope_module: {"spec": merged_spec}}, list_resource_reconcile=[KubeVirt], wait_for_reconcile_post_update=True, admin_client=admin_client, diff --git a/tests/storage/cdi_config/test_cdi_config.py b/tests/storage/cdi_config/test_cdi_config.py index a1219c6a68..72581a7ab9 100644 --- a/tests/storage/cdi_config/test_cdi_config.py +++ b/tests/storage/cdi_config/test_cdi_config.py @@ -104,13 +104,13 @@ def test_cdi_spec_reconciled_by_hco(initial_cdi_config_from_cr, cdi_with_extra_n id="test_storage_workloads_in_hco_propagated_to_cdi_cr", ), pytest.param( - NON_EXISTENT_SCRATCH_SC_DICT, + {"storage": NON_EXISTENT_SCRATCH_SC_DICT}, NON_EXISTENT_SCRATCH_SC_DICT, marks=(pytest.mark.polarion("CNV-6001")), id="test_scratch_sc_in_hco_propagated_to_cdi_cr", ), pytest.param( - {"storageImport": {"insecureRegistries": INSECURE_REGISTRIES_LIST}}, + {"storage": {"storageImport": {"insecureRegistries": INSECURE_REGISTRIES_LIST}}}, {"insecureRegistries": INSECURE_REGISTRIES_LIST}, marks=(pytest.mark.polarion("CNV-6092")), id="test_insecure_registries_in_hco_propagated_to_cdi_cr", diff --git a/tests/storage/cross_cluster_live_migration/utils.py b/tests/storage/cross_cluster_live_migration/utils.py index 404c412caa..f7e5c28542 100644 --- a/tests/storage/cross_cluster_live_migration/utils.py +++ b/tests/storage/cross_cluster_live_migration/utils.py @@ -10,6 +10,7 @@ from utilities import console from utilities.constants.components import VIRT_HANDLER +from utilities.constants.hco import HCOv1Spec from utilities.constants.timeouts import TIMEOUT_3MIN, TIMEOUT_5SEC from utilities.hco import ResourceEditorValidateHCOReconcile from utilities.infra import get_daemonset_by_name @@ -43,8 +44,8 @@ def configure_hco_live_migration_network( yield return - LOGGER.info("Adding live migration network configuration to HCO spec patch") - spec_patch = {"liveMigrationConfig": {"network": network_for_live_migration.name}} + LOGGER.info("Adding live migration network configuration to HCO virtualization spec") + hco_patch = HCOv1Spec.virtualization(liveMigrationConfig={"network": network_for_live_migration.name}) virt_handler_daemonset = get_daemonset_by_name( admin_client=client, @@ -53,7 +54,7 @@ def configure_hco_live_migration_network( ) with ResourceEditorValidateHCOReconcile( - patches={hyperconverged_resource: {"spec": spec_patch}}, + patches={hyperconverged_resource: hco_patch}, list_resource_reconcile=[KubeVirt], wait_for_reconcile_post_update=True, admin_client=client, diff --git a/tests/storage/test_cdi_certificate.py b/tests/storage/test_cdi_certificate.py index b73e470694..9ff1993b2c 100644 --- a/tests/storage/test_cdi_certificate.py +++ b/tests/storage/test_cdi_certificate.py @@ -17,6 +17,7 @@ from timeout_sampler import TimeoutSampler from utilities.constants import Images +from utilities.constants.hco import HCOv1Spec from utilities.constants.pytest import QUARANTINED from utilities.constants.storage import CDI_SECRETS from utilities.constants.timeouts import TIMEOUT_1MIN, TIMEOUT_3MIN, TIMEOUT_5SEC, TIMEOUT_10MIN @@ -268,8 +269,8 @@ def test_upload_after_validate_aggregated_api_cert( @pytest.fixture() def certificate_exists(cdi_spec, hco_spec): # Verify CDI and HCO spec for cert configuration - for spec in (cdi_spec, hco_spec): - assert spec.get("certConfig"), "No certConfig found in spec." + assert cdi_spec.get("certConfig"), "No certConfig found in CDI spec." + assert HCOv1Spec.security.read(spec=hco_spec, default={}).get("certConfig"), "No certConfig found in HCO spec." @pytest.fixture() @@ -278,14 +279,12 @@ def updated_certconfig_in_hco_cr(admin_client, hyperconverged_resource_scope_fun with ResourceEditorValidateHCOReconcile( admin_client=admin_client, patches={ - hyperconverged_resource_scope_function: { - "spec": { - "certConfig": { - "ca": {"duration": "1h20m0s", "renewBefore": "1h10m0s"}, - "server": {"duration": "1h10m0s", "renewBefore": "1h5m0s"}, - } + hyperconverged_resource_scope_function: HCOv1Spec.security( + certConfig={ + "ca": {"duration": "1h20m0s", "renewBefore": "1h10m0s"}, + "server": {"duration": "1h10m0s", "renewBefore": "1h5m0s"}, } - } + ) }, list_resource_reconcile=[CDI, NetworkAddonsConfig], ): diff --git a/tests/storage/test_hotplug.py b/tests/storage/test_hotplug.py index 0234087be2..c747973b88 100644 --- a/tests/storage/test_hotplug.py +++ b/tests/storage/test_hotplug.py @@ -15,6 +15,7 @@ from tests.storage.utils import assert_disk_bus from tests.utils import create_windows2022_vm_with_data_volume_template +from utilities.constants.hco import HCOv1Spec from utilities.constants.storage import HOTPLUG_DISK_SCSI_BUS, HOTPLUG_DISK_SERIAL, HOTPLUG_DISK_VIRTIO_BUS from utilities.constants.virt import WIN_2K22 from utilities.hco import ResourceEditorValidateHCOReconcile @@ -55,7 +56,7 @@ def enabled_feature_gate_for_declarative_hotplug_volumes( with ResourceEditorValidateHCOReconcile( admin_client=admin_client, patches={ - hyperconverged_resource_scope_module: {"spec": {"featureGates": {"declarativeHotplugVolumes": True}}}, + hyperconverged_resource_scope_module: HCOv1Spec.feature_gates(declarativeHotplugVolumes=True), }, list_resource_reconcile=[KubeVirt], wait_for_reconcile_post_update=True, diff --git a/tests/storage/upgrade/conftest.py b/tests/storage/upgrade/conftest.py index 15334ab192..ff2ed43eee 100644 --- a/tests/storage/upgrade/conftest.py +++ b/tests/storage/upgrade/conftest.py @@ -10,6 +10,7 @@ create_snapshot_for_upgrade, create_vm_for_snapshot_upgrade_tests, ) +from utilities.constants.hco import HCOv1Spec from utilities.constants.storage import HOTPLUG_DISK_SERIAL, HOTPLUG_DISK_VIRTIO_BUS from utilities.hco import ResourceEditorValidateHCOReconcile from utilities.storage import create_dv, virtctl_volume @@ -101,7 +102,7 @@ def enabled_feature_gate_for_declarative_hotplug_volumes_upg( with ResourceEditorValidateHCOReconcile( admin_client=admin_client, patches={ - hyperconverged_resource_scope_session: {"spec": {"featureGates": {"declarativeHotplugVolumes": True}}}, + hyperconverged_resource_scope_session: HCOv1Spec.feature_gates(declarativeHotplugVolumes=True), }, list_resource_reconcile=[KubeVirt], wait_for_reconcile_post_update=True, diff --git a/tests/utils.py b/tests/utils.py index 061270a9c4..c636862975 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -34,6 +34,7 @@ ) from utilities.constants import Images from utilities.constants.cluster import RHSM_SECRET_NAME +from utilities.constants.hco import HCOv1Spec from utilities.constants.images import ( OS_FLAVOR_WIN_CONTAINER_DISK, OS_FLAVOR_WINDOWS, @@ -504,7 +505,7 @@ def download_and_extract_tar(tarfile_url, dest_path): def update_hco_with_persistent_storage_config(admin_client, hco_cr, storage_class): with ResourceEditorValidateHCOReconcile( admin_client=admin_client, - patches={hco_cr: {"spec": {"vmStateStorageClass": storage_class}}}, + patches={hco_cr: HCOv1Spec.storage(vmStateStorageClass=storage_class)}, list_resource_reconcile=[KubeVirt], wait_for_reconcile_post_update=True, ): diff --git a/tests/virt/cluster/migration_and_maintenance/test_evictionstrategy.py b/tests/virt/cluster/migration_and_maintenance/test_evictionstrategy.py index 69271a323e..c555d57969 100644 --- a/tests/virt/cluster/migration_and_maintenance/test_evictionstrategy.py +++ b/tests/virt/cluster/migration_and_maintenance/test_evictionstrategy.py @@ -6,6 +6,7 @@ from timeout_sampler import TimeoutSampler from tests.os_params import RHEL_LATEST, RHEL_LATEST_LABELS +from utilities.constants.hco import HCOv1Spec from utilities.constants.timeouts import ( TIMEOUT_3MIN, TIMEOUT_5MIN, @@ -68,7 +69,7 @@ def hco_cr_with_evictionstrategy_none( ): with ResourceEditorValidateHCOReconcile( admin_client=admin_client, - patches={hyperconverged_resource_scope_function: {"spec": {EVICTIONSTRATEGY: "None"}}}, + patches={hyperconverged_resource_scope_function: HCOv1Spec.virtualization(**{EVICTIONSTRATEGY: "None"})}, list_resource_reconcile=[KubeVirt], wait_for_reconcile_post_update=True, ): diff --git a/tests/virt/node/gpu/gpu_pci_passthrough/conftest.py b/tests/virt/node/gpu/gpu_pci_passthrough/conftest.py index 01fd355461..4e17f27cb5 100644 --- a/tests/virt/node/gpu/gpu_pci_passthrough/conftest.py +++ b/tests/virt/node/gpu/gpu_pci_passthrough/conftest.py @@ -15,6 +15,7 @@ NVIDIA_VFIO_MANAGER_DS, ) from tests.virt.node.gpu.utils import wait_for_ds_ready +from utilities.constants.hco import HCOv1Spec from utilities.constants.namespaces import NamespacesNames from utilities.constants.timeouts import ( TIMEOUT_1MIN, @@ -82,18 +83,16 @@ def hco_cr_with_permitted_hostdevices(admin_client, hyperconverged_resource_scop with ResourceEditorValidateHCOReconcile( admin_client=admin_client, patches={ - hyperconverged_resource_scope_class: { - "spec": { - "permittedHostDevices": { - "pciHostDevices": [ - { - "pciDeviceSelector": supported_gpu_device[DEVICE_ID_STR], - "resourceName": supported_gpu_device[GPU_DEVICE_NAME_STR], - } - ] - } + hyperconverged_resource_scope_class: HCOv1Spec.virtualization( + permittedHostDevices={ + "pciHostDevices": [ + { + "pciDeviceSelector": supported_gpu_device[DEVICE_ID_STR], + "resourceName": supported_gpu_device[GPU_DEVICE_NAME_STR], + } + ] } - } + ) }, list_resource_reconcile=[KubeVirt], wait_for_reconcile_post_update=True, diff --git a/tests/virt/node/gpu/vgpu/conftest.py b/tests/virt/node/gpu/vgpu/conftest.py index fae8747419..ce29ec0fa8 100644 --- a/tests/virt/node/gpu/vgpu/conftest.py +++ b/tests/virt/node/gpu/vgpu/conftest.py @@ -22,7 +22,7 @@ wait_for_nvidia_vgpu_manager, ) from tests.virt.utils import patch_hco_cr_with_mdev_permitted_hostdevices -from utilities.constants.hco import DISABLE_MDEV_CONFIGURATION, FEATURE_GATES +from utilities.constants.hco import DISABLE_MDEV_CONFIGURATION, HCOv1Spec from utilities.hco import ResourceEditorValidateHCOReconcile from utilities.infra import label_nodes @@ -64,24 +64,22 @@ def hco_cr_with_node_specific_mdev_permitted_hostdevices( with ResourceEditorValidateHCOReconcile( admin_client=admin_client, patches={ - hyperconverged_resource_scope_class: { - "spec": { - "permittedHostDevices": { - "mediatedDevices": [ - { - "externalResourceProvider": True, - "mdevNameSelector": supported_gpu_device[MDEV_NAME_STR], - "resourceName": supported_gpu_device[VGPU_DEVICE_NAME_STR], - }, - { - "externalResourceProvider": True, - "mdevNameSelector": supported_gpu_device[MDEV_GRID_NAME_STR], - "resourceName": supported_gpu_device[VGPU_GRID_NAME_STR], - }, - ] - }, + hyperconverged_resource_scope_class: HCOv1Spec.virtualization( + permittedHostDevices={ + "mediatedDevices": [ + { + "externalResourceProvider": True, + "mdevNameSelector": supported_gpu_device[MDEV_NAME_STR], + "resourceName": supported_gpu_device[VGPU_DEVICE_NAME_STR], + }, + { + "externalResourceProvider": True, + "mdevNameSelector": supported_gpu_device[MDEV_GRID_NAME_STR], + "resourceName": supported_gpu_device[VGPU_GRID_NAME_STR], + }, + ] } - } + ) }, list_resource_reconcile=[KubeVirt], wait_for_reconcile_post_update=True, @@ -100,7 +98,7 @@ def hco_with_disable_mdev_configuration(admin_client, hyperconverged_resource_sc """ with ResourceEditorValidateHCOReconcile( admin_client=admin_client, - patches={hyperconverged_resource_scope_session: {"spec": {FEATURE_GATES: {DISABLE_MDEV_CONFIGURATION: True}}}}, + patches={hyperconverged_resource_scope_session: HCOv1Spec.feature_gates(**{DISABLE_MDEV_CONFIGURATION: True})}, list_resource_reconcile=[KubeVirt], wait_for_reconcile_post_update=True, ): diff --git a/tests/virt/node/workload_density/test_free_page_reporting.py b/tests/virt/node/workload_density/test_free_page_reporting.py index 169fc15ad4..e660a949b5 100644 --- a/tests/virt/node/workload_density/test_free_page_reporting.py +++ b/tests/virt/node/workload_density/test_free_page_reporting.py @@ -2,6 +2,7 @@ from ocp_resources.kubevirt import KubeVirt from ocp_resources.resource import ResourceEditor +from utilities.constants.hco import HCOv1Spec from utilities.hco import ResourceEditorValidateHCOReconcile from utilities.virt import ( VirtualMachineForTests, @@ -74,11 +75,7 @@ def disabled_free_page_reporting_in_hco_cr( ): with ResourceEditorValidateHCOReconcile( admin_client=admin_client, - patches={ - hyperconverged_resource_scope_function: { - "spec": {"virtualMachineOptions": {"disableFreePageReporting": True}} - } - }, + patches={hyperconverged_resource_scope_function: HCOv1Spec.vm_options(disableFreePageReporting=True)}, list_resource_reconcile=[KubeVirt], wait_for_reconcile_post_update=True, ): @@ -103,9 +100,8 @@ class TestFreePageReporting: def test_free_page_reporting_enabled_by_default( self, admin_client, free_page_reporting_vm, hyperconverged_resource_scope_function ): - assert not hyperconverged_resource_scope_function.instance.to_dict()["spec"]["virtualMachineOptions"][ - "disableFreePageReporting" - ] + spec = hyperconverged_resource_scope_function.instance.to_dict()["spec"] + assert not HCOv1Spec.vm_options.read(spec=spec)["disableFreePageReporting"] assert_vmi_free_page_reporting( vm=free_page_reporting_vm, expected_free_page_reporting="on", diff --git a/tests/virt/node/workload_density/test_kernel_samepage_merging.py b/tests/virt/node/workload_density/test_kernel_samepage_merging.py index cccd564e66..b4489aaf68 100644 --- a/tests/virt/node/workload_density/test_kernel_samepage_merging.py +++ b/tests/virt/node/workload_density/test_kernel_samepage_merging.py @@ -9,6 +9,7 @@ from timeout_sampler import TimeoutExpiredError, TimeoutSampler from tests.utils import create_vms +from utilities.constants.hco import HCOv1Spec from utilities.constants.timeouts import ( TIMEOUT_5MIN, TIMEOUT_30SEC, @@ -103,9 +104,9 @@ def ksm_enabled_in_hco(admin_client, hyperconverged_resource_scope_class): with ResourceEditorValidateHCOReconcile( admin_client=admin_client, patches={ - hyperconverged_resource_scope_class: { - "spec": {"ksmConfiguration": {"nodeLabelSelector": {"matchLabels": KERNEL_SAMEPAGE_MERGING_TEST_LABEL}}} - } + hyperconverged_resource_scope_class: HCOv1Spec.virtualization( + ksmConfiguration={"nodeLabelSelector": {"matchLabels": KERNEL_SAMEPAGE_MERGING_TEST_LABEL}}, + ) }, list_resource_reconcile=[KubeVirt], wait_for_reconcile_post_update=True, diff --git a/tests/virt/upgrade/conftest.py b/tests/virt/upgrade/conftest.py index ad9d0195fc..6345214812 100644 --- a/tests/virt/upgrade/conftest.py +++ b/tests/virt/upgrade/conftest.py @@ -22,6 +22,7 @@ from tests.virt.utils import get_boot_time_for_multiple_vms from utilities.artifactory import get_test_artifact_server_url from utilities.constants import Images +from utilities.constants.hco import HCOv1Spec from utilities.constants.images import OS_FLAVOR_RHEL from utilities.constants.timeouts import ( TIMEOUT_30MIN, @@ -365,11 +366,9 @@ def parallel_live_migrations_increased(admin_client, hyperconverged_resource_sco with ResourceEditorValidateHCOReconcile( admin_client=admin_client, patches={ - hyperconverged_resource_scope_session: { - "spec": { - "liveMigrationConfig": {"parallelOutboundMigrationsPerNode": 5}, - } - } + hyperconverged_resource_scope_session: HCOv1Spec.virtualization( + liveMigrationConfig={"parallelOutboundMigrationsPerNode": 5} + ) }, list_resource_reconcile=[KubeVirt], wait_for_reconcile_post_update=True, diff --git a/tests/virt/upgrade_custom/vgpu/conftest.py b/tests/virt/upgrade_custom/vgpu/conftest.py index 5d39558b1d..c10e4f78a1 100644 --- a/tests/virt/upgrade_custom/vgpu/conftest.py +++ b/tests/virt/upgrade_custom/vgpu/conftest.py @@ -18,7 +18,7 @@ from tests.virt.upgrade.utils import vm_from_template from tests.virt.utils import build_node_affinity_dict, verify_gpu_device_exists_on_node from utilities.artifactory import get_test_artifact_server_url -from utilities.constants.hco import DISABLE_MDEV_CONFIGURATION, FEATURE_GATES +from utilities.constants.hco import DISABLE_MDEV_CONFIGURATION, HCOv1Spec from utilities.constants.timeouts import TIMEOUT_30MIN from utilities.constants.virt import ES_NONE from utilities.hco import ResourceEditorValidateHCOReconcile @@ -102,7 +102,7 @@ def hco_with_disable_mdev_configuration_session_scope(admin_client, hyperconverg """ with ResourceEditorValidateHCOReconcile( admin_client=admin_client, - patches={hyperconverged_resource_scope_session: {"spec": {FEATURE_GATES: {DISABLE_MDEV_CONFIGURATION: True}}}}, + patches={hyperconverged_resource_scope_session: HCOv1Spec.feature_gates(**{DISABLE_MDEV_CONFIGURATION: True})}, list_resource_reconcile=[KubeVirt], wait_for_reconcile_post_update=True, ): diff --git a/tests/virt/utils.py b/tests/virt/utils.py index 621ade3779..6a4147f2a2 100644 --- a/tests/virt/utils.py +++ b/tests/virt/utils.py @@ -25,7 +25,7 @@ VGPU_PRETTY_NAME_STR, ) from utilities.artifactory import get_test_artifact_server_url -from utilities.constants.hco import DEFAULT_HCO_CONDITIONS +from utilities.constants.hco import DEFAULT_HCO_CONDITIONS, HCOv1Spec from utilities.constants.images import OS_FLAVOR_WINDOWS from utilities.constants.os_matrix import DATA_SOURCE_STR from utilities.constants.timeouts import ( @@ -250,19 +250,17 @@ def patch_hco_cr_with_mdev_permitted_hostdevices(admin_client, hyperconverged_re with ResourceEditorValidateHCOReconcile( admin_client=admin_client, patches={ - hyperconverged_resource: { - "spec": { - "permittedHostDevices": { - "mediatedDevices": [ - { - "externalResourceProvider": True, - "mdevNameSelector": supported_gpu_device[MDEV_NAME_STR], - "resourceName": supported_gpu_device[VGPU_DEVICE_NAME_STR], - } - ] - }, + hyperconverged_resource: HCOv1Spec.virtualization( + permittedHostDevices={ + "mediatedDevices": [ + { + "externalResourceProvider": True, + "mdevNameSelector": supported_gpu_device[MDEV_NAME_STR], + "resourceName": supported_gpu_device[VGPU_DEVICE_NAME_STR], + } + ] } - } + ) }, list_resource_reconcile=[KubeVirt], wait_for_reconcile_post_update=True, diff --git a/utilities/constants/hco.py b/utilities/constants/hco.py index 99e1cb52ba..b26f75d428 100644 --- a/utilities/constants/hco.py +++ b/utilities/constants/hco.py @@ -7,6 +7,8 @@ - HCO-managed component deployment/pod name strings → ``components.py`` """ +from typing import Any + from ocp_resources.aaq import AAQ from ocp_resources.cdi import CDI from ocp_resources.data_import_cron import DataImportCron @@ -50,6 +52,86 @@ class UpgradeStreams: Z_STREAM = "z-stream" +class _SpecGroup: + def __init__(self, path: str | tuple[str, ...]) -> None: + self._path = (path,) if isinstance(path, str) else path + + def __call__(self, **fields: Any) -> dict: + result: dict = fields + for key in reversed(self._path): + result = {key: result} + return {"spec": result} + + def read(self, spec: dict, default: Any = None) -> Any: + """Navigate a v1 HCO spec dict to read this group's content.""" + current = spec + for key in self._path: + if not isinstance(current, dict): + return default + current = current.get(key, default) + return current + + +class HCOv1Spec: + """HCO v1 API spec patch builders and feature gate helpers.""" + + virtualization = _SpecGroup(path="virtualization") + security = _SpecGroup(path="security") + storage = _SpecGroup(path="storage") + deployment = _SpecGroup(path="deployment") + workload_sources = _SpecGroup(path="workloadSources") + networking = _SpecGroup(path="networking") + node_placements = _SpecGroup(path=("deployment", "nodePlacements")) + vm_options = _SpecGroup(path=("virtualization", "virtualMachineOptions")) + aaq_config = _SpecGroup(path=("deployment", "applicationAwareConfig")) + + @staticmethod + def feature_gates(**gates: bool) -> dict: + """Build a v1 featureGates spec patch. + + Args: + **gates: Feature gate names mapped to enabled (True) or disabled (False). + + Returns: + Patch dict with v1 list format at spec.featureGates. + """ + fg_list = [] + for name, enabled in gates.items(): + entry: dict[str, str] = {"name": name} + if not enabled: + entry["state"] = "Disabled" + fg_list.append(entry) + return {"spec": {"featureGates": fg_list}} + + @staticmethod + def is_fg_enabled( + feature_gates: list[dict[str, str]], + name: str, + fg_phases: dict[str, str], + ) -> bool: + """Check if a feature gate is effectively enabled. + + In v1, FGs at their phase default are absent from the spec.featureGates list. + Uses fg_phases to determine the default when absent: + beta = enabled by default, alpha/deprecated = disabled by default. + + Args: + feature_gates: The spec.featureGates list from the HCO CR. + name: Feature gate name. + fg_phases: Phase mapping from parse_hco_fg_phases(). + + Returns: + True if the feature gate is effectively enabled, False otherwise. + + Raises: + KeyError: If name is not in fg_phases (unknown feature gate). + """ + for fg in feature_gates: + if fg["name"] == name: + return fg.get("state", "Enabled") == "Enabled" + return fg_phases[name] == "beta" + + TLS_OLD_POLICY = "old" TLS_CUSTOM_POLICY = "custom" TLS_SECURITY_PROFILE = "tlsSecurityProfile" diff --git a/utilities/hco.py b/utilities/hco.py index bad8636028..6227b5a99c 100644 --- a/utilities/hco.py +++ b/utilities/hco.py @@ -1,11 +1,13 @@ import json import logging +import re from collections.abc import Collection, Iterator from contextlib import contextmanager from typing import TYPE_CHECKING from kubernetes.dynamic.exceptions import NotFoundError, ResourceNotFoundError from ocp_resources.cdi import CDI +from ocp_resources.custom_resource_definition import CustomResourceDefinition from ocp_resources.data_source import DataSource from ocp_resources.hyperconverged import HyperConverged from ocp_resources.kubevirt import KubeVirt @@ -24,6 +26,7 @@ HCO_SUBSCRIPTION, IMAGE_CRON_STR, SSP_CR_COMMON_TEMPLATES_LIST_KEY_NAME, + HCOv1Spec, ) from utilities.constants.storage import StorageClassNames from utilities.constants.timeouts import ( @@ -68,6 +71,43 @@ } +_FG_PHASE_RE = re.compile(r"\*\s+(\w+):\s.*?Phase:\s+(\w+)", re.DOTALL) + + +def parse_hco_fg_phases(admin_client: DynamicClient) -> dict[str, str]: + """Parse feature gate phases from the HCO v1 CRD schema on the cluster. + + Returns: + Mapping of feature gate name to phase ("alpha", "beta", "deprecated"). + + Raises: + ValueError: If v1 schema is not found or the description format changed. + """ + crd = CustomResourceDefinition( + client=admin_client, + name="hyperconvergeds.hco.kubevirt.io", + ) + crd_dict = crd.instance.to_dict() + v1_version = next( + (v for v in crd_dict["spec"]["versions"] if v["name"] == "v1"), + None, + ) + if not v1_version: + raise ValueError("HCO CRD does not have a v1 version") + + fg_description = v1_version["schema"]["openAPIV3Schema"]["properties"]["spec"]["properties"]["featureGates"].get( + "description", "" + ) + phases = dict(_FG_PHASE_RE.findall(fg_description)) + if not phases: + raise ValueError( + f"Failed to parse FG phases from HCO CRD — format may have changed. " + f"Description starts with: {fg_description[:200]!r}" + ) + LOGGER.info(f"Parsed {len(phases)} feature gate phases from HCO CRD: {phases}") + return phases + + class ResourceEditorValidateHCOReconcile(ResourceEditor): def __init__( self, @@ -197,17 +237,17 @@ def apply_np_changes( workloads_placement=None, exclude_deployments=None, ): - current_infra = hco.instance.to_dict()["spec"].get("infra") - current_workloads = hco.instance.to_dict()["spec"].get("workloads") + spec = hco.instance.to_dict()["spec"] + node_placements = HCOv1Spec.node_placements.read(spec=spec, default={}) + current_infra = node_placements.get("infra") + current_workloads = node_placements.get("workload") target_infra = infra_placement if infra_placement is not None else current_infra target_workloads = workloads_placement if workloads_placement is not None else current_workloads if target_workloads != current_workloads or target_infra != current_infra: - patch = { - "spec": { - "infra": target_infra or None, - "workloads": target_workloads or None, - }, - } + patch = HCOv1Spec.node_placements( + infra=target_infra or None, + workload=target_workloads or None, + ) LOGGER.info(f"Updating HCO with node placement. {patch}") editor = ResourceEditor(patches={hco: patch}) editor.update(backup_resources=False) @@ -368,7 +408,8 @@ def disable_common_boot_image_import_hco_spec( golden_images_data_import_crons: list[DataImportCron], exclude_data_source_names: Collection[str] | None = None, ) -> Iterator[None]: - if hco_resource.instance.spec[ENABLE_COMMON_BOOT_IMAGE_IMPORT]: + spec = hco_resource.instance.to_dict()["spec"] + if HCOv1Spec.workload_sources.read(spec=spec, default={}).get(ENABLE_COMMON_BOOT_IMAGE_IMPORT, True): update_common_boot_image_import_spec( hco_resource=hco_resource, enable=False, @@ -415,7 +456,12 @@ def _wait_for_spec_update(_hco_resource, _enable): for sample in TimeoutSampler( wait_timeout=TIMEOUT_2MIN, sleep=5, - func=lambda: _hco_resource.instance.spec[ENABLE_COMMON_BOOT_IMAGE_IMPORT] == _enable, + func=lambda: ( + HCOv1Spec.workload_sources.read(spec=_hco_resource.instance.to_dict()["spec"], default={}).get( + ENABLE_COMMON_BOOT_IMAGE_IMPORT + ) + == _enable + ), ): if sample: return @@ -423,8 +469,9 @@ def _wait_for_spec_update(_hco_resource, _enable): LOGGER.error(f"{ENABLE_COMMON_BOOT_IMAGE_IMPORT} was not updated to {_enable}") raise + patch = HCOv1Spec.workload_sources(**{ENABLE_COMMON_BOOT_IMAGE_IMPORT: enable}) editor = ResourceEditor( - patches={hco_resource: {"spec": {ENABLE_COMMON_BOOT_IMAGE_IMPORT: enable}}}, + patches={hco_resource: patch}, ) editor.update(backup_resources=True) _wait_for_spec_update(_hco_resource=hco_resource, _enable=enable) @@ -552,7 +599,11 @@ def update_hco_templates_spec( ): with ResourceEditorValidateHCOReconcile( admin_client=admin_client, - patches={hyperconverged_resource: {"spec": {SSP_CR_COMMON_TEMPLATES_LIST_KEY_NAME: [updated_template]}}}, + patches={ + hyperconverged_resource: HCOv1Spec.workload_sources(**{ + SSP_CR_COMMON_TEMPLATES_LIST_KEY_NAME: [updated_template] + }) + }, list_resource_reconcile=[SSP, CDI], wait_for_reconcile_post_update=True, ): @@ -570,11 +621,10 @@ def update_hco_templates_spec( @contextmanager def enabled_aaq_in_hco(client, hco_namespace, hyperconverged_resource, enable_acrq_support=False): - patches = {hyperconverged_resource: {"spec": {"enableApplicationAwareQuota": True}}} + aaq_config = {"enable": True} if enable_acrq_support: - patches[hyperconverged_resource]["spec"]["applicationAwareConfig"] = { - "allowApplicationAwareClusterResourceQuota": True, - } + aaq_config["allowApplicationAwareClusterResourceQuota"] = True + patches = {hyperconverged_resource: HCOv1Spec.aaq_config(**aaq_config)} with ResourceEditorValidateHCOReconcile( patches=patches, diff --git a/utilities/infra.py b/utilities/infra.py index ac8fdfdae3..9e0622b6ee 100644 --- a/utilities/infra.py +++ b/utilities/infra.py @@ -622,7 +622,6 @@ def get_hyperconverged_resource(client, hco_ns_name): namespace=hco_ns_name, name=hco_name, ) - hco.api_version = f"{hco.ApiGroup.HCO_KUBEVIRT_IO}/{hco.ApiVersion.V1BETA1}" if hco.exists: return hco raise ResourceNotFoundError(f"Hyperconverged: {hco_name} not found in {hco_ns_name}") diff --git a/utilities/unittests/test_hco.py b/utilities/unittests/test_hco.py index d4030b12fc..83ed95cd19 100644 --- a/utilities/unittests/test_hco.py +++ b/utilities/unittests/test_hco.py @@ -39,6 +39,7 @@ del sys.modules["utilities.hco"] # Import after setting up mocks to avoid circular dependency +from utilities.constants.hco import HCOv1Spec from utilities.hco import ( CDI, DEFAULT_HCO_PROGRESSING_CONDITIONS, @@ -58,6 +59,7 @@ get_json_patch_annotation_values, hco_cr_jsonpatch_annotations_dict, is_hco_tainted, + parse_hco_fg_phases, update_common_boot_image_import_spec, update_hco_annotations, update_hco_templates_spec, @@ -238,14 +240,18 @@ def test_get_hco_spec_success(self, mock_namespace_class, mock_get_hco): mock_hco = MagicMock() mock_hco.instance.to_dict.return_value = { - "spec": {"infra": {}, "workloads": {}, "featureGates": {"enableCommonBootImageImport": True}} + "spec": { + "deployment": {"nodePlacements": {"infra": {}, "workload": {}}}, + "workloadSources": {"enableCommonBootImageImport": True}, + "featureGates": [], + } } mock_get_hco.return_value = mock_hco result = get_hco_spec(mock_admin_client, mock_namespace) - assert "infra" in result - assert "workloads" in result + assert "deployment" in result + assert "workloadSources" in result assert "featureGates" in result mock_get_hco.assert_called_once_with(client=mock_admin_client, hco_ns_name="openshift-cnv") @@ -538,7 +544,9 @@ def test_apply_np_changes_infra_only(self, mock_namespace_class, mock_resource_e mock_hco = MagicMock() mock_namespace = MagicMock() - mock_hco.instance.to_dict.return_value = {"spec": {"infra": None, "workloads": None}} + mock_hco.instance.to_dict.return_value = { + "spec": {"deployment": {"nodePlacements": {"infra": None, "workload": None}}} + } new_infra_placement = {"nodeSelector": {"node-role.kubernetes.io/worker": ""}} @@ -557,7 +565,9 @@ def test_apply_np_changes_no_changes(self, mock_namespace_class, mock_resource_e mock_namespace = MagicMock() existing_placement = {"nodeSelector": {"node-role.kubernetes.io/worker": ""}} - mock_hco.instance.to_dict.return_value = {"spec": {"infra": existing_placement, "workloads": None}} + mock_hco.instance.to_dict.return_value = { + "spec": {"deployment": {"nodePlacements": {"infra": existing_placement, "workload": None}}} + } apply_np_changes(mock_admin_client, mock_hco, mock_namespace, infra_placement=existing_placement) @@ -754,7 +764,7 @@ def test_disable_when_enabled(self, mock_update_spec, mock_wait_deleted, mock_en """Test disabling common boot image import when it's enabled""" mock_admin_client = MagicMock() mock_hco = MagicMock() - mock_hco.instance.spec = {"enableCommonBootImageImport": True} + mock_hco.instance.to_dict.return_value = {"spec": {"workloadSources": {"enableCommonBootImageImport": True}}} mock_namespace = MagicMock() mock_dics = [MagicMock()] @@ -784,7 +794,7 @@ def test_disable_propagates_exclude_data_source_names(self, mock_update_spec, mo """Test that exclude_data_source_names is forwarded to the teardown call""" mock_admin_client = MagicMock() mock_hco = MagicMock() - mock_hco.instance.spec = {"enableCommonBootImageImport": True} + mock_hco.instance.to_dict.return_value = {"spec": {"workloadSources": {"enableCommonBootImageImport": True}}} mock_namespace = MagicMock() mock_dics = [MagicMock()] exclude_names = {"custom-datasource"} @@ -813,7 +823,7 @@ def test_disable_when_already_disabled(self, mock_update_spec, mock_wait_deleted """Test context manager when common boot image import is already disabled""" mock_admin_client = MagicMock() mock_hco = MagicMock() - mock_hco.instance.spec = {"enableCommonBootImageImport": False} + mock_hco.instance.to_dict.return_value = {"spec": {"workloadSources": {"enableCommonBootImageImport": False}}} mock_namespace = MagicMock() mock_dics = [MagicMock()] @@ -911,7 +921,7 @@ class TestUpdateCommonBootImageImportSpec: def test_update_spec_enable(self, mock_editor_class, mock_sampler): """Test enabling common boot image import spec""" mock_hco = MagicMock() - mock_hco.instance.spec = {"enableCommonBootImageImport": True} + mock_hco.instance.to_dict.return_value = {"spec": {"workloadSources": {"enableCommonBootImageImport": True}}} mock_editor = MagicMock() mock_editor_class.return_value = mock_editor @@ -930,7 +940,7 @@ def test_update_spec_enable(self, mock_editor_class, mock_sampler): def test_update_spec_timeout(self, mock_editor_class, mock_sampler): """Test timeout when spec doesn't update""" mock_hco = MagicMock() - mock_hco.instance.spec = {"enableCommonBootImageImport": False} + mock_hco.instance.to_dict.return_value = {"spec": {"workloadSources": {"enableCommonBootImageImport": False}}} mock_editor = MagicMock() mock_editor_class.return_value = mock_editor @@ -1252,7 +1262,7 @@ def test_enable_aaq_basic(self, mock_editor_class, mock_get_pod, mock_sampler): call_args = mock_editor_class.call_args patches = call_args[1]["patches"] assert mock_hco in patches - assert patches[mock_hco]["spec"]["enableApplicationAwareQuota"] is True + assert patches[mock_hco]["spec"]["deployment"]["applicationAwareConfig"] == {"enable": True} @patch("utilities.hco.TimeoutSampler") @patch("utilities.hco.utilities.infra.get_pod_by_name_prefix") @@ -1279,8 +1289,9 @@ def test_enable_aaq_with_acrq_support(self, mock_editor_class, mock_get_pod, moc # Verify ACRQ support is included call_args = mock_editor_class.call_args patches = call_args[1]["patches"] - assert patches[mock_hco]["spec"]["applicationAwareConfig"] == { - "allowApplicationAwareClusterResourceQuota": True + assert patches[mock_hco]["spec"]["deployment"]["applicationAwareConfig"] == { + "enable": True, + "allowApplicationAwareClusterResourceQuota": True, } @patch("utilities.hco.TimeoutSampler") @@ -1338,3 +1349,209 @@ def test_enable_aaq_handles_resource_not_found(self, mock_editor_class, mock_get pass mock_logger.info.assert_called_with("AAQ system PODs removed.") + + +class TestHCOv1Spec: + """Test cases for HCOv1Spec class""" + + def test_feature_gates_single_enable(self): + """Test feature_gates with a single enabled gate""" + result = HCOv1Spec.feature_gates(downwardMetrics=True) + + assert result == {"spec": {"featureGates": [{"name": "downwardMetrics"}]}} + + def test_feature_gates_single_disable(self): + """Test feature_gates with a single disabled gate""" + result = HCOv1Spec.feature_gates(hotplugNICs=False) + + assert result == {"spec": {"featureGates": [{"name": "hotplugNICs", "state": "Disabled"}]}} + + def test_feature_gates_multiple_gates(self): + """Test feature_gates with multiple gates of mixed states""" + result = HCOv1Spec.feature_gates(downwardMetrics=True, hotplugNICs=False, deployKubeSecondaryDNS=True) + + fg_list = result["spec"]["featureGates"] + assert len(fg_list) == 3 + fg_by_name = {fg["name"]: fg for fg in fg_list} + assert fg_by_name["downwardMetrics"] == {"name": "downwardMetrics"} + assert fg_by_name["hotplugNICs"] == {"name": "hotplugNICs", "state": "Disabled"} + assert fg_by_name["deployKubeSecondaryDNS"] == {"name": "deployKubeSecondaryDNS"} + + def test_is_fg_enabled_explicitly_enabled(self): + """Test is_fg_enabled returns True for a gate explicitly present without Disabled state""" + fg_list = [{"name": "downwardMetrics"}] + fg_phases = {"downwardMetrics": "beta"} + + assert HCOv1Spec.is_fg_enabled(feature_gates=fg_list, name="downwardMetrics", fg_phases=fg_phases) is True + + def test_is_fg_enabled_explicitly_disabled(self): + """Test is_fg_enabled returns False for a gate explicitly disabled""" + fg_list = [{"name": "hotplugNICs", "state": "Disabled"}] + fg_phases = {"hotplugNICs": "alpha"} + + assert HCOv1Spec.is_fg_enabled(feature_gates=fg_list, name="hotplugNICs", fg_phases=fg_phases) is False + + def test_is_fg_enabled_absent_beta_defaults_true(self): + """Test is_fg_enabled returns True for absent beta gate (beta default is enabled)""" + fg_list = [] + fg_phases = {"downwardMetrics": "beta"} + + assert HCOv1Spec.is_fg_enabled(feature_gates=fg_list, name="downwardMetrics", fg_phases=fg_phases) is True + + def test_is_fg_enabled_absent_alpha_defaults_false(self): + """Test is_fg_enabled returns False for absent alpha gate (alpha default is disabled)""" + fg_list = [] + fg_phases = {"hotplugNICs": "alpha"} + + assert HCOv1Spec.is_fg_enabled(feature_gates=fg_list, name="hotplugNICs", fg_phases=fg_phases) is False + + def test_is_fg_enabled_unknown_gate_raises_key_error(self): + """Test is_fg_enabled raises KeyError for unknown gate not in fg_phases""" + fg_list = [] + fg_phases = {"downwardMetrics": "beta"} + + with pytest.raises(KeyError, match="unknownGate"): + HCOv1Spec.is_fg_enabled(feature_gates=fg_list, name="unknownGate", fg_phases=fg_phases) + + def test_virtualization_spec_group(self): + """Test virtualization spec group builder produces correct nested dict""" + live_migration_config = {"completionTimeoutPerGiB": 800} + result = HCOv1Spec.virtualization(liveMigrationConfig=live_migration_config) + + assert result == {"spec": {"virtualization": {"liveMigrationConfig": {"completionTimeoutPerGiB": 800}}}} + + def test_node_placements_spec_group(self): + """Test node_placements spec group builder produces correct nested dict""" + infra_placement = {"nodeSelector": {"node-role.kubernetes.io/infra": ""}} + result = HCOv1Spec.node_placements(infra=infra_placement, workload=None) + + assert result == { + "spec": { + "deployment": { + "nodePlacements": { + "infra": {"nodeSelector": {"node-role.kubernetes.io/infra": ""}}, + "workload": None, + } + } + } + } + + def test_workload_sources_spec_group(self): + """Test workload_sources spec group builder produces correct nested dict""" + result = HCOv1Spec.workload_sources(enableCommonBootImageImport=True) + + assert result == {"spec": {"workloadSources": {"enableCommonBootImageImport": True}}} + + def test_aaq_config_spec_group(self): + """Test aaq_config spec group builder produces correct nested dict""" + result = HCOv1Spec.aaq_config(enable=True, allowApplicationAwareClusterResourceQuota=True) + + assert result == { + "spec": { + "deployment": { + "applicationAwareConfig": { + "enable": True, + "allowApplicationAwareClusterResourceQuota": True, + } + } + } + } + + +class TestParseHcoFgPhases: + """Test cases for parse_hco_fg_phases function""" + + @patch("utilities.hco.CustomResourceDefinition") + def test_parses_phases_correctly(self, mock_crd_class): + """Test parse_hco_fg_phases extracts feature gate phases from CRD description""" + mock_admin_client = MagicMock() + fg_description = ( + "* downwardMetrics: Enables exposing downward metrics to guests. Phase: beta\n" + "* hotplugNICs: Enables hotplugging network interfaces. Phase: alpha\n" + "* deployKubeSecondaryDNS: Deploy KubeSecondaryDNS. Phase: deprecated" + ) + mock_crd = MagicMock() + mock_crd.instance.to_dict.return_value = { + "spec": { + "versions": [ + { + "name": "v1", + "schema": { + "openAPIV3Schema": { + "properties": { + "spec": { + "properties": { + "featureGates": { + "description": fg_description, + } + } + } + } + } + }, + } + ] + } + } + mock_crd_class.return_value = mock_crd + + result = parse_hco_fg_phases(admin_client=mock_admin_client) + + assert result == { + "downwardMetrics": "beta", + "hotplugNICs": "alpha", + "deployKubeSecondaryDNS": "deprecated", + } + mock_crd_class.assert_called_once_with( + client=mock_admin_client, + name="hyperconvergeds.hco.kubevirt.io", + ) + + @patch("utilities.hco.CustomResourceDefinition") + def test_raises_value_error_when_no_phases_found(self, mock_crd_class): + """Test parse_hco_fg_phases raises ValueError when description has no phase matches""" + mock_admin_client = MagicMock() + mock_crd = MagicMock() + mock_crd.instance.to_dict.return_value = { + "spec": { + "versions": [ + { + "name": "v1", + "schema": { + "openAPIV3Schema": { + "properties": { + "spec": { + "properties": { + "featureGates": { + "description": "No feature gate phases here", + } + } + } + } + } + }, + } + ] + } + } + mock_crd_class.return_value = mock_crd + + with pytest.raises(ValueError, match="Failed to parse FG phases"): + parse_hco_fg_phases(admin_client=mock_admin_client) + + @patch("utilities.hco.CustomResourceDefinition") + def test_raises_value_error_when_v1_schema_missing(self, mock_crd_class): + """Test parse_hco_fg_phases raises ValueError when CRD has no v1 version""" + mock_admin_client = MagicMock() + mock_crd = MagicMock() + mock_crd.instance.to_dict.return_value = { + "spec": { + "versions": [ + {"name": "v1beta1", "schema": {}}, + ] + } + } + mock_crd_class.return_value = mock_crd + + with pytest.raises(ValueError, match="HCO CRD does not have a v1 version"): + parse_hco_fg_phases(admin_client=mock_admin_client) diff --git a/uv.lock b/uv.lock index 779efce222..469bb7b237 100644 --- a/uv.lock +++ b/uv.lock @@ -1298,7 +1298,7 @@ sdist = { url = "https://files.pythonhosted.org/packages/11/8c/5a03b7c28670dd355 [[package]] name = "openshift-python-wrapper" -version = "11.0.138" +version = "11.0.139" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cloup" }, @@ -1318,7 +1318,7 @@ dependencies = [ { name = "timeout-sampler" }, { name = "xmltodict" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/7b/e5/6a683c68262f56a13cda611691cd5cda3caadd54decf54d1efec8c1455ab/openshift_python_wrapper-11.0.138.tar.gz", hash = "sha256:a1f073e0217ccab93e671ee569e5b5917eae9d555c194a937994e566b83e932d", size = 8160531, upload-time = "2026-07-22T11:00:19.859Z" } +sdist = { url = "https://files.pythonhosted.org/packages/9f/40/55c080ae49440e4e5be1f3f0c7bed5d8df6790c80081a73ff1adf8f8426c/openshift_python_wrapper-11.0.139.tar.gz", hash = "sha256:63baddad7a8880eee3b1b88ada798b242265d96d886277a9b8c32336c8f17102", size = 8167523, upload-time = "2026-08-05T18:34:38.902Z" } [[package]] name = "openshift-python-wrapper-data-collector" @@ -1420,7 +1420,7 @@ requires-dist = [ { name = "kubernetes", specifier = ">=34.1.0" }, { name = "netaddr", specifier = ">=1.3.0" }, { name = "openshift-python-utilities", specifier = ">=6.0.0" }, - { name = "openshift-python-wrapper", specifier = ">=11.0.132" }, + { name = "openshift-python-wrapper", specifier = ">=11.0.139" }, { name = "openstacksdk", specifier = ">=4.1.0" }, { name = "pexpect", specifier = ">=4.9.0" }, { name = "podman", specifier = ">=5.2.0" },