diff --git a/src/k8s-extension/azext_k8s_extension/partner_extensions/Dapr.py b/src/k8s-extension/azext_k8s_extension/partner_extensions/Dapr.py index e16a0cb1dee..f80f8540f33 100644 --- a/src/k8s-extension/azext_k8s_extension/partner_extensions/Dapr.py +++ b/src/k8s-extension/azext_k8s_extension/partner_extensions/Dapr.py @@ -4,43 +4,143 @@ # -------------------------------------------------------------------------------------------- # pylint: disable=unused-argument -# pylint: disable=line-too-long # pylint: disable=too-many-locals +# pylint: disable=too-many-instance-attributes +from typing import Tuple + +from azure.cli.core.azclierror import InvalidArgumentValueError +from knack.log import get_logger +from knack.prompting import prompt, prompt_y_n + +from ..vendored_sdks.models import Extension, Scope, ScopeCluster from .DefaultExtension import DefaultExtension -from ..vendored_sdks.models import ( - Extension, -) +logger = get_logger(__name__) class Dapr(DefaultExtension): def __init__(self): + self.TSG_LINK = "https://docs.microsoft.com/en-us/azure/aks/dapr" + self.DEFAULT_RELEASE_NAME = 'dapr' + self.DEFAULT_RELEASE_NAMESPACE = 'dapr-system' + self.DEFAULT_RELEASE_TRAIN = 'stable' + self.DEFAULT_CLUSTER_TYPE = 'managedclusters' + self.DEFAULT_HA = 'true' + # constants for configuration settings. - self.CLUSTER_TYPE = 'global.clusterType' + self.CLUSTER_TYPE_KEY = 'global.clusterType' + self.HA_KEY_ENABLED_KEY = 'global.ha.enabled' + self.SKIP_EXISTING_DAPR_CHECK_KEY = 'skipExistingDaprCheck' + self.EXISTING_DAPR_RELEASE_NAME_KEY = 'existingDaprReleaseName' + self.EXISTING_DAPR_RELEASE_NAMESPACE_KEY = 'existingDaprReleaseNamespace' + + # constants for message prompts. + self.MSG_IS_DAPR_INSTALLED = "Is Dapr already installed in the cluster?" + self.MSG_ENTER_RELEASE_NAME = "Enter the Helm release name for Dapr, "\ + f"or press Enter to use the default name [{self.DEFAULT_RELEASE_NAME}]: " + self.MSG_ENTER_RELEASE_NAMESPACE = "Enter the namespace where Dapr is installed, "\ + f"or press Enter to use the default namespace [{self.DEFAULT_RELEASE_NAMESPACE}]: " + self.MSG_WARN_EXISTING_INSTALLATION = "The extension will use your existing Dapr installation. "\ + f"Note, if you have updated the default values for global.ha.* or dapr_placement.* in your existing "\ + "Dapr installation, you must provide them via --configuration-settings. Failing to do so will result in"\ + "an error, since Helm upgrade will try to modify the StatefulSet."\ + f"Please refer to {self.TSG_LINK} for more information." + + self.RELEASE_INFO_HELP_STRING = "The Helm release name and namespace can be found by running 'helm list -A'." + + # constants for error messages. + self.ERR_MSG_INVALID_SCOPE_TPL = "Invalid scope '{}'. This extension can be installed only at 'cluster' scope."\ + f" Check {self.TSG_LINK} for more information." + + def _get_release_info(self, release_name: str, release_namespace: str, configuration_settings: dict)\ + -> Tuple[str, str, bool]: + ''' + Check if Dapr is already installed in the cluster and get the release name and namespace. + If user has provided the release name and namespace in configuration settings, use those. + Otherwise, prompt the user for the release name and namespace. + If Dapr is not installed, return the default release name and namespace. + ''' + name, namespace, dapr_exists = release_name, release_namespace, False + + # Set the default release name and namespace if not provided. + name = name or self.DEFAULT_RELEASE_NAME + namespace = namespace or self.DEFAULT_RELEASE_NAMESPACE + + if configuration_settings.get(self.SKIP_EXISTING_DAPR_CHECK_KEY, 'false') == 'true': + logger.info("%s is set to true, skipping existing Dapr check.", self.SKIP_EXISTING_DAPR_CHECK_KEY) + return name, namespace, False + + cfg_name = configuration_settings.get(self.EXISTING_DAPR_RELEASE_NAME_KEY, None) + cfg_namespace = configuration_settings.get(self.EXISTING_DAPR_RELEASE_NAMESPACE_KEY, None) - def Create(self, cmd, client, resource_group_name, cluster_name, name, cluster_type, cluster_rp, - extension_type, scope, auto_upgrade_minor_version, release_train, version, target_namespace, - release_namespace, configuration_settings, configuration_protected_settings, - configuration_settings_file, configuration_protected_settings_file): + # If the user has specified the release name and namespace in configuration settings, then use it. + if cfg_name and cfg_namespace: + logger.info("Using the release name and namespace specified in the configuration settings.") + return cfg_name, cfg_namespace, True + # If either release name or namespace is missing, ignore the configuration settings and prompt the user. + if cfg_name or cfg_namespace: + logger.warning("Both '%s' and '%s' must be specified via --configuration-settings. Only one of them is " + "specified, ignoring.", self.EXISTING_DAPR_RELEASE_NAME_KEY, + self.EXISTING_DAPR_RELEASE_NAMESPACE_KEY) + + # Check explictly if Dapr is already installed in the cluster. + # If so, reuse the release name and namespace to avoid conflicts. + if prompt_y_n(self.MSG_IS_DAPR_INSTALLED, default='n'): + dapr_exists = True + + name = prompt(self.MSG_ENTER_RELEASE_NAME, self.RELEASE_INFO_HELP_STRING) or self.DEFAULT_RELEASE_NAME + if release_name and name != release_name: + logger.warning("The release name has been changed from '%s' to '%s'.", release_name, name) + + namespace = prompt(self.MSG_ENTER_RELEASE_NAMESPACE, self.RELEASE_INFO_HELP_STRING)\ + or self.DEFAULT_RELEASE_NAMESPACE + if release_namespace and namespace != release_namespace: + logger.warning("The release namespace has been changed from '%s' to '%s'.", + release_namespace, namespace) + + return name, namespace, dapr_exists + + def Create(self, cmd, client, resource_group_name: str, cluster_name: str, name: str, cluster_type: str, + cluster_rp: str, extension_type: str, scope: str, auto_upgrade_minor_version: bool, + release_train: str, version: str, target_namespace: str, release_namespace: str, + configuration_settings: dict, configuration_protected_settings: dict, + configuration_settings_file: str, configuration_protected_settings_file: str): """ExtensionType 'Microsoft.Dapr' specific validations & defaults for Create Must create and return a valid 'ExtensionInstance' object. """ - if cluster_type.lower() == '' or cluster_type.lower() == 'managedclusters': - configuration_settings[self.CLUSTER_TYPE] = 'managedclusters' + # Dapr extension is only supported at the cluster scope. + if scope == 'namespace': + raise InvalidArgumentValueError(self.ERR_MSG_INVALID_SCOPE_TPL.format(scope)) + + release_name, release_namespace, dapr_exists = \ + self._get_release_info(name, release_namespace, configuration_settings) + + # Inform the user that the extension will be installed on an existing Dapr installation. + # Disable HA mode if Dapr is already installed in the cluster. + if dapr_exists: + logger.warning(self.MSG_WARN_EXISTING_INSTALLATION) + if self.HA_KEY_ENABLED_KEY not in configuration_settings: + configuration_settings[self.HA_KEY_ENABLED_KEY] = 'false' + + scope_cluster = ScopeCluster(release_namespace=release_namespace or self.DEFAULT_RELEASE_NAMESPACE) + extension_scope = Scope(cluster=scope_cluster, namespace=None) + + if cluster_type.lower() == '' or cluster_type.lower() == self.DEFAULT_CLUSTER_TYPE: + configuration_settings[self.CLUSTER_TYPE_KEY] = self.DEFAULT_CLUSTER_TYPE create_identity = False extension_instance = Extension( extension_type=extension_type, auto_upgrade_minor_version=auto_upgrade_minor_version, - release_train=release_train, + release_train=release_train or self.DEFAULT_RELEASE_TRAIN, version=version, - scope=scope, + scope=extension_scope, configuration_settings=configuration_settings, configuration_protected_settings=configuration_protected_settings, identity=None, location="" ) - return extension_instance, name, create_identity + return extension_instance, release_name, create_identity diff --git a/src/k8s-extension/azext_k8s_extension/tests/latest/test_k8s_extension_scenario.py b/src/k8s-extension/azext_k8s_extension/tests/latest/test_k8s_extension_scenario.py index 6a0369bfcdc..04c08611f31 100644 --- a/src/k8s-extension/azext_k8s_extension/tests/latest/test_k8s_extension_scenario.py +++ b/src/k8s-extension/azext_k8s_extension/tests/latest/test_k8s_extension_scenario.py @@ -28,7 +28,7 @@ def test_k8s_extension(self): self.cmd('k8s-extension create -g {rg} -n {name} -c {cluster_name} --cluster-type {cluster_type} ' '--extension-type {extension_type} --release-train {release_train} --version {version} ' - '--no-wait') + '--configuration-settings "skipExistingDaprCheck=true" --no-wait') # Update requires agent running in k8s cluster that is connected to Azure - so no update tests here # self.cmd('k8s-extension update -g {rg} -n {name} --tags foo=boo', checks=[ diff --git a/testing/test/extensions/public/Dapr.Tests.ps1 b/testing/test/extensions/public/Dapr.Tests.ps1 new file mode 100644 index 00000000000..5af40546c19 --- /dev/null +++ b/testing/test/extensions/public/Dapr.Tests.ps1 @@ -0,0 +1,63 @@ +Describe 'DAPR Testing' { + BeforeAll { + $extensionType = "microsoft.dapr" + $extensionName = "dapr" + $clusterType = "connectedClusters" + + . $PSScriptRoot/../../helper/Constants.ps1 + . $PSScriptRoot/../../helper/Helper.ps1 + } + + It 'Creates the extension and checks that it onboards correctly' { + $output = az $Env:K8sExtensionName create -c $($ENVCONFIG.arcClusterName) -g $($ENVCONFIG.resourceGroup) --cluster-type $clusterType -n $extensionName --extension-type $extensionType --configuration-settings "skipExistingDaprCheck=true" --no-wait + $? | Should -BeTrue + + $n = 0 + do + { + $output = az $Env:K8sExtensionName show -c $($ENVCONFIG.arcClusterName) -g $($ENVCONFIG.resourceGroup) --cluster-type $clusterType -n $extensionName + $provisioningState = ($output | ConvertFrom-Json).provisioningState + Write-Host "Provisioning State: $provisioningState" + if ($provisioningState -eq "Succeeded") { + break + } + Start-Sleep -Seconds 40 + $n += 1 + } while ($n -le $MAX_RETRY_ATTEMPTS) + $n | Should -BeLessOrEqual $MAX_RETRY_ATTEMPTS + } + + It "Performs a show on the extension" { + $output = az $Env:K8sExtensionName show -c $($ENVCONFIG.arcClusterName) -g $($ENVCONFIG.resourceGroup) --cluster-type $clusterType -n $extensionName + $? | Should -BeTrue + $output | Should -Not -BeNullOrEmpty + } + + It "Lists the extensions on the cluster" { + $output = az $Env:K8sExtensionName list -c $($ENVCONFIG.arcClusterName) -g $($ENVCONFIG.resourceGroup) --cluster-type $clusterType + $? | Should -BeTrue + + $output | Should -Not -BeNullOrEmpty + $extensionExists = $output | ConvertFrom-Json | Where-Object { $_.extensionType -eq $extensionType } + $extensionExists | Should -Not -BeNullOrEmpty + } + + It "Deletes the extension from the cluster" { + $output = az $Env:K8sExtensionName delete -c $($ENVCONFIG.arcClusterName) -g $($ENVCONFIG.resourceGroup) --cluster-type $clusterType -n $extensionName --force + $? | Should -BeTrue + + # Extension should not be found on the cluster + $output = az $Env:K8sExtensionName show -c $($ENVCONFIG.arcClusterName) -g $($ENVCONFIG.resourceGroup) --cluster-type $clusterType -n $extensionName + $? | Should -BeFalse + $output | Should -BeNullOrEmpty + } + + It "Performs another list after the delete" { + $output = az $Env:K8sExtensionName list -c $($ENVCONFIG.arcClusterName) -g $($ENVCONFIG.resourceGroup) --cluster-type $clusterType + $? | Should -BeTrue + $output | Should -Not -BeNullOrEmpty + + $extensionExists = $output | ConvertFrom-Json | Where-Object { $_.extensionType -eq $extensionName } + $extensionExists | Should -BeNullOrEmpty + } +} \ No newline at end of file