Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Dapr] Prompt user for existing Dapr installation during extension create #188

Merged
138 changes: 124 additions & 14 deletions src/k8s-extension/azext_k8s_extension/partner_extensions/Dapr.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,43 +4,153 @@
# --------------------------------------------------------------------------------------------

# pylint: disable=unused-argument
# pylint: disable=line-too-long
# pylint: disable=too-many-locals
# pylint: disable=too-many-instance-attributes

from typing import Dict, 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_PREFIX = 'global.ha.'
self.PLACEMENT_KEY_PREFIX = 'dapr_placement.'
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.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."\
shubham1172 marked this conversation as resolved.
Show resolved Hide resolved
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':
shubham1172 marked this conversation as resolved.
Show resolved Hide resolved
logger.debug("%s is set to true, skipping existing Dapr check.", self.SKIP_EXISTING_DAPR_CHECK_KEY)
return name, namespace, dapr_exists

# If the user has specified the release name and namespace in configuration settings, then use it.
if configuration_settings.get(self.EXISTING_DAPR_RELEASE_NAME_KEY, None) and \
configuration_settings.get(self.EXISTING_DAPR_RELEASE_NAMESPACE_KEY, None):
name = configuration_settings[self.EXISTING_DAPR_RELEASE_NAME_KEY]
namespace = configuration_settings[self.EXISTING_DAPR_RELEASE_NAMESPACE_KEY]
dapr_exists = True

logger.debug("Using the release name and namespace specified in the configuration settings.")

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):
return name, namespace, dapr_exists

# If either release name and namespace is missing, ignore the configuration settings and prompt the user.
if configuration_settings.get(self.EXISTING_DAPR_RELEASE_NAME_KEY, None) or \
configuration_settings.get(self.EXISTING_DAPR_RELEASE_NAMESPACE_KEY, None):
logger.warning("Both '%s' and '%s' must be specified in the configuration settings. Only one of them is "
"specified, ignoring.", self.EXISTING_DAPR_RELEASE_NAME_KEY,
self.EXISTING_DAPR_RELEASE_NAMESPACE_KEY)
shubham1172 marked this conversation as resolved.
Show resolved Hide resolved

# 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.info("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.info("The release namespace has been changed from '%s' to '%s'.", release_namespace, namespace)

return name, namespace, dapr_exists

def _reset_configuration_settings_for_existing_dapr(self, configuration_settings: dict) -> Dict:
'''
Reset some configuration settings for the extension if Dapr is already installed in the cluster.
This is required because some settings modify the StatefulSet and is unsupported by K8s.
'''
for key in configuration_settings.keys():
if key.startswith(self.HA_KEY_PREFIX) or key.startswith(self.PLACEMENT_KEY_PREFIX):
logger.warning("The configuration setting '%s' will be ignored as Dapr is already installed, "
"please refer to %s for more information.", key, self.TSG_LINK)
del configuration_settings[key]
return configuration_settings

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.
# Reset some configuration settings for the extension if Dapr is already installed in the cluster.
if dapr_exists:
shubham1172 marked this conversation as resolved.
Show resolved Hide resolved
logger.info("The extension will be installed on your existing Dapr installation. "
"Please refer to %s for more information.", self.TSG_LINK)
configuration_settings = self._reset_configuration_settings_for_existing_dapr(configuration_settings)
shubham1172 marked this conversation as resolved.
Show resolved Hide resolved

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
Original file line number Diff line number Diff line change
Expand Up @@ -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=[
Expand Down
63 changes: 63 additions & 0 deletions testing/test/extensions/public/Dapr.Tests.ps1
Original file line number Diff line number Diff line change
@@ -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
}
}