From 44bb448af60082b95f6d8a9d91d74c9f330e24c9 Mon Sep 17 00:00:00 2001 From: Priyansh Choudhary Date: Wed, 4 Mar 2026 20:08:41 +0000 Subject: [PATCH] Add controller and initContainer resource settings, fix full-path key pass-through in DataProtectionKubernetes Co-authored-by: priyansh17|choudharypr@microsoft.com --- .../DataProtectionKubernetes.py | 35 +++ .../latest/test_data_protection_kubernetes.py | 202 ++++++++++++++++++ 2 files changed, 237 insertions(+) create mode 100644 src/k8s-extension/azext_k8s_extension/tests/latest/test_data_protection_kubernetes.py diff --git a/src/k8s-extension/azext_k8s_extension/partner_extensions/DataProtectionKubernetes.py b/src/k8s-extension/azext_k8s_extension/partner_extensions/DataProtectionKubernetes.py index 8ce10e539aa..b0d7c06d403 100644 --- a/src/k8s-extension/azext_k8s_extension/partner_extensions/DataProtectionKubernetes.py +++ b/src/k8s-extension/azext_k8s_extension/partner_extensions/DataProtectionKubernetes.py @@ -22,6 +22,10 @@ def __init__(self): - Backup storage location (required) - Resource Requests (optional) - Resource Limits (optional) + - Controller Resource Requests (optional) + - Controller Resource Limits (optional) + - Init Container (veleroPluginForMicrosoftAzure) Resource Requests (optional) + - Init Container (veleroPluginForMicrosoftAzure) Resource Limits (optional) - Disable Informer Cache (optional) """ self.TENANT_ID = "credentials.tenantId" @@ -33,6 +37,14 @@ def __init__(self): self.RESOURCE_REQUEST_MEMORY = "resources.requests.memory" self.RESOURCE_LIMIT_CPU = "resources.limits.cpu" self.RESOURCE_LIMIT_MEMORY = "resources.limits.memory" + self.CONTROLLER_RESOURCE_REQUEST_CPU = "controller.resources.requests.cpu" + self.CONTROLLER_RESOURCE_REQUEST_MEMORY = "controller.resources.requests.memory" + self.CONTROLLER_RESOURCE_LIMIT_CPU = "controller.resources.limits.cpu" + self.CONTROLLER_RESOURCE_LIMIT_MEMORY = "controller.resources.limits.memory" + self.PLUGIN_RESOURCE_REQUEST_CPU = "initContainers.veleroPluginForMicrosoftAzure.resources.requests.cpu" + self.PLUGIN_RESOURCE_REQUEST_MEMORY = "initContainers.veleroPluginForMicrosoftAzure.resources.requests.memory" + self.PLUGIN_RESOURCE_LIMIT_CPU = "initContainers.veleroPluginForMicrosoftAzure.resources.limits.cpu" + self.PLUGIN_RESOURCE_LIMIT_MEMORY = "initContainers.veleroPluginForMicrosoftAzure.resources.limits.memory" self.BACKUP_STORAGE_ACCOUNT_USE_AAD = "configuration.backupStorageLocation.config.useAAD" self.BACKUP_STORAGE_ACCOUNT_STORAGE_ACCOUNT_URI = "configuration.backupStorageLocation.config.storageAccountURI" self.DISABLE_INFORMER_CACHE = "configuration.disableInformerCache" @@ -45,6 +57,14 @@ def __init__(self): self.memory_request = "memoryRequest" self.cpu_limit = "cpuLimit" self.memory_limit = "memoryLimit" + self.controller_cpu_request = "controllerCpuRequest" + self.controller_memory_request = "controllerMemoryRequest" + self.controller_cpu_limit = "controllerCpuLimit" + self.controller_memory_limit = "controllerMemoryLimit" + self.plugin_cpu_request = "pluginCpuRequest" + self.plugin_memory_request = "pluginMemoryRequest" + self.plugin_cpu_limit = "pluginCpuLimit" + self.plugin_memory_limit = "pluginMemoryLimit" self.use_aad = "useAAD" self.storage_account_uri = "storageAccountURI" self.disable_informer_cache = "disableInformerCache" @@ -58,6 +78,14 @@ def __init__(self): self.memory_request.lower(): self.RESOURCE_REQUEST_MEMORY, self.cpu_limit.lower(): self.RESOURCE_LIMIT_CPU, self.memory_limit.lower(): self.RESOURCE_LIMIT_MEMORY, + self.controller_cpu_request.lower(): self.CONTROLLER_RESOURCE_REQUEST_CPU, + self.controller_memory_request.lower(): self.CONTROLLER_RESOURCE_REQUEST_MEMORY, + self.controller_cpu_limit.lower(): self.CONTROLLER_RESOURCE_LIMIT_CPU, + self.controller_memory_limit.lower(): self.CONTROLLER_RESOURCE_LIMIT_MEMORY, + self.plugin_cpu_request.lower(): self.PLUGIN_RESOURCE_REQUEST_CPU, + self.plugin_memory_request.lower(): self.PLUGIN_RESOURCE_REQUEST_MEMORY, + self.plugin_cpu_limit.lower(): self.PLUGIN_RESOURCE_LIMIT_CPU, + self.plugin_memory_limit.lower(): self.PLUGIN_RESOURCE_LIMIT_MEMORY, self.use_aad.lower(): self.BACKUP_STORAGE_ACCOUNT_USE_AAD, self.storage_account_uri.lower(): self.BACKUP_STORAGE_ACCOUNT_STORAGE_ACCOUNT_URI, self.disable_informer_cache.lower(): self.DISABLE_INFORMER_CACHE @@ -199,10 +227,17 @@ def __validate_and_map_config(self, configuration_settings, validate_bsl=True): if key.lower() not in input_configuration_keys: raise RequiredArgumentMissingError(f"Missing required configuration setting: {key}") + recognized_full_paths = set(self.configuration_mapping.values()) + recognized_full_paths_lower = {p.lower(): p for p in recognized_full_paths} + for key in input_configuration_settings: _key = key.lower() if _key in self.configuration_mapping: configuration_settings[self.configuration_mapping[_key]] = configuration_settings.pop(key).strip() + elif _key in recognized_full_paths_lower: + # User provided the full configuration path directly - normalize to canonical form + canonical = recognized_full_paths_lower[_key] + configuration_settings[canonical] = configuration_settings.pop(key).strip() else: configuration_settings.pop(key) logger.warning(f"Ignoring unrecognized configuration setting: {key}") diff --git a/src/k8s-extension/azext_k8s_extension/tests/latest/test_data_protection_kubernetes.py b/src/k8s-extension/azext_k8s_extension/tests/latest/test_data_protection_kubernetes.py new file mode 100644 index 00000000000..0e4d7031d7d --- /dev/null +++ b/src/k8s-extension/azext_k8s_extension/tests/latest/test_data_protection_kubernetes.py @@ -0,0 +1,202 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + +import unittest +from unittest.mock import patch + +from azure.cli.core.azclierror import RequiredArgumentMissingError + +from azext_k8s_extension.partner_extensions.DataProtectionKubernetes import DataProtectionKubernetes + + +class TestDataProtectionKubernetesConfigMapping(unittest.TestCase): + def setUp(self): + self.ext = DataProtectionKubernetes() + # Access the private method via name mangling + self._validate = self.ext._DataProtectionKubernetes__validate_and_map_config + + def _make_bsl_settings(self): + return { + "blobContainer": "mycontainer", + "storageAccount": "myaccount", + "storageAccountResourceGroup": "myrg", + "storageAccountSubscriptionId": "mysub", + } + + # ------------------------------------------------------------------ + # Short-name mapping tests + # ------------------------------------------------------------------ + + def test_short_names_mapped_to_full_paths(self): + config = self._make_bsl_settings() + self._validate(config) + self.assertIn("configuration.backupStorageLocation.bucket", config) + self.assertIn("configuration.backupStorageLocation.config.storageAccount", config) + self.assertIn("configuration.backupStorageLocation.config.resourceGroup", config) + self.assertIn("configuration.backupStorageLocation.config.subscriptionId", config) + + def test_cpu_and_memory_limit_short_names(self): + config = {**self._make_bsl_settings(), "cpuLimit": "500m", "memoryLimit": "256Mi"} + self._validate(config) + self.assertEqual(config.get("resources.limits.cpu"), "500m") + self.assertEqual(config.get("resources.limits.memory"), "256Mi") + + def test_cpu_and_memory_request_short_names(self): + config = {**self._make_bsl_settings(), "cpuRequest": "100m", "memoryRequest": "128Mi"} + self._validate(config) + self.assertEqual(config.get("resources.requests.cpu"), "100m") + self.assertEqual(config.get("resources.requests.memory"), "128Mi") + + # ------------------------------------------------------------------ + # Controller resource settings + # ------------------------------------------------------------------ + + def test_controller_cpu_limit_short_name(self): + config = {**self._make_bsl_settings(), "controllerCpuLimit": "1"} + self._validate(config) + self.assertEqual(config.get("controller.resources.limits.cpu"), "1") + + def test_controller_memory_limit_short_name(self): + config = {**self._make_bsl_settings(), "controllerMemoryLimit": "512Mi"} + self._validate(config) + self.assertEqual(config.get("controller.resources.limits.memory"), "512Mi") + + def test_controller_cpu_request_short_name(self): + config = {**self._make_bsl_settings(), "controllerCpuRequest": "200m"} + self._validate(config) + self.assertEqual(config.get("controller.resources.requests.cpu"), "200m") + + def test_controller_memory_request_short_name(self): + config = {**self._make_bsl_settings(), "controllerMemoryRequest": "256Mi"} + self._validate(config) + self.assertEqual(config.get("controller.resources.requests.memory"), "256Mi") + + # ------------------------------------------------------------------ + # initContainers.veleroPluginForMicrosoftAzure resource settings + # ------------------------------------------------------------------ + + def test_plugin_cpu_limit_short_name(self): + config = {**self._make_bsl_settings(), "pluginCpuLimit": "1"} + self._validate(config) + self.assertEqual( + config.get("initContainers.veleroPluginForMicrosoftAzure.resources.limits.cpu"), "1" + ) + + def test_plugin_memory_limit_short_name(self): + config = {**self._make_bsl_settings(), "pluginMemoryLimit": "512Mi"} + self._validate(config) + self.assertEqual( + config.get("initContainers.veleroPluginForMicrosoftAzure.resources.limits.memory"), "512Mi" + ) + + def test_plugin_cpu_request_short_name(self): + config = {**self._make_bsl_settings(), "pluginCpuRequest": "100m"} + self._validate(config) + self.assertEqual( + config.get("initContainers.veleroPluginForMicrosoftAzure.resources.requests.cpu"), "100m" + ) + + def test_plugin_memory_request_short_name(self): + config = {**self._make_bsl_settings(), "pluginMemoryRequest": "128Mi"} + self._validate(config) + self.assertEqual( + config.get("initContainers.veleroPluginForMicrosoftAzure.resources.requests.memory"), "128Mi" + ) + + # ------------------------------------------------------------------ + # Full-path pass-through tests + # ------------------------------------------------------------------ + + def test_full_path_memory_limit_passes_through(self): + config = {**self._make_bsl_settings(), "resources.limits.memory": "512Mi"} + self._validate(config) + self.assertEqual(config.get("resources.limits.memory"), "512Mi") + + def test_full_path_cpu_limit_passes_through(self): + config = {**self._make_bsl_settings(), "resources.limits.cpu": "500m"} + self._validate(config) + self.assertEqual(config.get("resources.limits.cpu"), "500m") + + def test_full_path_controller_cpu_limit_passes_through(self): + config = {**self._make_bsl_settings(), "controller.resources.limits.cpu": "1"} + self._validate(config) + self.assertEqual(config.get("controller.resources.limits.cpu"), "1") + + def test_full_path_controller_memory_limit_passes_through(self): + config = {**self._make_bsl_settings(), "controller.resources.limits.memory": "512Mi"} + self._validate(config) + self.assertEqual(config.get("controller.resources.limits.memory"), "512Mi") + + def test_full_path_plugin_cpu_limit_passes_through(self): + config = { + **self._make_bsl_settings(), + "initContainers.veleroPluginForMicrosoftAzure.resources.limits.cpu": "1", + } + self._validate(config) + self.assertEqual( + config.get("initContainers.veleroPluginForMicrosoftAzure.resources.limits.cpu"), "1" + ) + + def test_full_path_plugin_memory_limit_passes_through(self): + config = { + **self._make_bsl_settings(), + "initContainers.veleroPluginForMicrosoftAzure.resources.limits.memory": "512Mi", + } + self._validate(config) + self.assertEqual( + config.get("initContainers.veleroPluginForMicrosoftAzure.resources.limits.memory"), "512Mi" + ) + + def test_full_path_strips_whitespace(self): + config = {**self._make_bsl_settings(), "resources.limits.memory": " 256Mi "} + self._validate(config) + self.assertEqual(config.get("resources.limits.memory"), "256Mi") + + def test_full_path_case_insensitive(self): + config = {**self._make_bsl_settings(), "Resources.Limits.Memory": "512Mi"} + self._validate(config) + self.assertEqual(config.get("resources.limits.memory"), "512Mi") + + # ------------------------------------------------------------------ + # Unrecognized keys are ignored + # ------------------------------------------------------------------ + + def test_unrecognized_key_is_ignored(self): + config = {**self._make_bsl_settings(), "unknownSetting": "value"} + self._validate(config) + self.assertNotIn("unknownSetting", config) + + # ------------------------------------------------------------------ + # BSL validation + # ------------------------------------------------------------------ + + def test_missing_bsl_key_raises_error(self): + config = { + "storageAccount": "myaccount", + "storageAccountResourceGroup": "myrg", + "storageAccountSubscriptionId": "mysub", + # missing blobContainer + } + with self.assertRaises(RequiredArgumentMissingError): + self._validate(config) + + def test_bsl_validation_skipped_when_disabled(self): + # Should not raise even with missing BSL settings + config = {"controllerCpuLimit": "1"} + self._validate(config, validate_bsl=False) + self.assertEqual(config.get("controller.resources.limits.cpu"), "1") + + # ------------------------------------------------------------------ + # Case-insensitive short name matching + # ------------------------------------------------------------------ + + def test_short_name_case_insensitive(self): + config = {**self._make_bsl_settings(), "CONTROLLERCPULIMIT": "500m"} + self._validate(config) + self.assertEqual(config.get("controller.resources.limits.cpu"), "500m") + + +if __name__ == "__main__": + unittest.main()