From 3cc845217e03c489ee1abe120a6d256c249c2695 Mon Sep 17 00:00:00 2001 From: Rishabh Raj Date: Thu, 25 Aug 2022 12:33:42 +0530 Subject: [PATCH 01/16] Kubernetes Data Protection Extension CLI (#173) * First draft for Data Protection K8s backup extension (Pending internal review) * Removing tracing * Minor changes to improve azdev style * Internal PR review feedback Co-authored-by: Rishabh Raj --- .../azext_k8s_extension/_client_factory.py | 10 + .../azext_k8s_extension/custom.py | 2 + .../DataProtectionKubernetes.py | 192 ++++++++++++++++++ 3 files changed, 204 insertions(+) create mode 100644 src/k8s-extension/azext_k8s_extension/partner_extensions/DataProtectionKubernetes.py diff --git a/src/k8s-extension/azext_k8s_extension/_client_factory.py b/src/k8s-extension/azext_k8s_extension/_client_factory.py index e507a64b1d8..0ecf3a7ee15 100644 --- a/src/k8s-extension/azext_k8s_extension/_client_factory.py +++ b/src/k8s-extension/azext_k8s_extension/_client_factory.py @@ -51,3 +51,13 @@ def cf_log_analytics(cli_ctx, subscription_id=None): def _resource_providers_client(cli_ctx): from azure.mgmt.resource import ResourceManagementClient return get_mgmt_service_client(cli_ctx, ResourceManagementClient).providers + + +def cf_storage(cli_ctx, subscription_id=None): + from azure.mgmt.storage import StorageManagementClient + return get_mgmt_service_client(cli_ctx, StorageManagementClient, subscription_id=subscription_id) + + +def cf_managed_clusters(cli_ctx, subscription_id=None): + from azure.mgmt.containerservice import ContainerServiceClient + return get_mgmt_service_client(cli_ctx, ContainerServiceClient, subscription_id=subscription_id).managed_clusters diff --git a/src/k8s-extension/azext_k8s_extension/custom.py b/src/k8s-extension/azext_k8s_extension/custom.py index 249bfcfd5b2..e8769dbc0b6 100644 --- a/src/k8s-extension/azext_k8s_extension/custom.py +++ b/src/k8s-extension/azext_k8s_extension/custom.py @@ -27,6 +27,7 @@ from .partner_extensions.AzureDefender import AzureDefender from .partner_extensions.OpenServiceMesh import OpenServiceMesh from .partner_extensions.AzureMLKubernetes import AzureMLKubernetes +from .partner_extensions.DataProtectionKubernetes import DataProtectionKubernetes from .partner_extensions.Dapr import Dapr from .partner_extensions.DefaultExtension import ( DefaultExtension, @@ -47,6 +48,7 @@ def ExtensionFactory(extension_name): "microsoft.openservicemesh": OpenServiceMesh, "microsoft.azureml.kubernetes": AzureMLKubernetes, "microsoft.dapr": Dapr, + "microsoft.dataprotection.kubernetes": DataProtectionKubernetes, } # Return the extension if we find it in the map, else return the default diff --git a/src/k8s-extension/azext_k8s_extension/partner_extensions/DataProtectionKubernetes.py b/src/k8s-extension/azext_k8s_extension/partner_extensions/DataProtectionKubernetes.py new file mode 100644 index 00000000000..3b5b1fe5534 --- /dev/null +++ b/src/k8s-extension/azext_k8s_extension/partner_extensions/DataProtectionKubernetes.py @@ -0,0 +1,192 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + +# pylint: disable=unused-argument +from knack.log import get_logger +from azure.cli.core.commands.client_factory import get_subscription_id +from azure.cli.core.azclierror import RequiredArgumentMissingError, InvalidArgumentValueError + +from .DefaultExtension import DefaultExtension +from .._client_factory import cf_storage, cf_managed_clusters +from ..vendored_sdks.models import (Extension, PatchExtension, Scope, ScopeCluster) + +logger = get_logger(__name__) + + +class DataProtectionKubernetes(DefaultExtension): + def __init__(self): + """Constants for configuration settings + - Tenant Id (required) + - Backup storage location (required) + - Resource Limits (optional) + """ + self.TENANT_ID = "credentials.tenantId" + self.BACKUP_STORAGE_ACCOUNT_CONTAINER = "configuration.backupStorageLocation.bucket" + self.BACKUP_STORAGE_ACCOUNT_NAME = "configuration.backupStorageLocation.config.storageAccount" + self.BACKUP_STORAGE_ACCOUNT_RESOURCE_GROUP = "configuration.backupStorageLocation.config.resourceGroup" + self.BACKUP_STORAGE_ACCOUNT_SUBSCRIPTION = "configuration.backupStorageLocation.config.subscriptionId" + self.RESOURCE_LIMIT_CPU = "resources.limits.cpu" + self.RESOURCE_LIMIT_MEMORY = "resources.limits.memory" + + self.blob_container = "blobContainer" + self.storage_account = "storageAccount" + self.storage_account_resource_group = "storageAccountResourceGroup" + self.storage_account_subsciption = "storageAccountSubscriptionId" + self.cpu_limit = "cpuLimit" + self.memory_limit = "memoryLimit" + + self.configuration_mapping = { + self.blob_container.lower(): self.BACKUP_STORAGE_ACCOUNT_CONTAINER, + self.storage_account.lower(): self.BACKUP_STORAGE_ACCOUNT_NAME, + self.storage_account_resource_group.lower(): self.BACKUP_STORAGE_ACCOUNT_RESOURCE_GROUP, + self.storage_account_subsciption.lower(): self.BACKUP_STORAGE_ACCOUNT_SUBSCRIPTION, + self.cpu_limit.lower(): self.RESOURCE_LIMIT_CPU, + self.memory_limit.lower(): self.RESOURCE_LIMIT_MEMORY + } + + self.bsl_configuration_settings = [ + self.blob_container, + self.storage_account, + self.storage_account_resource_group, + self.storage_account_subsciption + ] + + 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 + ): + # Current scope of DataProtection Kubernetes Backup extension is 'cluster' #TODO: add TSGs when they are in place + if scope == 'namespace': + raise InvalidArgumentValueError(f"Invalid scope '{scope}'. This extension can only be installed at 'cluster' scope.") + + scope_cluster = ScopeCluster(release_namespace=release_namespace) + ext_scope = Scope(cluster=scope_cluster, namespace=None) + + if cluster_type.lower() != 'managedclusters': + raise InvalidArgumentValueError(f"Invalid cluster type '{cluster_type}'. This extension can only be installed for managed clusters.") + + if release_namespace is not None: + logger.warning(f"Ignoring 'release-namespace': {release_namespace}") + + tenant_id = self.__get_tenant_id(cmd.cli_ctx) + if not tenant_id: + raise SystemExit(logger.error("Unable to fetch TenantId. Please check your subscription or run 'az login' to login to Azure.")) + + self.__validate_and_map_config(configuration_settings) + self.__validate_backup_storage_account(cmd.cli_ctx, resource_group_name, cluster_name, configuration_settings) + + configuration_settings[self.TENANT_ID] = tenant_id + + if release_train is None: + release_train = 'stable' + + create_identity = True + extension = Extension( + extension_type=extension_type, + auto_upgrade_minor_version=True, + release_train=release_train, + scope=ext_scope, + configuration_settings=configuration_settings + ) + return extension, name, create_identity + + def Update( + self, + cmd, + resource_group_name, + cluster_name, + auto_upgrade_minor_version, + release_train, + version, + configuration_settings, + configuration_protected_settings, + original_extension, + yes=False, + ): + if configuration_settings is None: + configuration_settings = {} + + if len(configuration_settings) > 0: + bsl_specified = self.__is_bsl_specified(configuration_settings) + self.__validate_and_map_config(configuration_settings, validate_bsl=bsl_specified) + if bsl_specified: + self.__validate_backup_storage_account(cmd.cli_ctx, resource_group_name, cluster_name, configuration_settings) + + return PatchExtension( + auto_upgrade_minor_version=True, + release_train=release_train, + configuration_settings=configuration_settings, + ) + + def __get_tenant_id(self, cli_ctx): + from azure.cli.core._profile import Profile + if not cli_ctx.data.get('tenant_id'): + cli_ctx.data['tenant_id'] = Profile(cli_ctx=cli_ctx).get_subscription()['tenantId'] + return cli_ctx.data['tenant_id'] + + def __validate_and_map_config(self, configuration_settings, validate_bsl=True): + """Validate and set configuration settings for Data Protection K8sBackup extension""" + input_configuration_settings = dict(configuration_settings.items()) + input_configuration_keys = [key.lower() for key in configuration_settings] + + if validate_bsl: + for key in self.bsl_configuration_settings: + if key.lower() not in input_configuration_keys: + raise RequiredArgumentMissingError(f"Missing required configuration setting: {key}") + + 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) + else: + configuration_settings.pop(key) + logger.warning(f"Ignoring unrecognized configuration setting: {key}") + + def __validate_backup_storage_account(self, cli_ctx, resource_group_name, cluster_name, configuration_settings): + """Validations performed on the backup storage account + - Existance of the storage account + - Cluster and storage account are in the same location + """ + sa_subscription_id = configuration_settings[self.BACKUP_STORAGE_ACCOUNT_SUBSCRIPTION] + storage_account_client = cf_storage(cli_ctx, sa_subscription_id).storage_accounts + + storage_account = storage_account_client.get_properties( + configuration_settings[self.BACKUP_STORAGE_ACCOUNT_RESOURCE_GROUP], + configuration_settings[self.BACKUP_STORAGE_ACCOUNT_NAME]) + + cluster_subscription_id = get_subscription_id(cli_ctx) + managed_clusters_client = cf_managed_clusters(cli_ctx, cluster_subscription_id) + managed_cluster = managed_clusters_client.get( + resource_group_name, + cluster_name) + + if managed_cluster.location != storage_account.location: + error_message = f"The Kubernetes managed cluster '{cluster_name} ({managed_cluster.location})' and the backup storage account '{configuration_settings[self.BACKUP_STORAGE_ACCOUNT_NAME]} ({storage_account.location})' are not in the same location. Please make sure that the cluster and the storage account are in the same location." + raise SystemExit(logger.error(error_message)) + + def __is_bsl_specified(self, configuration_settings): + """Check if the backup storage account is specified in the input""" + input_configuration_keys = [key.lower() for key in configuration_settings] + for key in self.bsl_configuration_settings: + if key.lower() in input_configuration_keys: + return True + return False From 6c56613a6844d28c1895e3fde1f6e7f86e7a4681 Mon Sep 17 00:00:00 2001 From: bragi92 Date: Wed, 28 Sep 2022 12:18:54 -0700 Subject: [PATCH 02/16] {AKS - ARC} fix: Update DCR creation to Clusters resource group instead of workspace (#175) * fix: Update DCR creation to Clusters resource group instead of workspace * . * . * casing check --- .../partner_extensions/ContainerInsights.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/k8s-extension/azext_k8s_extension/partner_extensions/ContainerInsights.py b/src/k8s-extension/azext_k8s_extension/partner_extensions/ContainerInsights.py index 8f1271b2593..fbbc45ac325 100644 --- a/src/k8s-extension/azext_k8s_extension/partner_extensions/ContainerInsights.py +++ b/src/k8s-extension/azext_k8s_extension/partner_extensions/ContainerInsights.py @@ -587,8 +587,8 @@ def _ensure_container_insights_dcr_for_monitoring(cmd, subscription_id, cluster_ raise ex # extract subscription ID and resource group from workspace_resource_id URL - parsed = parse_resource_id(workspace_resource_id) - workspace_subscription_id, workspace_resource_group = parsed["subscription"], parsed["resource_group"] + parsed = parse_resource_id(workspace_resource_id.lower()) + workspace_subscription_id = parsed["subscription"] workspace_region = '' resources = cf_resources(cmd.cli_ctx, workspace_subscription_id) try: @@ -601,7 +601,7 @@ def _ensure_container_insights_dcr_for_monitoring(cmd, subscription_id, cluster_ raise ex dataCollectionRuleName = f"MSCI-{cluster_name}-{cluster_region}" - dcr_resource_id = f"/subscriptions/{workspace_subscription_id}/resourceGroups/{workspace_resource_group}/providers/Microsoft.Insights/dataCollectionRules/{dataCollectionRuleName}" + dcr_resource_id = f"/subscriptions/{subscription_id}/resourceGroups/{cluster_resource_group_name}/providers/Microsoft.Insights/dataCollectionRules/{dataCollectionRuleName}" # first get the association between region display names and region IDs (because for some reason # the "which RPs are available in which regions" check returns region display names) From 083ed6d4d331f3be8cc38f47d0edf59d36354ebe Mon Sep 17 00:00:00 2001 From: Yue Yu Date: Tue, 27 Sep 2022 06:50:00 +0000 Subject: [PATCH 03/16] Add self-signed cert to fix PR gate for azureml extension --- testing/.gitignore | 9 + testing/Bootstrap.ps1 | 80 +++++++ testing/Cleanup.ps1 | 30 +++ testing/README.md | 128 ++++++++++ testing/Test.ps1 | 133 +++++++++++ .../k8s_configuration-1.0.0-py3-none-any.whl | Bin 0 -> 42351 bytes .../bin/k8s_extension-0.4.0-py3-none-any.whl | Bin 0 -> 55464 bytes testing/docs/test_authoring.md | 142 +++++++++++ testing/owners.txt | 2 + testing/pipeline/k8s-custom-pipelines.yml | 204 ++++++++++++++++ .../pipeline/templates/build-extension.yml | 52 +++++ testing/pipeline/templates/run-test.yml | 104 +++++++++ testing/settings.template.json | 11 + .../Configuration.HTTPS.Tests.ps1 | 54 +++++ .../Configuration.HelmOperator.Tests.ps1 | 137 +++++++++++ .../Configuration.KnownHost.Tests.ps1 | 6 + .../Configuration.PrivateKey.Tests.ps1 | 86 +++++++ .../configurations/Configuration.Tests.ps1 | 80 +++++++ testing/test/configurations/Constants.ps1 | 8 + testing/test/configurations/Helper.ps1 | 45 ++++ .../extensions/data/azure_ml/test_cert.pem | 32 +++ .../extensions/data/azure_ml/test_key.pem | 52 +++++ .../extensions/public/AzureDefender.Tests.ps1 | 71 ++++++ .../public/AzureMLKubernetes.Tests.ps1 | 221 ++++++++++++++++++ .../extensions/public/AzureMonitor.Tests.ps1 | 68 ++++++ .../extensions/public/AzurePolicy.Tests.ps1 | 74 ++++++ .../extensions/public/Cassandra.Tests.ps1 | 98 ++++++++ .../public/OpenServiceMesh.Tests.ps1 | 72 ++++++ testing/test/helper/Constants.ps1 | 7 + testing/test/helper/Helper.ps1 | 72 ++++++ 30 files changed, 2078 insertions(+) create mode 100644 testing/.gitignore create mode 100644 testing/Bootstrap.ps1 create mode 100644 testing/Cleanup.ps1 create mode 100644 testing/README.md create mode 100644 testing/Test.ps1 create mode 100644 testing/bin/k8s_configuration-1.0.0-py3-none-any.whl create mode 100644 testing/bin/k8s_extension-0.4.0-py3-none-any.whl create mode 100644 testing/docs/test_authoring.md create mode 100644 testing/owners.txt create mode 100644 testing/pipeline/k8s-custom-pipelines.yml create mode 100644 testing/pipeline/templates/build-extension.yml create mode 100644 testing/pipeline/templates/run-test.yml create mode 100644 testing/settings.template.json create mode 100644 testing/test/configurations/Configuration.HTTPS.Tests.ps1 create mode 100644 testing/test/configurations/Configuration.HelmOperator.Tests.ps1 create mode 100644 testing/test/configurations/Configuration.KnownHost.Tests.ps1 create mode 100644 testing/test/configurations/Configuration.PrivateKey.Tests.ps1 create mode 100644 testing/test/configurations/Configuration.Tests.ps1 create mode 100644 testing/test/configurations/Constants.ps1 create mode 100644 testing/test/configurations/Helper.ps1 create mode 100644 testing/test/extensions/data/azure_ml/test_cert.pem create mode 100644 testing/test/extensions/data/azure_ml/test_key.pem create mode 100644 testing/test/extensions/public/AzureDefender.Tests.ps1 create mode 100644 testing/test/extensions/public/AzureMLKubernetes.Tests.ps1 create mode 100644 testing/test/extensions/public/AzureMonitor.Tests.ps1 create mode 100644 testing/test/extensions/public/AzurePolicy.Tests.ps1 create mode 100644 testing/test/extensions/public/Cassandra.Tests.ps1 create mode 100644 testing/test/extensions/public/OpenServiceMesh.Tests.ps1 create mode 100644 testing/test/helper/Constants.ps1 create mode 100644 testing/test/helper/Helper.ps1 diff --git a/testing/.gitignore b/testing/.gitignore new file mode 100644 index 00000000000..5687a0bf32d --- /dev/null +++ b/testing/.gitignore @@ -0,0 +1,9 @@ +settings.json +tmp/ +bin/* +!bin/connectedk8s-1.0.0-py3-none-any.whl +!bin/k8s_extension-0.4.0-py3-none-any.whl +!bin/k8s_extension_private-0.1.0-py3-none-any.whl +!bin/k8s_configuration-1.0.0-py3-none-any.whl +!bin/connectedk8s-values.yaml +*.xml \ No newline at end of file diff --git a/testing/Bootstrap.ps1 b/testing/Bootstrap.ps1 new file mode 100644 index 00000000000..0598b139c79 --- /dev/null +++ b/testing/Bootstrap.ps1 @@ -0,0 +1,80 @@ +param ( + [switch] $SkipInstall, + [switch] $CI +) + +# Disable confirm prompt for script +az config set core.disable_confirm_prompt=true + +# Configuring the environment +$ENVCONFIG = Get-Content -Path $PSScriptRoot/settings.json | ConvertFrom-Json + +az account set --subscription $ENVCONFIG.subscriptionId + +if (-not (Test-Path -Path $PSScriptRoot/tmp)) { + New-Item -ItemType Directory -Path $PSScriptRoot/tmp +} + +if (!$SkipInstall) { + Write-Host "Removing the old connnectedk8s extension..." + az extension remove -n connectedk8s + Write-Host "Installing connectedk8s..." + az extension add -n connectedk8s + if (!$?) { + Write-Host "Unable to install connectedk8s, exiting..." + exit 1 + } +} + +Write-Host "Onboard cluster to Azure...starting!" + +az group show --name $envConfig.resourceGroup +if (!$?) { + Write-Host "Resource group does not exist, creating it now in region 'eastus2euap'" + az group create --name $envConfig.resourceGroup --location eastus2euap + + if (!$?) { + Write-Host "Failed to create Resource Group - exiting!" + Exit 1 + } +} + +# Skip creating the AKS Cluster if this is CI +if (!$CI) { + az aks show -g $ENVCONFIG.resourceGroup -n $ENVCONFIG.aksClusterName + if (!$?) { + Write-Host "Cluster does not exist, creating it now" + az aks create -g $ENVCONFIG.resourceGroup -n $ENVCONFIG.aksClusterName --generate-ssh-keys + } else { + Write-Host "Cluster already exists, no need to create it." + } + + Write-Host "Retrieving credentials for your AKS cluster..." + + az aks get-credentials -g $ENVCONFIG.resourceGroup -n $ENVCONFIG.aksClusterName -f tmp/KUBECONFIG + if (!$?) + { + Write-Host "Cluster did not create successfully, exiting!" -ForegroundColor Red + Exit 1 + } + Write-Host "Successfully retrieved the AKS kubectl credentials" +} else { + Copy-Item $HOME/.kube/config -Destination $PSScriptRoot/tmp/KUBECONFIG +} + +az connectedk8s show -g $ENVCONFIG.resourceGroup -n $ENVCONFIG.arcClusterName +if ($?) +{ + Write-Host "Cluster is already connected, no need to re-connect" + Exit 0 +} + +Write-Host "Connecting the cluster to Arc with connectedk8s..." +$Env:KUBECONFIG="$PSScriptRoot/tmp/KUBECONFIG" +az connectedk8s connect -g $ENVCONFIG.resourceGroup -n $ENVCONFIG.arcClusterName +if (!$?) +{ + kubectl get pods -A + Exit 1 +} +Write-Host "Successfully onboarded the cluster to Azure" \ No newline at end of file diff --git a/testing/Cleanup.ps1 b/testing/Cleanup.ps1 new file mode 100644 index 00000000000..cef1e2d1bc3 --- /dev/null +++ b/testing/Cleanup.ps1 @@ -0,0 +1,30 @@ +param ( + [switch] $CI +) + +# Disable confirm prompt for script +az config set core.disable_confirm_prompt=true + +$ENVCONFIG = Get-Content -Path $PSScriptRoot/settings.json | ConvertFrom-Json + +az account set --subscription $ENVCONFIG.subscriptionId + +$Env:KUBECONFIG="$PSScriptRoot/tmp/KUBECONFIG" +Write-Host "Removing the connectedk8s arc agents from the cluster..." +az connectedk8s delete -g $ENVCONFIG.resourceGroup -n $ENVCONFIG.arcClusterName +if (!$?) +{ + kubectl get pods -A + kubectl logs -l app.kubernetes.io/component=cluster-metadata-operator -n azure-arc -c cluster-metadata-operator + Exit 0 +} + +# Skip deleting the AKS Cluster if this is CI +if (!$CI) { + Write-Host "Deleting the AKS cluster from Azure..." + az aks delete -g $ENVCONFIG.resourceGroup -n $ENVCONFIG.aksClusterName + if (Test-Path -Path $PSScriptRoot/tmp) { + Write-Host "Deleting the tmp directory from the test directory" + Remove-Item -Path $PSScriptRoot/tmp -Force -Confirm:$false + } +} \ No newline at end of file diff --git a/testing/README.md b/testing/README.md new file mode 100644 index 00000000000..088a819a429 --- /dev/null +++ b/testing/README.md @@ -0,0 +1,128 @@ +# K8s Partner Extension Test Suite + +This repository serves as the integration testing suite for the `k8s-extension` Azure CLI module. + +## Testing Requirements + +All partners who wish to merge their __Custom Private Preview Release__ (owner: _Partner_) into the __Official Private Preview Release__ are required to author additional integration tests for their extension to ensure that their extension will continue to function correctly as more extensions are added into the __Official Private Preview Release__. + +For more information on creating these tests, see [Authoring Tests](docs/test_authoring.md) + +## Pre-Requisites + +In order to properly test all regression tests within the test suite, you must onboard an AKS cluster which you will use to generate your Azure Arc resource to test the extensions. Ensure that you have a resource group where you can onboard this cluster. + +### Required Installations + +The following installations are required in your environment for the integration tests to run correctly: + +1. [Helm 3](https://helm.sh/docs/intro/install/) +2. [Kubectl](https://kubernetes.io/docs/tasks/tools/install-kubectl/) +3. [Azure CLI](https://docs.microsoft.com/en-us/cli/azure/install-azure-cli) + +## Setup + +### Step 1: Install Pester + +This project contains [Pester](https://pester.dev/) test framework commands that are required for the integration tests to run. In an admin powershell terminal, run + +```powershell +Install-Module Pester -Force -SkipPublisherCheck +Import-Module Pester -PassThru +``` + +If you run into issues installing the framework, refer to the [Installation Guide](https://pester.dev/docs/introduction/installation) provided by the Pester docs. + +### Step 2: Get Test suite files + +You can either clone this repo (preferred option, since you will be adding your tests to this suite) or copy the files in this repo locally. Rest of the instructions here assume your working directory is k8spartner-extension-testing. + +### Step 3: Generate `k8s-extension` .whl package + +You can perform the generation of the `k8s-extension` .whl package by running the following command + +```bash +azdev setup -r . -e k8s-extension +azdev extension build k8s-extension +``` + +Additional guidance for build the extension .whl package can be found [here](https://github.com/Azure/azure-cli/blob/master/doc/extensions/authoring.md#building) + +### Step 4: Update the `k8s-extension`/`k8s-extension-private` .whl package + +This integration test suite references the .whl packages found in the `\bin` directory. After generating your `k8s-extension`/`k8s-extension-private` .whl package, copy your updated package into the `\bin` directory. + + +### Step 5: Create a `settings.json` + +To onboard the AKS and Arc clusters correctly, you will need to create a `settings.json` configuration. Create a new `settings.json` file by copying the contents of the `settings.template.json` into this file. Update the subscription id, resource group, and AKS and Arc cluster name fields with your specific values. + +### Step 6: Update the extension version value in `settings.json` + +To ensure that the tests point to your `k8s-extension-private` `.whl` package, change the value of the `k8s-extension-private` to match your package versioning in the format (Major.Minor.Patch.Extension). For example, the `k8s_extension_private-0.1.0.openservicemesh_5-py3-none-any.whl` whl package would have extension versions set to +```json +{ + "k8s-extension": "0.1.0", + "k8s-extension-private": "0.1.0.openservicemesh_5", + "connectedk8s": "0.3.5" +} + +``` + +_Note: Updates to the `connectedk8s` version and `k8s-extension` version can also be made by adding a different version of the `connectedk8s` and `k8s-extension` whl packages and changing the `connectedk8s` and `k8s-extension` values to match the (Major.Minor.Patch) version format shown above_ + +### Step 7: Run the Bootstrap Command +To bootstrap the environment with AKS and Arc clusters, run +```powershell +.\Bootstrap.ps1 +``` +This script will provision the AKS and Arc clusters needed to run the integration test suite + +## Testing + +### Testing All Extension Suites +To test all extension test suites, you must call `.\Test.ps1` with the `-ExtensionType` parameter set to either `Public` or `Private`. Based on this flag, the test suite will install the extension type specified below + +| `-ExtensionType` | Installs `az extension` | +| ---------------- | --------------------- | +| `Public` | `k8s-extension` | +| `Private` | `k8s-extension-private` | + +For example, when calling +```bash +.\Test.ps1 -ExtensionType Public +``` +the script will install your `k8s-extension` whl package and run the full test suite of `*.Tests.ps1` files included in the `\test\extensions` directory + +### Testing Public Extensions Only +If you only want to run the test cases against public-preview or GA extension test cases, you can use the `-OnlyPublicTests` flag to specify this +```bash +.\Test.ps1 -ExtensionType Public -OnlyPublicTests +``` + +### Testing Specific Extension Suite + +If you only want to run the test script on your specific test file, you can do so by specifying path to your extension test suite in the execution call + +```powershell +.\Test.ps1 -Path +``` +For example to call the `AzureMonitor.Tests.ps1` test suite, we run +```powershell +.\Test.ps1 -ExtensionType Public -Path .\test\extensions\public\AzureMonitor.Tests.ps1 +``` + +### Skipping Extension Re-Install + +By default the `Test.ps1` script will uninstall any old versions of `k8s-extension`/'`k8s-extension-private` and re-install the version specified in `settings.json`. If you do not want this re-installation to occur, you can specify the `-SkipInstall` flag to skip this process. + +```powershell +.\Test.ps1 -ExtensionType Public -SkipInstall +``` + +## Cleanup +To cleanup the AKS and Arc clusters you have provisioned in testing, run +```powershell +.\Cleanup.ps1 +``` +This will remove the AKS and Arc clusters as well as the `\tmp` directory that were created by the bootstrapping script. \ No newline at end of file diff --git a/testing/Test.ps1 b/testing/Test.ps1 new file mode 100644 index 00000000000..c1d4f093b2e --- /dev/null +++ b/testing/Test.ps1 @@ -0,0 +1,133 @@ +param ( + [string] $Path, + [switch] $SkipInstall, + [switch] $CI, + [switch] $ParallelCI, + [switch] $OnlyPublicTests, + + [Parameter(Mandatory=$True)] + [ValidateSet('k8s-extension','k8s-configuration', 'k8s-extension-private')] + [string]$Type +) + +# Disable confirm prompt for script +# Only show errors, don't show warnings +az config set core.disable_confirm_prompt=true +az config set core.only_show_errors=true + +$ENVCONFIG = Get-Content -Path $PSScriptRoot/settings.json | ConvertFrom-Json + +az account set --subscription $ENVCONFIG.subscriptionId + +$Env:KUBECONFIG="$PSScriptRoot/tmp/KUBECONFIG" +$TestFileDirectory="$PSScriptRoot/results" + +if (-not (Test-Path -Path $TestFileDirectory)) { + New-Item -ItemType Directory -Path $TestFileDirectory +} + +if ($Type -eq 'k8s-extension') { + $k8sExtensionVersion = $ENVCONFIG.extensionVersion.'k8s-extension' + $Env:K8sExtensionName = "k8s-extension" + + if (!$SkipInstall) { + Write-Host "Removing the old k8s-extension extension..." + az extension remove -n k8s-extension + Write-Host "Installing k8s-extension version $k8sExtensionVersion..." + az extension add --source ./bin/k8s_extension-$k8sExtensionVersion-py3-none-any.whl + if (!$?) { + Write-Host "Unable to find k8s-extension version $k8sExtensionVersion, exiting..." + exit 1 + } + } + if ($OnlyPublicTests) { + $testFilePath = "$PSScriptRoot/test/extensions/public" + } else { + $testFilePath = "$PSScriptRoot/test/extensions" + } +} elseif ($Type -eq 'k8s-extension-private') { + $k8sExtensionPrivateVersion = $ENVCONFIG.extensionVersion.'k8s-extension-private' + $Env:K8sExtensionName = "k8s-extension-private" + + if (!$SkipInstall) { + Write-Host "Removing the old k8s-extension-private extension..." + az extension remove -n k8s-extension-private + Write-Host "Installing k8s-extension-private version $k8sExtensionPrivateVersion..." + az extension add --source ./bin/k8s_extension_private-$k8sExtensionPrivateVersion-py3-none-any.whl + if (!$?) { + Write-Host "Unable to find k8s-extension-private version $k8sExtensionPrivateVersion, exiting..." + exit 1 + } + } + if ($OnlyPublicTests) { + $testFilePath = "$PSScriptRoot/test/extensions/public" + } else { + $testFilePath = "$PSScriptRoot/test/extensions" + } +} elseif ($Type -eq 'k8s-configuration') { + $k8sConfigurationVersion = $ENVCONFIG.extensionVersion.'k8s-configuration' + if (!$SkipInstall) { + Write-Host "Removing the old k8s-configuration extension..." + az extension remove -n k8s-configuration + Write-Host "Installing k8s-configuration version $k8sConfigurationVersion..." + az extension add --source ./bin/k8s_configuration-$k8sConfigurationVersion-py3-none-any.whl + } + $testFilePaths = "$PSScriptRoot/test/configurations" +} + +if ($ParallelCI) { + # This runs the tests in parallel during the CI pipline to speed up testing + + Write-Host "Invoking Pester to run tests from '$testFilePath'..." + $testFiles = @() + foreach ($paths in $testFilePaths) + { + $temp = Get-ChildItem $paths + $testFiles += $temp + } + $resultFileNumber = 0 + foreach ($testFile in $testFiles) + { + $resultFileNumber++ + $testName = Split-Path $testFile –leaf + Start-Job -ArgumentList $testName, $testFile, $resultFileNumber, $TestFileDirectory -Name $testName -ScriptBlock { + param($name, $testFile, $resultFileNumber, $testFileDirectory) + + Write-Host "$testFile to result file #$resultFileNumber" + $testResult = Invoke-Pester $testFile -Passthru -Output Detailed + $testResult | Export-JUnitReport -Path "$testFileDirectory/$name.xml" + } + } + + do { + Write-Host ">> Still running tests @ $(Get-Date –Format "HH:mm:ss")" –ForegroundColor Blue + Get-Job | Where-Object { $_.State -eq "Running" } | Format-Table –AutoSize + Start-Sleep –Seconds 30 + } while((Get-Job | Where-Object { $_.State -eq "Running" } | Measure-Object).Count -ge 1) + + Get-Job | Wait-Job + $failedJobs = Get-Job | Where-Object { -not ($_.State -eq "Completed")} + Get-Job | Receive-Job –AutoRemoveJob –Wait –ErrorAction 'Continue' + + if ($failedJobs.Count -gt 0) { + Write-Host "Failed Jobs" –ForegroundColor Red + $failedJobs + throw "One or more tests failed" + } +} elseif ($CI) { + if ($Path) { + $testFilePath = "$PSScriptRoot/$Path" + } + Write-Host "Invoking Pester to run tests from '$testFilePath'..." + $testResult = Invoke-Pester $testFilePath -Passthru -Output Detailed + $testName = Split-Path $testFilePath –leaf + $testResult | Export-JUnitReport -Path "$testFileDirectory/$testName.xml" +} else { + if ($Path) { + Write-Host "Invoking Pester to run tests from '$PSScriptRoot/$Path'" + Invoke-Pester -Output Detailed $PSScriptRoot/$Path + } else { + Write-Host "Invoking Pester to run tests from '$testFilePath'..." + Invoke-Pester -Output Detailed $testFilePath + } +} diff --git a/testing/bin/k8s_configuration-1.0.0-py3-none-any.whl b/testing/bin/k8s_configuration-1.0.0-py3-none-any.whl new file mode 100644 index 0000000000000000000000000000000000000000..cc8e8e0995f81da31f6f919f83d344cc164e9568 GIT binary patch literal 42351 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%0De{h@2i2mo+>9hAiTOe4VIP9}qG z?N z*dxxlIwr+gA?gZSAO|Or<(gb`;6&65j~K*SWnr*b6A;3JgR%#Irmz}pW^s_VP2qUp ze3R{o<0svQt(mshpx$lRO9wke5lHZEfkcJ|=YsXB=^oX=Z8ocq8B#Rc@K9^6ccig< zk8i`Qfn4^wHPnz*;gPmt`vC6?a>|IeVeK#4eUpJ6h`K+<$ik8g>^ki(+7zonbw&pK zNW)koYV;TxUB4XFo#(#wQzg#bL2`ArRlQi^|BeZX@pQ5KRWcqe=IwIV@dT0cZ1SXg z{VD0iwZEX+tsIVDZh%$rg;9-HUdfbMi0Eq;di%m4J^ypB&b?nOW+YzccMXSx*m%)n z_(~C2el1*8MN#vbHy=ta;DU-{xC0yUHl4yDTX( zBffEq@TTJT=weoNuCb)|4;=5GA|tUDakNrvYg*4WV10=c|J!7jS|l1cF}PGr%{E1^ zdp`yuWjzW?$puxf?NfKE)+yrqGAP4Q&bo-s!y7Qt3Ve9<~1bsA>FW&c=kzr5Tyr(9I?mU1^Cqd#sA0Bb)i zE7*~8E4vgzA|P=2UKA&~v|z_K=iA%~yhx`jie9qA7P>p|eFPgAjzr?UE;JBpa8%DmYF! z3Jz&vKtA&7i((IVsO0cLKO@{^OGefeBOTVbPS_&KS;YOzx7tWS;E!oCBJxG9Mp!yg zDyks6MXa^2fM*bgd}tVG1(~jJmcxRSER`#ze^*NrN-L=P8ws7nfYB$H&u%hg2uNb zmNW@Bj6NMBW4}L&tQPV+3mJmD7pyq>SG-0qcDwPbohbMTZj*d2ZoWc?UG;qKD97%~ zcZgrbued}60Zf!0@P&q%Y;RE!69Z~f>OB(hJ|yj8rwmr;Kk>o2UnNBCja!4W6FzhN zU@*2at=2`IF5%OYcH7H0>VpR4g`E5*5`Y|P#?-vf%%#=_rC0vzm6a7tI`LxcHWAv5 zPM%k^ca)mCR9n0GGiF;uM=BlOSB{UG-^8FKi~TcptB(EOaI!?XA$x?rYY%O#2?N3G zkoRiXx)76r_q1f&3}db5#FcIS2}$5)*xPQZ&doV1%t4O``=A6Dw3)3;+1ePclS41< zZqkzZu|Xq=u`R@j(5M&1m2d*($=F1GpTb`gZZ^%82Q_ZbH^~9P-|r#McFtlh|8e9laI+%xmBm~IL|j7J$|P{8u9)PI~s&oyUp&HQ|$~j-|>G7L>vvY z5|8$5t4Vnd@;@b)xNPnX|1%QQv>sSRP=J6+)&8#r=c0}dZpPOC7Io79{+6}0`tJkC z8NM&aZH}fs%(I{Vt?s-Q&#nhZ?+zb{_FsTF#_3T2K#xt+2l`yFffpC*IG8j%7s z@SqAQIa&>J+A>_gkyqFkr+F`lCwA;j5Sw9Andd|ml0kaSGSic(eiM;QGSTEgARt6i zBdy<`^@Ogg+!}?DGx>_`Ly!^a9am|+5nMxUTg8I{o3F;C;Pa74CBoPBgi+Nxuu zd&hACQVJ#;3x&n*IQf$qVfDOnoTdVDOJcVZcjy+1`xr7lv?Mm&R-B;yJac9x>~J9* zL=CTL9lP>)^b8Uum$U&p?cqGm3;n=n7Mm;c6wE8KTCb7Gdr%#Z-T9_Ttx9PwNh9#f zD>gCYPWt|zXIzlJGYQ#*x#@N%?Gtz6t^P^TtOYlHa^!px2kRFzW3E8LFl_xsCN!Wu z0BUK9D-?^iIBEgnFvVc>ZHcEh*+k?9JU2Bq)aof+PvC}Tft`5{hrJ2U6`G6v?p2$O zem}0N>VzkE^8FFjHFlwUYPfqNo%QsfdeR^BUvn~e;jHB`LGT*nOsu4xJwZ$$DE-=A z!laNc>`mar%6U6vQ#zfDzl_ua1s@%FL*`*bsBj{J&WJrtVH}fMIOPd*fkX$|ttgWt zSEqCn(C;;DekV9|Pd0)m^pA!}* zL%9YcL5HfUnu<}14Ex0&{wM{Ajh2}&?C|pvHu!mXb^|zxF=!))U zNaU4P2VD>QB;h;Ms?0(oAxWdx@+cFs4n3{j5tqbe$yYs2-(v}bsoh+~{-(hz1cG@2 z+gv(XB)kuCF^-sn$P#4di5~`du<;U{)=bCwOPT(Y zPMO0OG#en;!&sgKG)yeqCNHZE+*U3n#N!Fb&2cI#(}?909I<8CedA>|4T;Bll-Uy- zlFn`(#_euYq4#L`((CuTSK9;?cdG?3S6Zd9i>>x3OqCI`#13Z3_FY>o!$X8$j57p$iqwSr0R`7^CDgc4` zQ>(ECb23YcgS1l^3jL2)@?t4erX_QYmvpPc{ge@OWZcz!nbo?lXjWT*xWB_5M%2ul z$&n6sw+I6VT^sWfgp-QrA6HwV@T~?NjHqG4rHcoA#gyu28gtjfA{0iXZ-Y z&d6#V3*B^tS=EM_7|xI@3r4Et!L6t6mAZx9xZCN7!Qp!EF=qtC=!aqAmyQOp>=J6C zZ-QYfRbjtwke&EwNr-~lNsQ?{PQS~|BQM5i!qKrU9VutX1V)8*#X3wg_im1);7XzCP zPMm4hR}A23F~Xca9)Dbe9E0zNo5N!@K|Kx!YxxthLCr;-j^Q>Y@(3{oKCR&aM<8hV z9t(`#s4SidWvAj3B{gF;{@HP?P03noeSG)ba^Wd^)Y>Fmdr--q6d+D}Uq@iUhraUq zS%c*E@>55E0KiA4VdnA_$pq*o56roTMu&NjB+J-W7f%5aVnSQNuP2jmaG;ClWoA>W)395z;1pq*XI)Lf&455pFTvq>NOl=z1gWsL%P zlvatToYt{5O_MFtv{2y3QIO;o)3t;+a+v;yYFVV>7Xk4X`Qp0#gE||^Fb6GJ$Vq3y zzy-FNBa5ea|4mRM+Jn!-;(~88N&jZJkKGK7-R)WkqL&w?$}Q4rYJ>JN^AKjW1SQ*5 z63dG}4fS4DBQ2zlutp12!*D;lVhvtNMZIc`rn8Td@IhPkNrY8g0aTH8n&xpQ^^JB6 z@BFReu6__-dPmVH`_Kb<5m3CeYgP>k`cpy4EI2R|oRNYt8Wg5yRB@JTkFt@X0duFy zP?yh?dfuESo9^5aUlDcwxu=v2&1#4)x7HoaFF0ha2tt(v-w{!1?q%_eD5V&#{Ftlo zRjtu-XmDiBf3c~SOv=LD%a=Q?rv$VA^qPxH1X8sRY^=b;M5KqK3q(1_5pWe$kf*fZ z|Gc?k=(_NBvv?skNtOQpF!xSTwsy<9X4;%-+qP|Mrfu7{XWF)H+s>S6+qN?+|Fia~ zI%~Ds)%ImweQn&082x)=M1OmaCxGQ*JhdHW%M9hG;4`X)RPidz3GBP;+HEDJVPI{x zuyxe{diyNIXPysdGwp~v~bU3cM47CbkpGy%|9~YSJ zhD)uHw)HkcNAtzH&S&HI&nR|?Q0Vp4ap z7cPe59a++e&t@_ zdl{$3t%BD%Lxx43VP(d#-dYQWH>iAdi=p1d%bsS}U~x>8O=`qPfC=Ol1*Ucxm!^i* z8@g`iHgir0S>aw0pR*xno_R0%E-AuP_ZB>^=lkYy6D6M|CMCN|t~){a7MJmTRT!?~ zd|N&|JQcEy4bWORY=DqM<#h1rDqc1T%|ivnWEGPvE2a|*{Lp9+>I&?-z%IH2&oOS{ zp5g8JfWPS^U2|_0Q3(bf>+JYB{NOb+C^HZ+lgvRm4)jDGa3|Aa{@<{=d5#~ zy|&pPRZJ7}WpK5lAXrOLdZ^YhUdfPMtvE^A?A^k96qsxrho&55J)tl=l@?Y%pSDss z28jgVZZ-cF7jm>B1#^XbfW*JHzQfRlsBC)XPf;k1Y zpHP??zTSu+AEZB7{*5obK37OmgSVp?(4OH_R2M?>4Z-qNFiv~;^ewey2g17r`!+ zsh-2F>)E>3L9N!vV9atXhL>{Gq$Rw9?rBqG<5BtN~hyFL-DCA&TVl{Mp>*cl%EHk5C3?Qz_ zf>!%e4(h@C=QWp}0i!lI`$rSm=(PEYip2D!g>q^p6y=t}3{^JkOCx*#vCCrzWJ(>z zPLruUz)Df*_Ts0?T;LP5Hm=U^aPxH2%7#6@rW!dHl9^@o1>BuxmA^^b_cBLm$G$=V zi}AiYn>X|HpLQ9A(pp9hm%u!B`u5nPD&IS{A=-f=8uSQfX?&L}hPDpjV-5fx+JWfT z$U{*rD~9i;)zRyb)rKl1P-{x^Pijfb<&`(W(>koKMd_{hDLD2c!#E4u+8@-25%8q3 zb6Y7$-5CfRm1xUZdS z$HIE(yOa|1AZc>-%k7EkCcxRO3ci_H~M}w>3XCtOzxE1}|IkJJ2HD;I&V9Dr? z=r3pvwLLv}7na-m?i$c>E;dNHXiwcUslFB|mf+CC0joS8A#AK8NVX8>6Qa&jT{+WN zd%pH+9I6Jox6S2HK8-D8ngJ+NCyLmK;8QI1UW*U6Jb6NOz|G4Jcg<;pz2~BI7gHKL zuoZ0X91x~l5Kp&!(B)l&ET==~;jCmyds#!?mCL!? zeDMOAa$8-dYtMf&xQW~te*w(@cJ2*S=UowEWmqP?8aHX(<=+@{CN*Y#oox@jZWB3| zsmUZai$kvlpMHOS> zwN~nZHSvE2wjXbJQY?i|TTn(C4w-|j8+rj5G!T_gvOQ`(4@*>1HsQ@^WS>NeHypw<2R^9yd0R7Vj zY<1TA@CuVL)y8atgsAneCQ$p*`K8{U2~C2tVwQ*{b!ONDd`%>=s72abkFen*bIfgd zjQuqlg>{}TkI>|~k1IT{t)=62Xk6jP0A>}E`u4984(oi?Lm(sknx-ezVeheqPonHg zJkIj-3uya;qMkd2n)PlphGrg7?>de*kHKS~w%5rT)<>~x9MKGTXU?i6G?=OC#o6L^ zQ`{jA^#S{e%+KM6GZ9LBU zcEyO=8hmv#d!_dNBE?Qn#=JYf`zr}Z9Z!B(NcPTDz0SQ`aJ@b{Ausq~vf;?AHGOni zKM>MT-`yEG^&_m`vbSc3wH*mjJ)70!r8zXTiYXMkE$&&vcXF>4`fuo^~I30nzAF z@b9_(F}W7|Y|iP`j~>Vgyy`7($zae}BYdW_O=|aah|6(o)vrR6+Eu1{Skv)c>bDu3ZIsL z_Oa@{x;JoN)_GhY<nAu`ET}QWq(l>Ft>D3?WLcNp8C`QLY{rzCn zf1X@f>efLj*gA<_%wE}0{Fu;lwVDcbW91+G>8!ZoDwsi6Ko@t>+wQ9s76ynD=hFCs zJ@T{+mV!Rxy2UZz`nf8I$Za>uTjQ}ep9=h1l0%O@Sk2)wk>3iVQpxdyGh(S&HLnP{dh|E9`SyeyFk}YRy2je6DFP=H>pky@YV=dL*I*s4qwEK!~^R8CP4K)v2<{wjf7*S)JkZvAMU0ygX{QvcDFsj2KP zU>)BqlfOKm64oacG*AJHKpE_4nEmtBk5`-hHdsPqOK$NXv#WueW{G-;Lz zj^Hk3ZVzDI%-dVTpP~6SoR@S=L=`@a5Waa#+j!rsxK1P~hVsf;u1RO&oO#ZhWvMnc zgq(VXd#)7JY+%|((+Yw5&$*M^AxLYw0hL-3KLZ?7h&epF1KJp= z3u+QjjT+3Sw#_YVeUIyZJQ$wIwUPFJT{%r-0xoO$!I z^%&_wY{Eb`5XAN5;s6|oY!Lc3J;pTy2o$VRw8CmX<`5|? zXnnbGl^ncL|AOiqhVg1&+==XfY9foK*L?&0 zo)Tz(=zzqZE|@3{sGq|8IJKW&&XBPGIJJG7ywGc0)wa?a*v?&>%5$<21CzyMX~^b{ zJFAiJJ{}VdXdrr%42nOrBUnQtG4D*W1)6gzt~Q?TjaA4;w=vi5_f<dX><(2V*#`JLs_*H! zZ53)a;@)AGgAH+bjV$^-fb>rb1(!9yit7!wsyj*IO;61&aUQ#P-Udlwe?9nEfnt)` zOh{VgWSfJNxv3M}g`>G!syHGk=C@wXt(*l#>A6iNBmSI$ya=$M7bZd&SE7QYjf74WSRsgrNDjLKEyN z)W`On`OwFv;YLgS?*SEDFv>7hG%Ya`>Vu1iuNXTicQ{&D(ZzH$DIF}t?h)U)F-e+A zljnblxu^foLeu?$>pFb2Xg!7lt#%_S*HDf3$RXif>Xj_nuOEexa?gY=lIc7S*@`n7 zOFOJ%lHRG`bVbiKSu-r)En$h9?c;FOwq(0?=x;)P4G z1<8TP6nwuN_5$Zpr`G7xCRU93jxU1y^Q~ZOMK8aAEni)zB6u6HV1!xxjcX5AQ@#Y@TR##8Y>W4!(g^u_^t3tkm zH3=-@u6nB&yB>@{fX0cI*VDTt0@p%8Ih?mz4n=IjurzN&U@-U=h^j06K2wT73NH??q-D;U8x84e4Za1Jsr)X848UH&D^7 zxTX8*5#VhWh*vcL9Aoqcfh-`wv1_2d&s|;}mi12i+W@~Rn4QP+2g?Irf%^UD`Po-8 z1N;NQ_f9CvSk8BqK213um-Z|QlVb+Jx0{~{hnC=P=0=9H4TgZ2u#1aB3i#3KS(orM zx0%{)M3DNiGl)89(|RW8EoreH7$Uo6 zVUs@Znj+u)KPxESEG8jDk)~9T?lor+0Gn)2itnOKmDsYQY`HT;S1&_5JQb4-5P)yk zNNI9zTsrOJN|8!N!g+7W)8_-aEg&Fr(iF^&9PCve)0B=_NCDsI()KUNu&6~hTXdOj zPN^(UrPEm!oyohZ>**wGQwGQJFV8|AsaPrno->$x+MlcPHgn&Ov5sZyC&90+BUqta zPi`unFeBge&Khipf5F-?qwpG7vWtn3jg;>LB4?#Gh<0DhW59~l?oNSu;Eu4xmRi#n z%)eq#axxZXd2r#><*Pm~W)~D^TZiuH%QN4K6S+QG7T~sN z2DSVdtkB{8I-WPWFDM-uuP7OV+9zQ;v^>ANpttKh&9dmyt|_&gJdDwJuoo4D@_5Xw zJ^l93nGVCZ3jHO~vN;61Gvg^FLK>+NAg|`X~4(AHoR2C=jf?-nzOx$=V!gB#;RB<+faeAzRga6S_POOr+2C?VOjY-DkLITWbK#5Sh!R zj-K&Stz!s<0#Vy^oLXdod|%YF9SDh@^eA|vGQl=SAVFR*ubNRTPyf}^v0C*G_?}ao z7$917>gWSj)DQTPd2k0@`YYRZbGGrs_v&UsWMIMHhkmD>yRvtx<&tzep_cV;-o;op zzp`_$Uie(O(RX1vQ6z6v=XFf{r|EJFK{y=Pcg!P|294Su8nof_#2uXmkORmzDxeB0 zM{PQ^*qP;J8>u-1V9nvmCdrcw*wpExP*-##C021Btlug z*?VQoPF!5{0EU-F`RkhLTz>iGO>{|B94&` z%vt%Yh7}4Y&YSXPTI=w0Dv%;tW=gP27-^9`!pA$3a5n14Qd?N%<@R4yzK_{=Os=rY zx<~qDIVgtFl4Fy6zzjj>$YJIRk6JIln(46jTT8St^lIs_cmHt-0ES6hHsELB1%F-w z#{c;X@PB}Tt6~J8fEZvzo&#K8=&`g{xWcIV)K5VRr`Y4x5}6~mwp_q?`i!3O==cf; zY{t}UII)R+6KxCh_ghX@E08fN9j5YC4JF{-(HSH_C4(PC`kD@rJSBsHvMFNwMFo93 zKWYckPVg?QcbGZU4`sJ<2}1-Lm50CAM*W)gy$zBlK&>6 zpC%_K$N!UVTouCr_X8^xah1I-qNR`J!Y3u>>;skGQ!GFbi;moRQmfhld3?&rw*G{B zXEB}MC4VgN!h>CJ6`D~f8q=F@irD&71cL1_OwB^h@)xaWr^7V*HztWRN!&jnw13(4 z2LG=R7`c=W1?K(5IJR<=LGLECVb`S~Mr#K+A>t--td+_0&F;)PlhlqRtV85x^AnQEX|_sAz1F zI}?X2lN3Z$f(-_{itD;9pWP{1t%imO#M}e;HzqsuM{cYtiNP8J&r066Mx{2tt9T0l zd9JO19=y)#?7QB~=xkMkIKYDYgZQR!e&MOz_PzZEUm|Nj&ALA^(h2*)|JSMt!d_69AsO9}XE7uOGUK@6avtt04q_OxqC%gXe#PG(7`9cF zk4L25dJRO@%T$Xli`{yPZI$-yFz;_m#3TDXEcmI{-c z4jtW+n+gr+9ZZAj0b^0u+Ai^6uM2;;rk<@b@5^WNHD;0c`=Z46H~P&vJsvg1Je>1` z$6D`ctyywY;e{=fbejO73&!fHZUL)RTrBYsLyC0%t;8=L>X6o8J2%p zcHgiWsaz6B7MB4@h!&WXB|$njy~hD?vU;jER@Ag1WEWMO0A|qC*(I@Qtq?cgfmm)9xZ?wl!Y622zhAyXr?m{478Z_Lge$CAgXT4qG*QN^zbIfRT5RTg zuVW*u#G^10DTS-h^NCb4w*^(BicjE|d7bLKz`d#o8&#v8#E7pDcZh;Kwuj4D?- zc}tdJoyN@oRh?l8MuGA~)(k0Lt}K;vo7DtMq=@afg4o#L2kQJ-_U!k0IpjG98;yAN zcK!a?*Rl+XJL3%@cG`L@oe+*vAyO`kDyEt_qD%a_Mru9FY!f?w>s9s6C8)7!N@y2^ zj^gGg=pnMe*Daiq-Gzngk zirqdlOP~VQtXi>hJn_5{xU{-kVmrJR0*{eclZ=7kScXh@3}TQ&7(*x|cl5cR0|_{s zsnnP7XUhU-J4G)PJDQ0g!`1tp>PsH|u5ZSul*O7I{hq*QcVZBS9u&3QcKgD*L*>cj zYGfCgz*I81t|9ftUg z#SF|GBWrC#{TNFb4YJ{N6WJ8zbMoHlkk>E+$$L zk8~Gv9u|`J%$9ZY{PwUD+m8Iz5_>F3m2zxfb7!tz*%E25j&__GNz`|@WM;IF1Ly`I z#^w#_CCo~IU*?v%J=Q*eCLFT2y1B!W8e=n)cj{K$bc=*%H%N`alr{DS+RPM$#~4B< zWKYICKtZkF?O5HkdT314R9U<0+yqET}{~Ca|D25#}uG!2|9J2GZn`PoTTJ1tsB~68kxURLtO_X^E z?2IncGf_YJT9upGutb|3S~y>9q4l<69m|ZiiMsDa+vAlL=|jI)@=P!Bg~u$d)!zL@ z=50=XiIhQRDmjd+6^+9I`|};{HWcUB{=MqUMu0sP`cd1{k5Bp!SM&dEn#F&p&C%A` z!O&RuM|4gOwpRZ<{~z(`{%f$oPtnn(tZh3_kL-P2)4GP6EMCUq1}Fkkb~ejw?Z>>s zQ8;aFnY9ffB)jY_f#egmcd+3({dx09V5bb(#(sL?_2J_Lv9GFmU{$`> zgc?~C98_|nZddguUGuG4vq*|6(TKjx@&+Fm6ZQ;6<&PB|sF?ZB6bzNpK3YWH9N(Vz z$sH_%p1Ky)podhI3f)l}vm)xrBvLtNe#C$$89f{v4|kM)Ad+}w!t^Sl@G)hmgz0iB zW>$b1AbIl=n|A6$AXw%@Ot)3{Yc2E3QqxvyRY7xdV^Fq${tSYJh>x(g<+B|= zst&ZM+NB1zMVwRi<%Nso*~4MI%rA|%g&HuP_l;=xP2hY#7|qcVO(~qZ8LQgeXm7QN!R3je*M_B<4y5S zOp!WdI#PCMYt&E4^Zuj0vaCw|=g}CQ7^P_$U3i2r_cQqiDwVaRL0GYpHGE%xj5BOZ zT2ZT@OA(aewj~Num|j4SOaAykat?!y_qsjpe>hQ@ze3qf?s^##5)(Zkc8-Cs``O{ zxN76I>V3;)FQR$``2pX5DNo$*?RceJ_6_*&N_D_y62JOU=;cp=^55-l{#~gq#t#2X zqxlhIRjh!;06l`pGw%@D7I9=4Kij4eq9CL{DPV~cos^;G&|GR<=2AA z*0&Piz0DLFrR?8aER z)AJ_Aoj;#&!MGFP^1dL~BWuOa+d_8w}nT)y(N?#8$*Q z30ZYyM?&Hj`wuz@vw>r13p#q-04!;I)=!gwdxHo39vd}mv(Ng+_GT!W^Nr=A*Wem$ z%9E}aK@6A{@w9X}nwUUHE{&DUi_|&G`b8$E)LNeDBY3W{vy56QqqO-LQAVOct_GW_ z%*(Yr%&gk86Hyh++WQ;K7qsa)0H5U$v8^VK?KG%}8LZ>{7?0~?FzrnW=wU31RxcaX zsjSgl-4{q<_93G>LJ_K%ofg|MDJoByHd*=I!WDy(OzYESw2EPELytDmEI`!He66Us zCH*m`1uOb63>ixdelxM8((;WH@vC9-ghUi9Ds7c5aVVzVc&cFTVFL2cAcBpO(tbN* z9hj_;!d??TMH8q$>w@0Xi4}VKdp&zPG+LmZFPY^`gP&dUWUSE zri~Z3oWE3+aE4$VsRd@lQ>iTuSB4Xu!CY~V;OBwYuxdJft^;Y68joqumao>veS+J9hD;kuri0-Wl6lWHEM7EKrddr%piixVY^ah-#XNcJn-nm3;hHGJhb`|fU;{$Jv6ZE2Ur5h~3u{1z$yW=kQ*XT3X>woaHJHI?$SAMuR zuXg{_O8b8+<6qBztOe~&$Bkx0-(_8YdlL_b_yC^31_>wdi5RzoF7cA%ymv z%-r!L0;{Sq?=7xqx*+_12d74C)|n|M;(B!}F?gtvpr4=;6Ms;ieXUt;R9Q59?7H{kFL{&c@IJy8gs_ zYRKz>g0-(}QfGI&%T8&}^mu9P=2)FMh`GRqCDI9$Q;)6o3~oTK``>W1 z0ns{?V0fqeh00)VpOTucu%)6@el*q&z4H!o z1pNRN?-%m5ns0RgK=l;6 zkjpnU+z!Zk60reQg?Tinq-bG{hUKzN>4;`D>b%MfgH8f30e3LxcE<`V*kH+VXC-Um z(TwfeNgLVtPE$5X3yn@OV;(}&4J6R38!xAqa7~4&*R;z5$N~lQE&H{&27IegT4Kl(PhW{GaRZ7wUxj&m9(Z5O zdV3Mx(V8kaH;3#c*-xjfYk!11KD)OUs8V*B07#l2%1YwRW3ZcNwT?!kniB2TN6Gfb z5<#i371CS`*oqx8(g~Fj4nb~E(j2UMje6gUhxgj77ZX{}*Q1JPx*q0=0Aqjrq*ybTa$LeoEqZ}eNM6&89o*^VASRe& znLT%`#Pp*1al@E+LR&PGZHivhgw~4pX=>BaYhA%v{7BLF8vT+6?1+42O+X7!`lf!X zRPJ)?4J}82TR{wDvZ)1Yrr(v=OGJRXH&PQA+^g2vCaM&cnE=`h*GmYMP2}6ok;vF(JcB-yMQUTf?|)Q(MOHz;lgWiqwFtd__z3E|jKPTz8JsQeHZ?-%+*~sdY;5 z-JpE)S?QLg*^ql-jB-Be>TeWDT_@*!ckJ2G6i!h4H8~z9k9I`BbOmHlb1y0BH^J5# zH6v5F+q8WH>#!t8$h6}m?x+(3%W8Z)pY2;K1?u_sYh9-Lta zk0v!@4Drhqxz}LmnWwJ8Y8JlwFT(cBV6h=d6O*uv?+P}6=Lw5|zU|sZz;w*0#Ri=A z32XFym2fLo4Y7dtnD~5vg|t+J&n&gPq%YEx4$z_-mDAH#eZ1>j8%0^+EVt79gZV0k zvq2Ym6N@HDhmr5BrW=)m3kCfKAqq*ed5YmHWE*^v- zv`G!wc0gJHBxMkjiw<_W!Gs*Qb|Wr$H0tphl@^;2^A~JOp<=BglST8ako6Xbrzi|+OU zqt^%y5ZYVs;WAS`n>HsQSQxcAE~{?ewSF7r?!C(hElO`#|1foR{$A}V$kwFX{Ny7s zEp##vw1kxnn}HUrMvdU^DA&JmqN9uzf#uOzTofa&* z?fLh9G2sBU5%nRT-^vkr2j0H4(8Gkja=|M3doOSprV3XQy@(FGXKtUlOnMwKv%Ura z=|0l$wWUSK@=9R2;DgA*iIM1g=BjE!9ToyKiW z{LAY)?tYID0?=RkS0Hdr>(Me1&a@9Zuw~UWOKx-iJr>7Qp{Xkl=Y<^u0Z=1|xZ{rj zwhRj7AxgYo(#wsWK6tI=K#N9Gq=;GEggYW}vwL=2Q;lGaw*A9$wSHEJo6Hf_be;J9 z-9WIUiCZ!#&iC$yWljQa?pn>fz*ONmtA-i7^Q2MdV~j<%ehBmf8`@%f@4EFo}LjyS=)G43EtR8{&%SA-j*Yxs?Bn+xkw= z--Bl=gK?_yoX%Q^1S4|7M74mCSPi9V0JKy?^Na7LZrlnT4LCv7+GdqrPgt)(4gpK2 z%q1c0Z4{G~;+RczHgL<&(Ga+j;Zt0!&qu}PY%X9}v1h9`aHV&w8`|_szdq7g0LUKX zJ`9#$JGM4Z1D6JEE?{#&8YY^2XM|5$Ar{#THzZ$2Mg*aT6yd^f2LBY`Hz>IM>~O&e zp@NzylzdUbHjy{Vtv!+M6z{<0R-xwX+@)?1Nv^Lg6=1%3Dl?ET7| zNUCaX#ispbujsRRQnM0L{LZ}0lESqS>LvRMTk;X%0Fqs;( zr6y{t$Y`sm{Fjc>z$@ZViSuYd3*cbV0~}xW zj8Psty{>pXp$o_nzkR5~E=?0}Emiu^e^oK3-S;Q9lEHE7<9z;=|b_2WHisRtbWZ}3m9VmykCT?Ea4lvTV9^hww<@%~C zI&5-C0zi3|(i&^gx;Va$k|GJR;XTN$k(G4j-3cTO#@^P+40w7SY=kR)vW1loizeWT zEeHfvhM_U-F@^YRoS}j?rZZBDnt+D<>iz1FRhIl#N{f8f5F!Wk+l_6GnaH{z{RrE@ z7j^HShx=rXVYLQi@4-B(+XC1eL2Tkn(gR(OR@#;eER@R*7}YWiuC_zARGn6E6@q0M zsm6=gtEEF9^V<6fE|gMsFz0*E*se@}d;H}nDxcGP;7zYtW^PK-X2B<{y6SPsj{K6O zzVst3ZX|Wm7#SE8jZONV zpbntDmS`I+5{R#oSX+$|?*kE`&a&uU2U0J$f($WA&h5#NL$f)_VL_6wW)B-uTQ@TF!3U#WqWX37yrO&s{dT)n?TUVEwll=}DMz41m=7w;`l#4yDEVh^*UPl&zMC zW`T7`Bp5sEh&=IFgvyWlh?n;tr?JH1$Usx_Eh`0lN8 zbM&{bR(P<1!fc}9#MNSQsLjytgHE57jD2UsXaK>PpvE;mjRN9qa+ORcZRj)`31I4| zCkGJ4s=IDLT>87zh*y={>@YV-pKW+ffpF(6suV?P%_?l({JZ@^Oe!ZRexF8f-{T8C zdvK9W?=3d@+$XAl#HU(N;RgB;ZW(T?hO;YNnhRrly|m$F@oO5KTBReSuPcxWMnk`L zflq37hB%j$#V39R`|*Q{bYA+Z-c_~C$9eO|F$PO217<-Gfn(6?ipmeHts}`i%$AFU z{zeOpNW&h;M+`;#L)jl%e;S|Bd>PN=$SQpn=UZPLG+P0LNc1L<=5>-l4}+`!9WZ;4 zmCVRvn_^^w4nQ~Hjc@d~0m>~rUUo(uS08K5K|5yFTz7)^4?9D9O_^$cMYPriqrRG{ zA{0RoyD~#Ih1$5_v^1@9&}=vfl`=<@7Ir);L}TxWmW&PVzdRWiv?BLQ6!%rRYogsr z5$P()`iN9(Uint;qWR9B-=kEes~8Wssb4Q!XVr|lHgWuT&KFVUCs!$(SAE)!>g)1; zU>mQ{beA2vq`_5z^p0Jin6zWcchQ$?IE*hHd|%lIhni}5N?KwGool2b9R(~>BE?t- zQs*YJ&F_*}W%T;!eo1Myk6(bgq`n=Yo_QRD#bq&DOMiu7DI=+4e1Fx-7p-J;Q4pfR zm0Y8XP_-Hk47Hx=b~BNo8*U%JQQB@#2?}AkdQ3XQXJtLh@%}*l8`R_O4dkjjuE|dm zqHUwnQiB_7U1KP>u@losFY)UBY>m5LZUY0Ur@L5m!EVz+`Cv|MyTl<*uN3iedgq)iz}TdZmH*rOGnT5(Gu1c6DQW2fGO# zpE#-ME_BAF%yn>;FtVcV&CYSzJf*W6M2ymohNQos8=rppR+8_fS=nMdfc4Q$3}TM` zXU7)6$@gs_0M$)41F-^eE^3n7t-rQdKu1SrY-&mhD3E1yd8`(MGB~)X?;4+1uWNnT zZCBwnp*0J5X>qnhB zu8$H_j9!WBQAkC0wW}d=gf#0Y&4GaIC*xn7^`9G`ybe2Iwu7NFN*@UEojuP$3G?Mk zhccQc^(h&sYqK!xW*hL{xEzC*L^yA2gom4N(B91LD5`9qGwM`vS`YdxcLK*xh@1~S z4CmH{U+>RZ0kw=vWi{h>6feSOTd$J|+eunTQeXyDK3p zMQ4LiM2XHY?;gS@vPSHJju*5^Xq8d&9m!E4<8r*8%J^(I7-@Y`zMAE}PpJ2|1Vjl0 z2*Uh+88>qJ2*=c^za z7cnZY&>OYhA+;<%AkT z9KC1-C;9vIp8gdov0;do1{WV9Yf(ct}k^^N^IYK7)NZ*Fcca;X- zEh8~2IvzU z?JB=kL$0XKb~G4@Kjf4-GD)-;h5%&U>nk$lvQI624<0(!Bae3hV*}o=r$M?!9a1+s z;Js6?V^$38Ur*qfAJh{e(-FS+qIqg7-WT^D5cLhimi0H)tcn##9w9l)({J=$3`GmD zM3#4oS>;~Tq;wrZn{x)E=}Pxq+Oij%5JSD*TKZbKiA8(9S9r%0s@6X<*R=z`A>`%v zmiQ)|Gd@$}_NO*E@%GH?)u@*u zhxUZFYa1tpZ{4~R;3N66y#`QoF8TOf)K%U!xNV5JWOVB-?0RXo(H_yk|B0B#xi0RnjgL1ZSdlF}Ndydh%)O(F!l>zoF`x!?qXxqf^ z#j#msT&SmCAV2-sg%@wxw((>NOVkvMPddFfR~!>ky+pXkQ55+|u(@Sz=RU9QX})*z zDVojJenk$DS|}2hbRHes7FnSi!mt6`EQQezQSG$#+KI$4UR7gD8Cl=s9Th2;|UL27~?vM z16LQBV0#&9k>c2-Hv4pcp&xt0IY?Mcj}vY;lvld6q0lkvnyc}U^&`fJr~{9_!_pz= zhoG6EPy<4KKEt^eaH)lt6oS2~GcLi#_NiXTN-QFlNUEC`X>*5-WGT1?hioRw5c$bJ zB_80+G??hH(z{L&ZPEu*yEiT}i{2ZDq3xNcu}wcf9CoVmr7xN!AI!!j%HqK8O@X*_ zp3ZUm8DVmRN2Q?Da)DBS7*h|3JmpL3tRLjD$eE{}0<*puC&=g}KgbE$bOio8LfUa) zEjyp-cl>IEcjhAZ*lfVge)ZO^oTFt0aPmgXC0cY?Cufn!C;~RGN?)vLF;XyTJ}56~ zW%utUwBo-5B5D_Xd=LX;$Z%mJQj)7@iS{xcUp>+g+ z;W_4?`etxrE=aK|1dj%_Ljj7O8ulJisgd;@Pr+uWzG^BKk zuQ-fCU@=#i6>~STQ;Wqsf;6!EE?NUonbh6g(Q%vp#*lNxfP3?r*a?(T5v+lz-ZHvw zEF<^9PO>p>6;|XKv66vTIoL-kVxW%TZLDOFb&B{m*PNYpKFlN_4V|5L#aDSFRxA zVjH5YY*H%gjN_(~7P)UTNBF!AZ9d9kRm5y&CB1Y4*3litxW^(j7tyHc?^2AG@LztN zH+52FqtgVN=m;I6lw#u%fn}_L(j7dS;O5 zS*x7Ii9aXaUU~eOMB-xO%Ay66HmRDu4+kv0a%;lCAa!+$D9hsQTA`69)`LeC!O^M9 zJGV%73&ukagkZ%ZAQO%FY`KF|XBQWU500)eQdLYWB-s>nb)_;n&|PX}XwFI5o5zQr ztE_z@6RY#rDLq_)D(m%X#7Sy?cyZ40<0frW-E+v)Rx!}2P-qg_bmUjV_X|1Y9n$dE1o48kH zxEp8hzmGSO%n4IM1ZQo6#i$lMb*Jez7Y;+a9A%%x&U0wDv<>&Z2eQ>kzRwc7Ig4s! zPrtjB2$Q#OrFjLfTS8l4DfxPV+WLaouF{ZqL?J9N&)M}%^3>g9)+2N`BUY~{l#J!5 zo`ygA5jw66=CC;7-YQ({2kOtoE6gnuz*#utPc3(`rJT-$Y=r0UFnq`~;!^D*-fE35 zB-_1U(3QdS&cJTDp^W0x>CA;~puJuLu@J_z)In|Qlsn$Fn6YGk5$%ow&5?1bSGz76 zW*O}ug02i+-YZX?`f2ilPr54<=X=<{090(L61kOJgl5&2!;cvs&52FbWB==-*+?_TFO4@elMo`P_$TV`t7KNz1me8Hn=R2 zNI2^&@?VP1I^lwcn*u=p7iI4pCE5063#V0Lf zZ%GoSSiIyA<(w^&mn0k#2(>@01n-gv7TR_c11M}&om|6LZ@G$SW`P+N*ny8jOVU+o zXqhwMezm!Z2&1OjqcF#gWvf)de?Bey!4aZN1p{Rdm#E<*tD!X>spY&bfdxj(QZCs>bM+!T zKg>KVSUAdS56%^40}aPBxC!#~&7UrBV;_+9k%NCsAm>pwcHihK#n9nuqnNgXx_iRkDImc6Nx8TtyVX`vwO{Wh_~vGbsQ=flcku!$#a|&;RHY>b4WuvarDK1 z{%(hKl;1yo^3UZy2?FclbF*ad`$p`R(pF5*hHq{#u`;i{{4%EuJ|LIBql3r~;$^AN z_x{blO&;{Q-F_({%f_nQTqjPi1hV^DC{yqB)!%+NmF!3S?Y0yvCfOs4XBKawSv#IAhFPq#xjSZQIQD|0?Yh{@Bp~)xnYOO-JLB z9sJ@x8EAUq*!3@dV{JYXY;GBPNt45qqrhmVqm8%A99+X=mjF@<7R>sAx)&F0xY{Qf z)+cFN^qHGC+uQo0mZF82z1pxU&*ypll|y0YlFz}F(%hBwgwUhU!Sze zY2eq5S=ud9U-x5ugEA~1X{G!`PrFvs&DJqUdaY87;f))ZWW_q)Mb?Xf`kMgcwVH@; z&M^^`R`;99@$L4B`NWG)kbr_Pz>o`C8w+hMW6NSaf0A!1WAav-dQaX}@?U8! zaoFU+;#+Cu_YqkQjE(6okkdPZGA*>BmSd$iTC+lcOE(pm?e$2>Ryr8^X|&g) zT#EHI(8I;`Tg?|oV^OJ!fF#a7?=ziAY!I}jMv|77ezW^6X= zeJrdNHI`EmuWVXucXTXRNoWtL4aA_-2nG5HlF3H4L|eHqxeS5HZQ(T|+#85rU4z>! zkx`={lE!a4y+7lPMB+c#9Bx^ z?s}J(hSN<{FrmHAaa-pZINMgticjZNzfR4i=L_vs0Uq(Ltr|MKTciuDu;lCTm|vv7 z9$SKfC|s1p$V?{r_1@|)w$vBy+uPgtel2{$u^@}xOmE~+3Rr?3?HKK8hZhGOeg5|D z1};C4vx>7S*Iq@UFXUNnJ4Yq~Z)At2NEwi2(bBVBA@aQNM|5nVv~vHw30XOs)l$}0 z%O*~#64?Oi1p3UT0veFTm?|%w@q2;ZPF&2wkYoaDbzv1H=B-H5fA$+DHD&99t+rm% z9-R!oY+}Lt7Iuxg{s1PzrF1Lwl6V{ZFV$j`R<8SEkV!$1bxWioEnfx0SOSl^xEu=- z6}Gse-^VM+LFEuPxkidk^B9&MMnW<`hoCbR=v$4kEsW=>DvvL`2xpRV4@&I}lBAF;K~<6Yyf-%M(&GP&>_69*5k`n^>` zb-dYWTole%W#0gVVd5{drD5q>u)Z7S%*zT{8VfrRQ^!&H%{%xM+l7HFN_Ovu%J-|B zzG3%~YjQT)AIZBJeOynnSN-r-{hs4-E&-Q+qJTYfXGK?SMm&r9N@g21^Le|E0v6a7 zoZx5sVTyR(Cw;cxB$|}KSZrmyicQEi_z|O`CQ0Va+dB}Vjm?y6dA{=ng(xZ3YkVmt zTtz$-Ss4Y|nhULy3qpT_MPDL*wQYmj%$ZDp8eoh>36q1!%N$3vQnhz_Pv+DYc{jty z=oK>7vObILqh>X~5imI7coN^qG6(`6bo8hjbnd*kSRX{cNrk7+WLGVFW={oduqmbJ zkSG#9h2=l@*E<*dF;N4vVMee;YyP-5;8eHX^GwXTq~7RF!81DOCcQ|WZ!RE74<=#v zirvLkc(vK3nQ%ytXT5R>=2LcT2gS-FihOvhEiN!zATXcdASl}wGd%ad$ZSi(;s3B! zcUW~U=ZpDy75!WPImD)>?&WRkAiOQHR!m1By79Q?)_pCyyRcj{dv6RKKFAZ%Lz57y zewgFudrCcIJ~*BhiBbir4PLL6;Yu@!3`Oh?)KsmffnW`Lz=aqnJig7#?Q(|MHVbpW z+BLblD{A%hP-ucor~nc$Jhkui{bnG3kFUV-GL)COekeVbsL4fFxfsksHY;R|>eJ*r z-wMa<=*krlzs@xI^Q4Wf3TH3;ndwCH5L(=n=smsZ57%{1nOJbXRrQOC!0C-Lcm4)O z_*lYK{nTbwp7ULFwkF4Bf>cDv>c=S!TWxcoS2Sw}oLY|2r}H&!Wo_!r3(OzeRbU$? zbw5|;e~m2IN&uwN)60$a+aZ-L{Shmq13L4ky-6ZYe{Xs3Gixk-8_s4wR}feSx-89u zGWZjgd`mpopr5@YwAV!)o^y7VM{>?techml%jB$^7-g#M3k$u>@ctBZUp4mX6Elr) zCX+OY0HfOu7uT*+{FrebEq}xnr-!^xR4jSdG;|1M=o>_IUe4z-f5?6KI$~(ik@g~u zdx83-N8l|ITpfr!hGPB;Wjn*Du5yIN@$ry^=i}t^*0=+7K;X;i$3AdM|MO;7^THKh41~mztAQ5F$-8oka);6C)7!Gva zSi|2$M+m{$B61@oM|54FRjVwNZTIqlpJaP)rp1$Z~{6&ns zaXwrk3Q_RX)VWYaJ2&tmXZZ?6=_VA}-S%~n^5@D6fsEV9Lkw9@_o(OtU?WY_U;k2V z1chqgLDcFf&HIM$Vp`d%QXQwpQmc1QQ^3_SN9-iq&}?R zq(m26!unwP8iX={umcO9a@%|K_2xnlWOBSgXWiIv^Fo%eSg)}Xt1 zd|<5>59uOx=Ew)Iswna!ovGd8Kd}aTR&aN9K}h@lGbEKv5oAweW(@=^W>z6!9U?7V z@3-gCI|%k)eYg;Y!ZNa#JQffR(L}DK62_Qh$hW%AD*5_3$wX*o3MCS`PM!kxUOfKp z{g)VguKw9=0^)S-gNmyle99KEnry&2Jb6t{;nI!l(E_V*1d$EN#0heCG6bBb7*lTk zMC-{h@;vX96l#J8#szrGWk^xI%FNCoxGH)WqOtUePj*K*b1imBqlpF%O01czL^gO? z@fu(b5P^a73jFJV(poQ1R{dlO6Gn3(Or^^3nAceA7uXu>c8WOdIFsA<4Skg> z+Z`6(N?12dLrX`gV(ll@A{)Q{B%!8)5gb;jtZwV?F8g$CiM0c@oVl5#2*+Vra!L*n zjC;YDhQlx%T0P8ciiGlkZY@acVeY33MX-cD)pUdMTl-hI1GNz~v)^`lSKav13EGw} zp9?UItZDn}wo$D3bxS{0!(Q9>-^M3YNS|qF`17=0i<9Y+JS*I;p}qb(ga-SpXJvqW zL;ZVFdM-kT&N(2FKMWiQNcDe|l>Yxvee=`U#@X8OuNoX>S-_eE)WNC2Jr0Hp9ifc$ z7ML>)$ku>zB|TV>+G<>cug0Q&yhXaGNl9&B2$j&MO>@|8Rj@){NbjANN0&!T58~1D zpCi{;Zao^ElJpBP=v%VBY||uhHfEvWUa8hJVHhX?_4zsV0a(eK#qwd*g^r|1w@Lb3 z?ifSVYQ!_}d`uYWmm2nc^Gpb@u&Lz(jFb|Ue{B_%4)V^?AX_+5DjUt@`U>0EYW1}l zcK7#`UTaa`v(~ipQ>z*Ji9O_~V9qvD;D~*@E|D8TrBv&SkKMyZs=&lk5{>roV-eJY zD!wU?LGLaZ;qa@n>|rHl?~hs$RWK_T`i;s~$u?%bUN7T&tfdWBBwuUcu7O4ePlwwOoCFJad(+~|HR=_#zl3iGasEH8FIzj~f2*JTd*MSYlO>>VB-r&UJP=MY=VqNu zn6VHI2S&14F^D+Z8ePLw&H?A^E$d>apdc7wmiX*`!&CkU8*8kRVcD8i^=TANl|zd- zNnyU0t{e)sKB$sS#y_0W+2)sX!@klACnPAE9~0p>Ci%jrRh)ZZM>FqexLTnV{?4p% zs&nLGVP4T1A3dBRL{{V!N+Cg7DVu_z-;R83znuBC<%|nTz zxA&k>o=)nd-cUdn+CcNy1UHZzZM}P>zR@w6W%m~+9^oV5g+4923mRcjyxlkT z%fl4mD594b2af%Yk*V5()oR*156pw;{@%NED@}Y3$&Y(;4*9{{YkG6`XbB;&Q1lhL z*6-v#n?hNbsRXlho#a>X(k(Q3CHCBQn;x;=N%1eF1PZ)S2^G*lKv(KOKw|&nnK1r$ zA~MIH|1m*-{gtNuWxLjd^0na$zVoi-CZlIo zfm%TIQg~z_^h+WnaqOsKuVXBE0Cm;?0arM2UKlB|472@|5aMCi4sZk+tRzOL3{s=X z{<2!}i(yQMam8~EJ^D^BM z)iiRwVt=Bijtm3PQ!xidEHLE6xQ-TOJvqj_cRphi)B{e9oJEQ|V1G+aJa**Rf|dt} zwCib75{FD91&Lj-<{#8JGzf=;MB-XvYI=R20uqT#&15@& zfJe}wdNXsRX7!@vO9v1GKzU5qQj3*m;70_r?lL&!#ACo911evCLOg;l>b#Qb-lET( zWv6Q3OC}A{_SY~&hHyqbZ^=xIA|qLjW0UY0CQ=cdrR_)mF@Mhpcq?50ZVKY6FDKV~ z6&?K(`OU8ctdqGP)TMV(^X zh3WqGgI$$1O9%lrxi^$Z3uWtwNv-R960N3OrApC~my+q~{Fp^gHY;q0UC~i`Dq-*X zf>=bVzz>H~WG)cD33N3wrwK?W+45>CQ`Nw>lXQoNp>MExlLoGT%tZA6G$Y-4Y`+RO zJWUT?QL=V@b@%x+a1qosg(WvAB1GZmpibQJ@2F$$oReG1#!a&3MZ3Cw&fz!|5kYk) zLv&88Gr>>{Yb%*c5evw##V4j1HWI_a0~rP#liBX&`hf;g*a`l9(dw4j&dgSa|MkxW8T)}?CuT)2JrdSb;5vlxmg?8rrQXWkyg2jo0wA^v2 zs`N&jiWTfmoq9zDLlKE^CY{PCQ=){hK4~ao|afWO&lVP3qr(hC( ztZ+o2eE~89v!&Hj*0H3>Vk2BgB{BnwsOcc!(*ad!P*HJiWbVI$B}#v%|LhBBq0c3- zKQf|Uaro>i$&?sGAbSEsNdQW{6<6Arj8|S(v->?}a8~>@C6+=qF`5K_BVn@`pk8ZW z^Me?`AlQja%y>sry@67FZ;ae6@|Iv<}6t@_WcMr8#yt#@60mM%OhryP4 z@)ja^M6IzjF+p~mtO9G0u>rOURk6(Tig)@+T$0b4#@$mCS5zjpOMDrPj|!(++pw$t z=)4&yIwM_*q~) zS;^H5A}Iqxzr4R^$!Om+lCIGWjn5(B=AITvt~r!6m3*6Z1BhRWw=7TsyU_A18y-&G zwnlXs7O4c9%9_x*Es3@3Si}aY4FdV}>{rP=X{v#T4$E%*F{5#83KRY%T*tXyW^jJ= zIxELwJmvH;BinqzReAi~UVV)@cL?_ZORH7MiBOCK|Kp(N!7rm2a$F_#o=x3khREUZ ze0y8R`O2A3fLrO=t^6t*LlJK^QR?DH`>I?g0wQ#Y@?y!5sq}M;zC12EbE8_KyHDLo z@D-MCxh_t-RLcZS(R`sE>UeLC*k0YdVdE}Detwm04&s`tG+gUft!A(Gm6`=C)>au? z)ss?ehGQiB+qVdiowg3wI?tB|{`yjysvf7eL3dE5D9I-eC!GLJnj!+=-c}mTyEs7w z>^}M%iNi?)#6R~rvWS1Y$N+ce`ya#k!uY>&Ju~u$xjg>5oiCE0TA#Utj0{-kf>?Rm zz@Xo63m_u29Q1Jb!L6*^y!85qx^?a$c4Aqnt&k;$50?`#9Nz>GFg$Bs2jDZfmjtaq z>1`Ez!&JLsMS1M!i}PpU-Re8|BKq8Ki~sN2+rTGTOwxhZ&W|zB&V7CAEKkqHz%sp! z?(^9_uIyh&3*i~IKy$#5hA|)~>*!rC8Z>gp@PfEz)tGaW<$;GL#ut)RLBq_ORj-5H}tnGYQBSGZ3ADmhix3 zozbeBAUL}-Oh6oUEO9e_a#)7mo!S4{6?#3gh=KKgS|hiu{ouWD&^gCnJEYrO){%QY z!MFCgIJUYgZ|}Fx_gIm`WPrl4dc(qnML&8EDKGu)+&JrYVOEGMY4eHzted?O(LC}| z;Im#fyBNyhkU9OQmuLE>j&@*N0xH3O#%|D*@JoinHY{hL)FJ7U1=b zApX~yNq-MG{KvtM;qQmQ%Kwjv6X4$M2bTEdWom08*#nWtuH{Htwp`{i!g+{B7syJ` zx3?!$qMg`Yod?RxU)u$YYCe$w`QJ26&6_?Wud*tk5&_9mUq)r?x6ulIx`>S^mh2Da$>W4YS za|9~doXC^Q@E9F6P&WYR!2%^(PnAuzv zAnPAacrDhAy24hd+#Mw-%=>N7LLRN7-39pt+`tG|A^d)5nA}%Odn?f>&aVv-*ai$;h zaSj1;6)BkJ$%E!fy}FGG}>#`~MLDUO}hSz+jKhjL0r$3M9%G}=orF?^9X5kyHXVOM{0 z_bm^YPAXu_I!Lom8*yW6OK9_0k-6Qr)B_Q zpC1bc>!j{{e=JPGLSGXS&t6kR=!@2F7u{;`8Es10;0-NY)6I#bQSOv51cU6R0 zUB|m3P;8j#^l5wAgV6@-De_B$GO zM!RhcF*9u5rEKtEf7q=_QZU>V zedK%>kG~bBDcx!FTsG%!+|2L6oudw(JHU)vIh3ai`&yFRy)9!n3wXg(6u=dS(ThV& zs@rS1sadlk|f!0jc*-oBKBs%)1#nPbxIqk zXRuqQa$Ea`iEXW%$hB`6-8esf|NAbTyo-pn3!ucAmIngT_#b=i|F;<3zdG`GmbPo0 zx9z^*u|74OzY`nOw=uff1b(lb(Y?aVt%r*KCq`tp>9arb1p!)igyXh&3S7#&I3bp|I~y>* zfFgDS|FO7&e6oDph8;p(WQ>9y>`{MUc)K{{jEMSY997L9zvE$LS%7p^dZwp!Gy6~{ zKiVPLf8otBV)w!Nh7$#tIbDIyo4Nk%^<#L zN6Qwd`+uUZtjiu3u3wyZ1#NS4a;}(TXXQ}5Ma3ZQ4~9h%fVYtmv)8q;%7nOuw_yOHlgQ@udZ^1i}{z$nUT;#71WX}=noe}83UXRDsU z9i%O|yFdZp}=g%ii2B8#F% z*Bk2WiOFkPm3fq`Z$?r&EP?b% zu=0EGr5CBGuBa2xkrV~=1BKVoT5!QJCny%7eki%A>KY^&`n$6QP07u|>Cbprj;CXx zjj;`Gm#tbF4xpYuMrU_cAzf1iKu!jQhy=<_x+i8ccL+}V)E0#tCpgH_vsA!G0GLt| zscb}|hdw4EK|LFCc^?OiBlIPtnk*h*DmJkjb?QSD=tQ7~-8=eD!ZZwHdITCBt~Cj? z1JCygf`&{fx4;HQM5zbb~ywhluB|0uK?{-W=e#}<;;Hx zk5`6p0#sSw&YQ!ZD_w7_;7wxdPHio5K{;S->J@JvW6$izI18~A2ahQUd`~Yp7}b(4 z^MJnd_mV90q8X2GExGRqNW^%Oxr{ab@;wQ%ssHnW+9>aYQP4VEL3)(8Oh}d8<6FdSJbSxW$99uc(K=KqCq~)jOuF+SrOWK$vsKteT zXO=x%I5{bqH%bs6V7_1M-%ebWU4WCYY`MVi#1e+!Wx>9FmYvig!D8#j3QeR_!_em| z!`vJ#3!{8U`P>*cvwSx2%8>Y(mjYH55lJd_9O`SQ*O?`e6kV#y21If-5=2+?r!}7m z|9*3t-K(g0a-soSvhg=q`pCR5<(CI%s+{$$E-gMeY7#gnJFh*3>{}8l4Yr6zMzz#P z1QK5QL9V9Bo7Qsv(NtM$W_x9EYu8Nv$%~M!lRET1a^@bdwK3vhvn~GZkHU$*<+Ys7 zp3>ln0e!^u0$!#Bkk(MnO*p(5u}5-cPGL^^h&b`Gse&`?W;kLU!3Nd z%7JO&qHCjJ7#79Nw~gwAdZPy;{qw~9*iWX|dva4wNqdT=6|`N^6*^`IM5yVwxUD3@ ziQdk+cu~AqxS2Th@en8K%9}vLVZuC4cm0SqCU&0316EkJgV$^Q0kbs3QTNxv_-?l8 zo-`Kh@*k->p*-Tdp&!694t!V}Hp9)b8bZmV#AC#JW?e>622lHVG*}cWvy8*bkHD@! z2UpaX9#tZ*I>MP)Eu-ZYp?@N&bC2LS64c{rjLgvI>sJip`ocv#x%6!%TRtA7+2&~P zQQ*D(ne1|@uikK4neK9#FWYdENw6Sn zVKI?;TPMW zmG*|9>G-r*8z)4&fT@(2t|}97W%2UdW2p`Q(ClJqi5&x@?I*3Wg~XQq6I`NI=Yh2t z-Hfo2!zQJqYm+(qMMCd-EaT8#HO8Nn=tF}ov94{{IfLVyXm8Tew-|GO)k$7#sNdH3 zbfdgUgz{w=DKJPrh!qyfCs}c?JY}E~ZnVD3lf07y!6L6iMDI78P0hossT>&HKgE+L z3ZRY2D-0BUt#e7p>Drhf8Cpwu@A{-D+XStS8jj_-xGA`=t!pYb^=kPX?N8b?BH!l0 z6UWNpu#e1nYD+^i;|gCjY2w7b)iO`_&Jk;Z=B9p3C-zY-pY}e2Z)g6tW!dJU>GYM^x;gE2Ytg41dea)WD){lreE7xadp+H9pEplIQ1&KTc@^Dc?J15; z+=JkTZM}@{srC#>?w=`F?I=+8g|PVLBpYptkt&Aq(KFOFcRr`@rG=&|SQcl_acfI| z&eS)jE;_RwymK+Kt0_CJ`Q0~8LVx4ra-XX)_)YJ@kyr0adN9(nXFP-`+0MFd zny_-;#BLc-t6o;jACD|l&+-a<)qU>es_vRMNs*1=UyZ$>lVh&m;%>fKw3E4^+BOG1 zdfMoR!>-_FP=Ilyk)6mlcdH&2aBpc{7<%!%{-=ZWer2ViyWw`+Gk|Z>5kPJI7sak6 zr{mB6^CuS_BRwlUBfXKiqZ6IEjfpLTh^UgVqT~-{NqISX2S=w#Rax6TcBIZbHKdVV z>!vu4bOH?EN4=KSp<@#*qxw!K*pwtO6yz{8iLtP|vo{INxI|9yfIKtj8c7TOtq$x{ zO%pLK9nLPdbGVy>V)X2d&Sq#nPDNDX4i0_Q_k6Btu1TCtR)rO)&hYUyd?UM!%Es6Axi~odg)$sbKe)82 zm*IxQ^<$5CB@@idPMIViifIxTb>z6dB>I4f&*bHw88$*9iN#$8hIMQ9hTmhvK($L@ zK8{=y9VfiPS>8K{rBw+jG!1jv6cs8`d8}&N_DZQ{JkS~5b{qkXE+EwXwZQDy+d4;y% zsr!D-ex06-VhXO%)f;q>jxVl7`TaF0JWrc*6ER(}t!yob2o=H`kJ*lXc;cMbLjqe` z$tPAtC^04H0NjwHw)h#{VcF#Ao>=eP`ql6?snh+D(Phl@0Iu~!j6b9aI=Pqg!|Ob0 zLe*s9cV$QK-TgUuAS~R)(sAX)Y=4Lp7`o@|7J`PTA815ni7`Pmos*BsL)CW}mI1dC z7}~eT&T5GlWZ4j9j7u6t10};-!|2FZi0w3P{3A!W$800t^}}{d;Fesm{O2<&ruL| zqU1YT5eNsccx(rDzb5pzzMRUeNVhdba@bT^f9OIG%q|gn?Lb&1e>9M92mJ*X`tKe+ zw6jB1^yV-lu30vG+j14C=Zy^`4uUuf>K4r8MsKMa`Av%!eCcUfeN{45+d(VCIW?zVB>E@fA&I`%}&iWdDxizA`k0F@fH}1$)GBm zJ)CV4=m*xNI{nNC%TN6HPbph}Z6P^~4QSbb+y)WAHbV2?ZXwpjPWndrPWto~jS-;ygq23tXW@(=A0|EAhwH8I_l-buvLC#9v(^Q{Gz} zPaCG$1vbi3kE22|4n$pJTATu?nvev85a@@b7_jzKCcB_HakfDfya60~1I)Ovx}c~- z>{+)AV}~h78R|Huow8WyA^x4wxzS?^tr#>1XBDSi`rZTH*I%aBC%ZD5H<#MGZ;oMV zGTIvO{0}SrMJVGTBBPE)x0zL5<=7|h9X}XiH9Y;Y)|@zM9Q5`vGQB%+ zjAm4O&y2lVHM*(V(`R!#&IU$wW4)(z`ThhH<)(sxX{^v?ay5yCf2C&Kx=Tr&|KkGT zeqF5EHo_roAg26YE!saJQ|acJxp0%vSoAkXec}UQ6A`PVS6Wj<`$x?_PHs-p6;hRyVbe{N(U5bc?t`f>?%Mx} z>isWpmW1yJ+5q6cE)u}?e}4|CNr;Ndz)nlZPR_zh(@{@NOxG(iF0gDn$WKbsNzsln z)G10xjndMGF+i3o%rMTdu+Ol}?8A&rGt55FF2PgMNllK*)G1O@QOh2{NXj%TQkJkT zO^yQ?RAeX0_jf`5#kd(EcdBE6vJ4*r5D@u)Yn-g8vY?2dvf!kemhBoln$J-+rvO1m zWBfYnpeht4!WaiKAm;z&s==xr)kV!-sh9kVJaaVdS z)M(80oBaqzRg1*gwR?D;qr{Qp&raC^4vK_sJ<9-&6Q1AN94dp8%=s*-xYiF7v&W@O z0Yf;LWfF3QD6ETZ4fpO!Nch#FS;tbOY}tF2!H$iSA^ojR5r_1X&TMeIA?v>J9SNCW z<6==-X7w+R>gk%emmMo^>A%9qnMppC8G1h*7w0^7&Xo_5LPDp>m0UC&IOdDSjt}4O zUIuNCth6004`g6?ZxwidDK01eh_1qSjVh$gcTJdKu+xok%F8hzg27=6Mx(~eau`H> z`rx~GnB!KMEh#C8Tk0k39CR_iU%!TVa^ya+*zEDnx^sZeUnD5-ac*Gne%;%|`+hNb z(=}Whp3vRak~LFO73Jf%R)^I7m{x1b$(uQo1G&=+anFB#XK2;-JINwz%hS0QOml>8 zpdNB*JYiJ1fOT8`gB6F-#m-KE;x#s-2*O2woP3O1Hwg)PRi}t`seQED5fY3KBK|f- z_x$lONzhGvA|fD8gvhZOWmzdioPwGGH>O1%gp$4QbsqF}eH}VK2({@hW^(1y@U?T! zu1cBZW7ySt4uiYdoS{2Jh%72tmI8&j*qp%@2SaCNnrB}fafxk+BVYI-J^jYc(ek!Be$nFAT~R>tQgeqrx4Ff> zIuVKy_Nm_ZcD2o}tm&}{FA7Zt=q@jq@QANO_hE+-U?ryY{zx6#I7&@_Ug4s$WlaEhDm1Sle+pWYoFghj zdd+L^T9J#`hr&p^N(*UH{(Nl-FBsVHLG5Gg`pukFlyCs+grhv;KD44TLB;KV@rSk< z7ue9g!~NrYc!kCy)p4bW<^^qzoHelO)qSIq5%E|$u?Cjmnv%~;a2rZ=?TJN$Ff=M< zIO{4nk7pki*j4YQM_MDu5H<@|g?=snhj66pBHTE;_cu(T7Zf^~6dEsih4(6v19AKl zR`}@hv4-+#g;igJ51UlmpH4lAB{P#JO^6dkk4+nj`5Ck)4y^duZZuX(h9i!7Kzjw6 zlfY1CyqrKD+daHEC!IC3*r0mffN}S)A~_24OXkO2rrZ$hz+$AdYiW9^qgp}M;rgT0;HW|`!m0}EjxO!=b-|*lDmYs24b1G6ET^Xk zc2Z{xWNY=cTJ9CaAz1eYag@l@;~A=(maRZBJ`T2lnq%BmQtC7c1tcYu!(PW)boOP{ z-w0ZjfKnWs8`MZHq9`>Tstdfn!b$2BD*ZA|myYG9wG&13Z%3bApc8e@*JsvTrglZNwx#3c0~ zh5i|P$XQJ!C`%Cg^Tx-tW@q;}HZ-cC06R3H@)-2|3*tISh@hh#C?<>!$mD|7Lv`og7=0{!c?cS5#2delJ{?Us9joFIqxsJ z6!_ucnPvrjy${GB^>yw%GJm4$XtBg&^7c6K%9qdNw%BH2i8KQ1vxHd>cS*O!>$)?pOn2Ja9wr@##=8IMD|ErT2T9ND{}PEcjG= zDa|~G;qLB_lj6vC+R$2&hEQw`I4TUwZSrS8^*@LN07hVS;dNXh7tpauk0tBsd>#&9~To{FD>ApRqg2n`k7$h>c zk#)=038RW8KsRwV5u_aNpnCbKmvgyB_&Rp}r|o&0_DK5D@SG53ufakHHY`jK(@1*k<7_aIg)cTm(65%b<)_V5`S?`cY7(U^SfxqV z3-d%GjLEr{)qW9eKd48gH`;EOn{hZf%q@AUS({cKAt^WiSiC9=_ZG=OjZ6B$URF&Z z@GR$jLOhC1?MB`Cu1fhDB^|iy=4gHA&m{dg-{SUg!#fpklTDbMkGS5f#_WfLy8%*QYd_n3NFZmyA zJ{@VFmdWkIOl4?7Q7C6c2V-8M-hI9AZoETXg~VZ?5_JFpL2VGEkeuw(&Z`mmg>p=l zk5H}Zfx7+^LHTq~+bhdW$_Mk9A=N7Ou_2y6uX`QM@G{3&m|#L-uENLp-o!!m7dl#36ILY2ms1@!n zQdYH*n(3R6w_$cDeZES<-AxEVUbHFrgum2Ix4?+`-A6@nN#X4f*}&^|-JDDz8brvH z45je*SZ#W1bn0{%$f5kJKOTE=v0j9{1iON$j<*}ymVIyf+$|;AsLW53F`T~3aRuYh z@Y{JfM7!5n)mxB|-F`^qjOG$4N0=0s$4Qf4LHuk#Z^ja3<$L0n5xO8ozgu0C>4%J54?-}y9 zOknM6zOnHQgThtMEG#GqP01095A9N8Mlh(|lI-@1=q;qOrW!z2dTDtV;8Z8c6C*JLHv*g20;V;%MFAAN(19d7$A-68{pf2e--%u z_357!Z2pt%;=h8an5WuK0l;)1{>|6=53s+=!u}EL-!yK1n&=xk**drbvhn{F)8a!) zh8=+E35ZY-{5v4vy}+Lt;MM&jrirbCwZ79oV7ZHh!pj1%-T_#6f5R#ObpIc*%#5w< z{sBqdNIO0jfD!@7Jo~#o&XKfzeA}3gdP47%GuG$*7_fy z27rA!Appx06A*OZ_}jR-0)Axw1O-?fPBz94{}I1;WDxXlb}$w(HUWs_89V$#Be4xw zSxW%R>NNi?`|;n~De#{IL}C6XjmXMKI~y1~*cbz}FaM#TbGs?0R>1gL0G1=mzqbN- zFYpHj)Imt5@epMrZT@@z84cGKY)p1HZLxT;hj|0ky z*dRVZg~4M@%~=_;i{~qeos)s7N+YClVTyc|8OAQXfQ?8ymJ(L-AD}`vLA;*%#mbN?@>?#Z8rd*9yk-Ha%9|K0p?uj%0!fgJt?Ct-^mv uJ{IIu90Z$v+enkd9Bc9;YiORm*fiq@*tMmq+$dEl6Y?ID8<5kt=-V%wG`o!e literal 0 HcmV?d00001 diff --git a/testing/docs/test_authoring.md b/testing/docs/test_authoring.md new file mode 100644 index 00000000000..b41286ba66d --- /dev/null +++ b/testing/docs/test_authoring.md @@ -0,0 +1,142 @@ +# Test Authoring + +All partners are _required_ to author additional integration tests when merging their extension into the __Official Private Preview Release__. The information below outlines how to setup and author these additional tests. + +## Requirements + +All partners are required to cover standard CLI scenarios in your extensions testing suite. When adding these tests and preparing to merge your updated extension whl package, your tests along with the other tests in the test suite must pass at 100%. + +Standard CLI scenarios include: + +1. `az k8s-extension create` +2. `az k8s-extension show` +3. `az k8s-extension list` +4. `az k8s-extension update` +5. `az k8s-extension delete` + +In addition to these standard scenarios, if there are any rigorous parameter validation standards, these should also be included in this test suite. + +## Setup + +The setup process for test authoring is the same as setup for generic testing. See [Setup](../README.md#setup) for guidance. + +## Writing Tests + +This section outlines the common flow for creating and running additional extension integration tests for the `k8s-extension` package. + +The suite utilizes the [Pester](https://pester.dev/) framework. For more information on creating generic Pester tests, see the [Create a Pester Test](https://pester.dev/docs/quick-start#creating-a-pester-test) section in the Pester docs. + +### Step 1: Create Test File + +To create an integration test suite for your extension, create an extension test file in the format `.Tests.ps1` and place the file in one of the following directories +| Extension Type | Directory | +| ---------------------- | ----------------------------------- | +| General Availability | .\test\extensions\public | +| Public Preview | .\test\extensions\public | +| Private Preview | .\test\extensions\private-preview | + +For example, to create a test suite file for the Azure Monitor extension, I create the file `AzureMonitor.Tests.ps1` in the `\test\extensions\public` directory because Container Insights extension is in _Public Preview_. + +### Step 2: Setup Global Variables + +All test suite files must have the following structure for importing the environment config and declaring globals + +```powershell +Describe ' Testing' { + BeforeAll { + $extensionType = "" + $extensionName = "" + $extensionAgentName = "" + $extensionAgentNamespace = "" + + . $PSScriptRoot/../../helper/Constants.ps1 + . $PSScriptRoot/../../helper/Helper.ps1 + } +} +``` + +You can declare additional global variables for your tests by adding additional powershell variable to this `BeforeAll` block. + +_Note: Commonly used constants used by all extension test suites are stored in the `Constants.ps1` file_ + +### Step 3: Add Tests + +Adding tests to the test suite can now be performed by adding `It` blocks to the outer `Describe` block. For instance to test create on a extension in the case of AzureMonitor, I write the following test: + +```powershell +Describe 'Azure Monitor Testing' { + BeforeAll { + $extensionType = "microsoft.azuremonitor.containers" + $extensionName = "azuremonitor-containers" + $extensionAgentName = "omsagent" + $extensionAgentNamespace = "kube-system" + + . $PSScriptRoot/../../helper/Constants.ps1 + . $PSScriptRoot/../../helper/Helper.ps1 + } + + It 'Creates the extension and checks that it onboards correctly' { + $output = az k8s-extension create -c $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type connectedClusters --extension-type $extensionType -n $extensionName + $? | Should -BeTrue + + $output = az k8s-extension show -c $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type connectedClusters -n $extensionName + $? | Should -BeTrue + + $isAutoUpgradeMinorVersion = ($output | ConvertFrom-Json).autoUpgradeMinorVersion + $isAutoUpgradeMinorVersion.ToString() -eq "True" | Should -BeTrue + + # Loop and retry until the extension installs + $n = 0 + do + { + if (Get-ExtensionStatus $extensionName -eq $SUCCESS_MESSAGE) { + if (Get-PodStatus $extensionAgentName -Namespace $extensionAgentNamespace -eq $POD_RUNNING) { + break + } + } + Start-Sleep -Seconds 10 + $n += 1 + } while ($n -le $MAX_RETRY_ATTEMPTS) + $n | Should -BeLessOrEqual $MAX_RETRY_ATTEMPTS + } +} +``` + +The above test calls `az k8s-extension create` to create the `azuremonitor-containers` extension and retries checking that the extension resource was actually created on the Arc cluster and that the extension status successfully returns `$SUCCESS_MESSAGE` which is equivalent to `Successfully installed the extension`. + +## Tips/Notes + +### Accessing Extension Data + +`.\Test.ps1` assumes that the user has `kubectl` and `az` installed in their environment; therefore, tests are able to access information on the extension at the service and on the arc cluster. For instance, in the above test, we access the `extensionconfig` CRDs on the arc cluster by calling + +```powershell +kubectl get extensionconfigs -A -o json +``` + +If we want to access the extension data on the cluster with a specific `$extensionName`, we run + +```powershell +(kubectl get extensionconfigs -A -o json).items | Where-Object { $_.metadata.name -eq $extensionName } +``` + +Because some of these commands are so common, we provide the following helper commands in the `test\Helper.ps1` file + +| Command | Description | +| ------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------- | +| Get-ExtensionData | Retrieves the ExtensionConfig CRD in JSON format with `.meatadata.name` matching the `extensionName` | +| Get-ExtensionStatus | Retrieves the `.status.status` from the ExtensionConfig CRD with `.meatadata.name` matching the `extensionName` | +| Get-PodStatus -Namespace | Retrieves the `status.phase` from the first pod on the cluster with `.metadata.name` matching `extensionName` | + +### Stdout for Debugging + +To print out to the Console for debugging while writing your test cases use the `Write-Host` command. If you attempt to use the `Write-Output` command, it will not show because of the way that Pester is invoked + +```powershell +Write-Host "Some example output" +``` + +### Global Constants + +Looking at the above test, we can see that we are accessing the `ENVCONFIG` to retrieve the environment variables from the `settings.json`. All variables in the `settings.json` are accessible from the `ENVCONFIG`. The most useful ones for testing will be `ENVCONFIG.arcClusterName` and `ENVCONFIG.resourceGroup`. + diff --git a/testing/owners.txt b/testing/owners.txt new file mode 100644 index 00000000000..ead6f446410 --- /dev/null +++ b/testing/owners.txt @@ -0,0 +1,2 @@ +joinnis +nanthi \ No newline at end of file diff --git a/testing/pipeline/k8s-custom-pipelines.yml b/testing/pipeline/k8s-custom-pipelines.yml new file mode 100644 index 00000000000..0592b58ec53 --- /dev/null +++ b/testing/pipeline/k8s-custom-pipelines.yml @@ -0,0 +1,204 @@ +trigger: + batch: true + branches: + include: + - k8s-extension/public + - k8s-extension/private +pr: + branches: + include: + - k8s-extension/public + - k8s-extension/private + +stages: +- stage: BuildTestPublishExtension + displayName: "Build, Test, and Publish Extension" + variables: + TEST_PATH: $(Agent.BuildDirectory)/s/testing + CLI_REPO_PATH: $(Agent.BuildDirectory)/s + SUBSCRIPTION_ID: "15c06b1b-01d6-407b-bb21-740b8617dea3" + RESOURCE_GROUP: "K8sPartnerExtensionTest" + BASE_CLUSTER_NAME: "k8s-extension-cluster" + IS_PRIVATE_BRANCH: $[or(eq(variables['Build.SourceBranch'], 'refs/heads/k8s-extension/private'), eq(variables['System.PullRequest.TargetBranch'], 'k8s-extension/private'))] + jobs: + - template: ./templates/run-test.yml + parameters: + jobName: AzureDefender + path: ./test/extensions/public/AzureDefender.Tests.ps1 + - template: ./templates/run-test.yml + parameters: + jobName: AzureMLKubernetes + path: ./test/extensions/public/AzureMLKubernetes.Tests.ps1 + - template: ./templates/run-test.yml + parameters: + jobName: AzureMonitor + path: ./test/extensions/public/AzureMonitor.Tests.ps1 + - template: ./templates/run-test.yml + parameters: + jobName: AzurePolicy + path: ./test/extensions/public/AzurePolicy.Tests.ps1 + - template: ./templates/run-test.yml + parameters: + jobName: Cassandra + path: ./test/extensions/public/Cassandra.Tests.ps1 + - template: ./templates/run-test.yml + parameters: + jobName: OpenServiceMesh + path: ./test/extensions/public/OpenServiceMesh.Tests.ps1 + - job: BuildPublishExtension + pool: + vmImage: 'ubuntu-latest' + displayName: "Build and Publish the Extension Artifact" + variables: + CLI_REPO_PATH: $(Agent.BuildDirectory)/s + workingDirectory: $(CLI_REPO_PATH) + displayName: "Setup and Build Extension with azdev" + steps: + - template: ./templates/build-extension.yml + parameters: + CLI_REPO_PATH: $(CLI_REPO_PATH) + IS_PRIVATE_BRANCH: $(IS_PRIVATE_BRANCH) + - task: PublishBuildArtifacts@1 + inputs: + pathToPublish: $(CLI_REPO_PATH)/dist + +- stage: AzureCLIOfficial + displayName: "Azure Official CLI Code Checks" + dependsOn: [] + jobs: + - job: CheckLicenseHeader + displayName: "Check License" + pool: + vmImage: 'ubuntu-latest' + steps: + - task: UsePythonVersion@0 + displayName: 'Use Python 3.10' + inputs: + versionSpec: 3.10 + - bash: | + set -ev + + # prepare and activate virtualenv + python -m venv env/ + + chmod +x ./env/bin/activate + source ./env/bin/activate + + # clone azure-cli + git clone -q --single-branch -b dev https://github.com/Azure/azure-cli.git ../azure-cli + + pip install --upgrade pip + pip install -q azdev + + azdev setup -c ../azure-cli -r ./ + + azdev --version + az --version + + azdev verify license + + - job: StaticAnalysis + displayName: "Static Analysis" + pool: + vmImage: 'ubuntu-latest' + steps: + - task: UsePythonVersion@0 + displayName: 'Use Python 3.6' + inputs: + versionSpec: 3.6 + - bash: pip install wheel==0.30.0 pylint==1.9.5 flake8==3.5.0 requests + displayName: 'Install wheel, pylint, flake8, requests' + - bash: python scripts/ci/source_code_static_analysis.py + displayName: "Static Analysis" + + - job: IndexVerify + displayName: "Verify Extensions Index" + pool: + vmImage: 'ubuntu-latest' + steps: + - task: UsePythonVersion@0 + displayName: 'Use Python 3.10' + inputs: + versionSpec: 3.10 + - bash: | + #!/usr/bin/env bash + set -ev + pip install wheel==0.30.0 requests packaging + export CI="ADO" + python ./scripts/ci/test_index.py -v + displayName: "Verify Extensions Index" + + - job: SourceTests + displayName: "Integration Tests, Build Tests" + pool: + vmImage: 'ubuntu-latest' + strategy: + matrix: + Python38: + python.version: '3.8' + Python39: + python.version: '3.9' + Python310: + python.version: '3.10' + steps: + - task: UsePythonVersion@0 + displayName: 'Use Python $(python.version)' + inputs: + versionSpec: '$(python.version)' + - bash: pip install wheel==0.30.0 + displayName: 'Install wheel==0.30.0' + - bash: | + set -ev + + # prepare and activate virtualenv + pip install virtualenv + python -m virtualenv venv/ + source ./venv/bin/activate + + # clone azure-cli + git clone --single-branch -b dev https://github.com/Azure/azure-cli.git ../azure-cli + + pip install --upgrade pip + pip install azdev + + azdev --version + + azdev setup -c ../azure-cli -r ./ -e k8s-extension + azdev test k8s-extension + displayName: 'Run integration test and build test' + + - job: LintModifiedExtensions + displayName: "CLI Linter on Modified Extensions" + pool: + vmImage: 'ubuntu-latest' + steps: + - task: UsePythonVersion@0 + displayName: 'Use Python 3.10' + inputs: + versionSpec: 3.10 + - bash: | + set -ev + + # prepare and activate virtualenv + pip install virtualenv + python -m virtualenv venv/ + source ./venv/bin/activate + + # clone azure-cli + git clone --single-branch -b dev https://github.com/Azure/azure-cli.git ../azure-cli + + pip install --upgrade pip + pip install azdev + + azdev --version + + azdev setup -c ../azure-cli -r ./ -e k8s-extension + + # overwrite the default AZURE_EXTENSION_DIR set by ADO + AZURE_EXTENSION_DIR=~/.azure/cliextensions az --version + + AZURE_EXTENSION_DIR=~/.azure/cliextensions azdev linter --include-whl-extensions k8s-extension + displayName: "CLI Linter on Modified Extension" + env: + ADO_PULL_REQUEST_LATEST_COMMIT: $(System.PullRequest.SourceCommitId) + ADO_PULL_REQUEST_TARGET_BRANCH: $(System.PullRequest.TargetBranch) \ No newline at end of file diff --git a/testing/pipeline/templates/build-extension.yml b/testing/pipeline/templates/build-extension.yml new file mode 100644 index 00000000000..cf63db635a6 --- /dev/null +++ b/testing/pipeline/templates/build-extension.yml @@ -0,0 +1,52 @@ +parameters: + CLI_REPO_PATH: "" +steps: +- bash: | + echo "Using the private preview of k8s-extension to build..." + + cp ${{ parameters.CLI_REPO_PATH }}/src/k8s-extension ${{ parameters.CLI_REPO_PATH }}/src/k8s-extension-private -r + mv ${{ parameters.CLI_REPO_PATH }}/src/k8s-extension-private/azext_k8s_extension ${{ parameters.CLI_REPO_PATH }}/src/k8s-extension-private/azext_k8s_extension_private + cp ${{ parameters.CLI_REPO_PATH }}/src/k8s-extension-private/setup_private.py ${{ parameters.CLI_REPO_PATH }}/src/k8s-extension-private/setup.py + cp ${{ parameters.CLI_REPO_PATH }}/src/k8s-extension-private/azext_k8s_extension_private/consts_private.py ${{ parameters.CLI_REPO_PATH }}/src/k8s-extension-private/azext_k8s_extension_private/consts.py + + EXTENSION_NAME="k8s-extension-private" + EXTENSION_FILE_NAME="k8s_extension_private" + + echo "##vso[task.setvariable variable=EXTENSION_NAME]$EXTENSION_NAME" + echo "##vso[task.setvariable variable=EXTENSION_FILE_NAME]$EXTENSION_FILE_NAME" + condition: and(succeeded(), eq(variables['IS_PRIVATE_BRANCH'], 'True')) + displayName: "Copy Files, Set Variables for k8s-extension-private" +- bash: | + echo "Using the public version of k8s-extension to build..." + + EXTENSION_NAME="k8s-extension" + EXTENSION_FILE_NAME="k8s_extension" + + echo "##vso[task.setvariable variable=EXTENSION_NAME]$EXTENSION_NAME" + echo "##vso[task.setvariable variable=EXTENSION_FILE_NAME]$EXTENSION_FILE_NAME" + condition: and(succeeded(), eq(variables['IS_PRIVATE_BRANCH'], 'False')) + displayName: "Copy Files, Set Variables for k8s-extension" +- task: UsePythonVersion@0 + displayName: 'Use Python 3.6' + inputs: + versionSpec: 3.6 +- bash: | + set -ev + echo "Building extension ${EXTENSION_NAME}..." + + # prepare and activate virtualenv + pip install virtualenv + python3 -m venv env/ + source env/bin/activate + + # clone azure-cli + git clone -q --single-branch -b dev https://github.com/Azure/azure-cli.git ../azure-cli + + pip install --upgrade pip + pip install -q azdev + + ls ${{ parameters.CLI_REPO_PATH }} + + azdev --version + azdev setup -c ../azure-cli -r ${{ parameters.CLI_REPO_PATH }} -e $(EXTENSION_NAME) + azdev extension build $(EXTENSION_NAME) \ No newline at end of file diff --git a/testing/pipeline/templates/run-test.yml b/testing/pipeline/templates/run-test.yml new file mode 100644 index 00000000000..a9950f09063 --- /dev/null +++ b/testing/pipeline/templates/run-test.yml @@ -0,0 +1,104 @@ +parameters: + jobName: '' + path: '' + +jobs: +- job: ${{ parameters.jobName}} + pool: + vmImage: 'ubuntu-latest' + variables: + ${{ if eq(variables['IS_PRIVATE_BRANCH'], 'False') }}: + EXTENSION_NAME: "k8s-extension" + EXTENSION_FILE_NAME: "k8s_extension" + ${{ if ne(variables['IS_PRIVATE_BRANCH'], 'False') }}: + EXTENSION_NAME: "k8s-extension-private" + EXTENSION_FILE_NAME: "k8s_extension_private" + steps: + - bash: | + echo "Installing helm3" + curl -fsSL -o get_helm.sh https://raw.githubusercontent.com/helm/helm/master/scripts/get-helm-3 + chmod 700 get_helm.sh + ./get_helm.sh --version v3.6.3 + + echo "Installing kubectl" + curl -LO "https://storage.googleapis.com/kubernetes-release/release/$(curl -s https://storage.googleapis.com/kubernetes-release/release/stable.txt)/bin/linux/amd64/kubectl" + chmod +x ./kubectl + sudo mv ./kubectl /usr/local/bin/kubectl + kubectl version --client + displayName: "Setup the VM with helm3 and kubectl" + - template: ./build-extension.yml + parameters: + CLI_REPO_PATH: $(CLI_REPO_PATH) + - bash: | + K8S_EXTENSION_VERSION=$(ls ${EXTENSION_FILE_NAME}* | cut -d "-" -f2) + echo "##vso[task.setvariable variable=K8S_EXTENSION_VERSION]$K8S_EXTENSION_VERSION" + cp * $(TEST_PATH)/bin + workingDirectory: $(CLI_REPO_PATH)/dist + displayName: "Copy the Built .whl to Extension Test Path" + + - bash: | + RAND_STR=$RANDOM + AKS_CLUSTER_NAME="${BASE_CLUSTER_NAME}-${RAND_STR}-aks" + ARC_CLUSTER_NAME="${BASE_CLUSTER_NAME}-${RAND_STR}-arc" + + JSON_STRING=$(jq -n \ + --arg SUB_ID "$SUBSCRIPTION_ID" \ + --arg RG "$RESOURCE_GROUP" \ + --arg AKS_CLUSTER_NAME "$AKS_CLUSTER_NAME" \ + --arg ARC_CLUSTER_NAME "$ARC_CLUSTER_NAME" \ + --arg K8S_EXTENSION_VERSION "$K8S_EXTENSION_VERSION" \ + '{subscriptionId: $SUB_ID, resourceGroup: $RG, aksClusterName: $AKS_CLUSTER_NAME, arcClusterName: $ARC_CLUSTER_NAME, extensionVersion: {"k8s-extension": $K8S_EXTENSION_VERSION, "k8s-extension-private": $K8S_EXTENSION_VERSION, connectedk8s: "1.0.0"}}') + echo $JSON_STRING > settings.json + cat settings.json + workingDirectory: $(TEST_PATH) + displayName: "Generate a settings.json file" + + - bash : | + echo "Downloading the kind script" + curl -Lo ./kind https://kind.sigs.k8s.io/dl/v0.11.1/kind-linux-amd64 + chmod +x ./kind + ./kind create cluster + displayName: "Create and Start the Kind cluster" + + - bash: | + curl -sL https://aka.ms/InstallAzureCLIDeb | sudo bash + displayName: "Upgrade az to latest version" + + - task: AzureCLI@2 + displayName: Bootstrap + inputs: + azureSubscription: AzureResourceConnection + scriptType: pscore + scriptLocation: inlineScript + inlineScript: | + .\Bootstrap.ps1 -CI + workingDirectory: $(TEST_PATH) + + - task: AzureCLI@2 + displayName: Run the Test Suite for ${{ parameters.path }} + inputs: + azureSubscription: AzureResourceConnection + scriptType: pscore + scriptLocation: inlineScript + inlineScript: | + .\Test.ps1 -CI -Path ${{ parameters.path }} -Type $(EXTENSION_NAME) + workingDirectory: $(TEST_PATH) + continueOnError: true + + - task: PublishTestResults@2 + inputs: + testResultsFormat: 'JUnit' + testResultsFiles: '**/testing/results/*.xml' + failTaskOnFailedTests: true + condition: succeededOrFailed() + + - task: AzureCLI@2 + displayName: Cleanup + inputs: + azureSubscription: AzureResourceConnection + scriptType: pscore + scriptLocation: inlineScript + inlineScript: | + .\Cleanup.ps1 -CI + workingDirectory: $(TEST_PATH) + condition: succeededOrFailed() \ No newline at end of file diff --git a/testing/settings.template.json b/testing/settings.template.json new file mode 100644 index 00000000000..5129dbd0a20 --- /dev/null +++ b/testing/settings.template.json @@ -0,0 +1,11 @@ +{ + "subscriptionId": "", + "resourceGroup": "", + "aksClusterName": "", + "arcClusterName": "", + + "extensionVersion": { + "k8s-extension": "0.3.0", + "k8s-extension-private": "0.1.0" + } +} \ No newline at end of file diff --git a/testing/test/configurations/Configuration.HTTPS.Tests.ps1 b/testing/test/configurations/Configuration.HTTPS.Tests.ps1 new file mode 100644 index 00000000000..a2dee2b348f --- /dev/null +++ b/testing/test/configurations/Configuration.HTTPS.Tests.ps1 @@ -0,0 +1,54 @@ +Describe 'Source Control Configuration (HTTPS) Testing' { + BeforeAll { + $configurationName = "https-config" + . $PSScriptRoot/Constants.ps1 + . $PSScriptRoot/Helper.ps1 + + $dummyValue = "dummyValue" + $secretName = "git-auth-$configurationName" + } + + It 'Creates a configuration with https user and https key on the cluster' { + $output = az k8s-configuration create -c $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type "connectedClusters" -u "https://github.com/Azure/arc-k8s-demo" -n $configurationName --scope cluster --https-user $dummyValue --https-key $dummyValue --operator-namespace $configurationName + $? | Should -BeTrue + + # Loop and retry until the configuration installs and helm pod comes up + $n = 0 + do + { + if (Get-ConfigStatus $configurationName -eq $SUCCESS_MESSAGE) { + if (Get-PodStatus $configurationName -Namespace $configurationName -eq $POD_RUNNING ) { + break + } + } + Start-Sleep -Seconds 10 + $n += 1 + } while ($n -le $MAX_RETRY_ATTEMPTS) + $n | Should -BeLessOrEqual $MAX_RETRY_ATTEMPTS + + Secret-Exists $secretName -Namespace $configurationName + } + + It "Lists the configurations on the cluster" { + $output = az k8s-configuration list -c $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type connectedClusters + $? | Should -BeTrue + + $configExists = $output | ConvertFrom-Json | Where-Object { $_.id -Match $configurationName } + $configExists | Should -Not -BeNullOrEmpty + } + + It "Deletes the configuration from the cluster" { + az k8s-configuration delete -c $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type connectedClusters -n $configurationName + $? | Should -BeTrue + + # Configuration should be removed from the resource model + az k8s-configuration show -c $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type connectedClusters -n $configurationName + $? | Should -BeFalse + } + + It "Performs another list after the delete" { + $output = az k8s-configuration list -c $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type connectedClusters + $configExists = $output | ConvertFrom-Json | Where-Object { $_.id -Match $configurationName } + $configExists | Should -BeNullOrEmpty + } +} \ No newline at end of file diff --git a/testing/test/configurations/Configuration.HelmOperator.Tests.ps1 b/testing/test/configurations/Configuration.HelmOperator.Tests.ps1 new file mode 100644 index 00000000000..8b89ba24c58 --- /dev/null +++ b/testing/test/configurations/Configuration.HelmOperator.Tests.ps1 @@ -0,0 +1,137 @@ +Describe 'Source Control Configuration (Helm Operator Properties) Testing' { + BeforeAll { + $configurationName = "helm-enabled-config" + . $PSScriptRoot/Constants.ps1 + . $PSScriptRoot/Helper.ps1 + + $customOperatorParams = "--set helm.versions=v3 --set mycustomhelmvalue=yay" + $customChartVersion = "0.6.0" + } + + It 'Creates a configuration with helm enabled on the cluster' { + $output = az k8s-configuration create -c $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type "connectedClusters" -u "https://github.com/Azure/arc-k8s-demo" -n $configurationName --scope cluster --enable-helm-operator --operator-namespace $configurationName --helm-operator-params "--set helm.versions=v3" + $? | Should -BeTrue + + # Loop and retry until the configuration installs and helm pod comes up + $n = 0 + do + { + if (Get-ConfigStatus $configurationName -eq $SUCCESS_MESSAGE) { + if (Get-PodStatus "$configurationName-helm" -Namespace $configurationName -eq $POD_RUNNING ) { + break + } + } + Start-Sleep -Seconds 10 + $n += 1 + } while ($n -le $MAX_RETRY_ATTEMPTS) + $n | Should -BeLessOrEqual $MAX_RETRY_ATTEMPTS + } + + It "Updates the helm operator params and performs a show" { + Set-ItResult -Skipped -Because "Update is not a valid scenario for now" + + az k8s-configuration update -c $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type "connectedClusters" -n $configurationName --helm-operator-params $customOperatorParams + $? | Should -BeTrue + + $output = az k8s-configuration show --cluster-name $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type connectedClusters -n $configurationName + $? | Should -BeTrue + + $configData = $output | ConvertFrom-Json | Where-Object { $_.id -Match $configurationName } + ($configData.helmOperatorProperties.chartValues -eq $customOperatorParams) | Should -BeTrue + + # Loop and retry until the configuration updates + $n = 0 + do + { + $helmOperatorChartValues = (Get-ConfigData $configurationName).spec.helmOperatorProperties.chartValues + if ($helmOperatorChartValues -ne $null -And $helmOperatorChartValues.ToString() -eq $customOperatorParams) { + if (Get-ConfigStatus $configurationName -Match $SUCCESS_MESSAGE) { + break + } + } + Start-Sleep -Seconds 10 + $n += 1 + } while ($n -le $MAX_RETRY_ATTEMPTS) + $n | Should -BeLessOrEqual $MAX_RETRY_ATTEMPTS + } + + It "Updates the helm operator chart version and performs a show" { + Set-ItResult -Skipped -Because "Update is not a valid scenario for now" + + az k8s-configuration update -c $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type "connectedClusters" -n $configurationName --helm-operator-chart-version $customChartVersion + $? | Should -BeTrue + + $output = az k8s-configuration show --cluster-name $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type connectedClusters -n $configurationName + $? | Should -BeTrue + + # Check that the helmOperatorProperties chartValues didn't change + $configData = $output | ConvertFrom-Json | Where-Object { $_.id -Match $configurationName } + ($configData.helmOperatorProperties.chartValues -eq $customOperatorParams) | Should -BeTrue + ($configData.helmOperatorProperties.chartVersion -eq $customChartVersion) | Should -BeTrue + + # Loop and retry until the configuration updates + $n = 0 + do + { + $helmOperatorChartVersion = (Get-ConfigData $configurationName).spec.helmOperatorProperties.chartVersion + if ($helmOperatorChartVersion -ne $null -And $helmOperatorChartVersion.ToString() -eq $customChartVersion) { + if (Get-ConfigStatus $configurationName -Match $SUCCESS_MESSAGE) { + break + } + } + Start-Sleep -Seconds 10 + $n += 1 + } while ($n -le $MAX_RETRY_ATTEMPTS) + $n | Should -BeLessOrEqual $MAX_RETRY_ATTEMPTS + } + + It "Disables the helm operator on the cluster" { + Set-ItResult -Skipped -Because "Update is not a valid scenario for now" + + az k8s-configuration update -c $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type "connectedClusters" -n $configurationName --enable-helm-operator=false + $? | Should -BeTrue + + $output = az k8s-configuration show --cluster-name $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type connectedClusters -n $configurationName + $? | Should -BeTrue + + $helmOperatorEnabled = ($output | ConvertFrom-Json).enableHelmOperator + $helmOperatorEnabled.ToString() -eq "False" | Should -BeTrue + + # Loop and retry until the configuration updates + $n = 0 + do { + $helmOperatorEnabled = (Get-ConfigData $configurationName).spec.enableHelmOperator + if ($helmOperatorEnabled -ne $null -And $helmOperatorEnabled.ToString() -eq "False") { + if (Get-ConfigStatus $configurationName -Match $SUCCESS_MESSAGE) { + break + } + } + Start-Sleep -Seconds 10 + $n += 1 + } while ($n -le $MAX_RETRY_ATTEMPTS) + $n | Should -BeLessOrEqual $MAX_RETRY_ATTEMPTS + } + + It "Lists the configurations on the cluster" { + $output = az k8s-configuration list -c $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type connectedClusters + $? | Should -BeTrue + + $configExists = $output | ConvertFrom-Json | Where-Object { $_.id -Match $configurationName } + $configExists | Should -Not -BeNullOrEmpty + } + + It "Deletes the configuration from the cluster" { + az k8s-configuration delete -c $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type connectedClusters -n $configurationName + $? | Should -BeTrue + + # Configuration should be removed from the resource model + az k8s-configuration show -c $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type connectedClusters -n $configurationName + $? | Should -BeFalse + } + + It "Performs another list after the delete" { + $output = az k8s-configuration list -c $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type connectedClusters + $configExists = $output | ConvertFrom-Json | Where-Object { $_.id -Match $configurationName } + $configExists | Should -BeNullOrEmpty + } +} \ No newline at end of file diff --git a/testing/test/configurations/Configuration.KnownHost.Tests.ps1 b/testing/test/configurations/Configuration.KnownHost.Tests.ps1 new file mode 100644 index 00000000000..2cb2946bc3e --- /dev/null +++ b/testing/test/configurations/Configuration.KnownHost.Tests.ps1 @@ -0,0 +1,6 @@ +Describe 'Source Control Configuration (SSH Configs) Testing' { + BeforeAll { + . $PSScriptRoot/Constants.ps1 + . $PSScriptRoot/Helper.ps1 + } +} \ No newline at end of file diff --git a/testing/test/configurations/Configuration.PrivateKey.Tests.ps1 b/testing/test/configurations/Configuration.PrivateKey.Tests.ps1 new file mode 100644 index 00000000000..4bf86d52012 --- /dev/null +++ b/testing/test/configurations/Configuration.PrivateKey.Tests.ps1 @@ -0,0 +1,86 @@ +Describe 'Source Control Configuration (SSH Configs) Testing' { + BeforeAll { + . $PSScriptRoot/Constants.ps1 + . $PSScriptRoot/Helper.ps1 + + $RSA_KEYPATH = "$TMP_DIRECTORY\rsa.private" + $DSA_KEYPATH = "$TMP_DIRECTORY\dsa.private" + $ECDSA_KEYPATH = "$TMP_DIRECTORY\ecdsa.private" + $ED25519_KEYPATH = "$TMP_DIRECTORY\ed25519.private" + + $KEY_ARR = [System.Tuple]::Create("rsa", $RSA_KEYPATH), [System.Tuple]::Create("dsa", $DSA_KEYPATH), [System.Tuple]::Create("ecdsa", $ECDSA_KEYPATH), [System.Tuple]::Create("ed25519", $ED25519_KEYPATH) + foreach ($keyTuple in $KEY_ARR) { + # Automattically say yes to overwrite with ssh-keygen + Write-Output "y" | ssh-keygen -t $keyTuple.Item1 -f $keyTuple.Item2 -P """" + } + + $SSH_GIT_URL = "git://github.com/anubhav929/flux-get-started.git" + $HTTP_GIT_URL = "https://github.com/Azure/arc-k8s-demo" + + $configDataRSA = [System.Tuple]::Create("rsa-config", $RSA_KEYPATH) + $configDataDSA = [System.Tuple]::Create("dsa-config", $DSA_KEYPATH) + $configDataECDSA = [System.Tuple]::Create("ecdsa-config", $ECDSA_KEYPATH) + $configDataED25519 = [System.Tuple]::Create("ed25519-config", $ED25519_KEYPATH) + + $CONFIG_ARR = $configDataRSA, $configDataDSA, $configDataECDSA, $configDataED25519 + } + + It 'Creates a configuration with each type of ssh private key' { + foreach($configData in $CONFIG_ARR) { + az k8s-configuration create -c $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type "connectedClusters" -u $SSH_GIT_URL -n $configData.Item1 --scope cluster --operator-namespace $configData.Item1 --ssh-private-key-file $configData.Item2 + $? | Should -BeTrue + } + + # Loop and retry until the configuration installs and helm pod comes up + $n = 0 + do + { + $readyConfigs = 0 + foreach($configData in $CONFIG_ARR) { + # TODO: Change this to checking the success message after we merge in the bugfix into the agent + if (Get-PodStatus $configData.Item1 -Namespace $configData.Item1 -eq $POD_RUNNING) { + $readyConfigs += 1 + } + } + Start-Sleep -Seconds 10 + $n += 1 + } while ($n -le 30 -And $readyConfigs -ne 4) + $n | Should -BeLessOrEqual 30 + } + + It 'Fails when trying to create a configuration with ssh url and https auth values' { + az k8s-configuration create -c $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type "connectedClusters" -u $HTTP_GIT_URL -n "config-should-fail" --scope cluster --operator-namespace "config-should-fail" --ssh-private-key-file $RSA_KEYPATH + $? | Should -BeFalse + } + + It "Lists the configurations on the cluster" { + $output = az k8s-configuration list -c $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type connectedClusters + $? | Should -BeTrue + + foreach ($configData in $CONFIG_ARR) { + $configExists = $output | ConvertFrom-Json | Where-Object { $_.id -Match $configData.Item1 } + $configExists | Should -Not -BeNullOrEmpty + } + } + + It "Deletes the configuration from the cluster" { + foreach ($configData in $CONFIG_ARR) { + az k8s-configuration delete -c $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type connectedClusters -n $configData.Item1 + $? | Should -BeTrue + + # Configuration should be removed from the resource model + az k8s-configuration show -c $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type connectedClusters -n $configData.Item1 + $? | Should -BeFalse + } + } + + It "Performs another list after the delete" { + $output = az k8s-configuration list -c $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type connectedClusters + $? | Should -BeTrue + + foreach ($configData in $CONFIG_ARR) { + $configExists = $output | ConvertFrom-Json | Where-Object { $_.id -Match $configData.Item1 } + $configExists | Should -BeNullOrEmpty + } + } +} \ No newline at end of file diff --git a/testing/test/configurations/Configuration.Tests.ps1 b/testing/test/configurations/Configuration.Tests.ps1 new file mode 100644 index 00000000000..a85df42ed2e --- /dev/null +++ b/testing/test/configurations/Configuration.Tests.ps1 @@ -0,0 +1,80 @@ +Describe 'Basic Source Control Configuration Testing' { + BeforeAll { + $configurationName = "basic-config" + . $PSScriptRoot/Constants.ps1 + . $PSScriptRoot/Helper.ps1 + } + + It 'Creates a configuration and checks that it onboards correctly' { + az k8s-configuration create -c $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type "connectedClusters" -u "https://github.com/Azure/arc-k8s-demo" -n $configurationName --scope cluster --enable-helm-operator=false --operator-namespace $configurationName + $? | Should -BeTrue + + # Loop and retry until the configuration installs + $n = 0 + do + { + if (Get-ConfigStatus $configurationName -Match $SUCCESS_MESSAGE) { + break + } + Start-Sleep -Seconds 10 + $n += 1 + } while ($n -le $MAX_RETRY_ATTEMPTS) + $n | Should -BeLessOrEqual $MAX_RETRY_ATTEMPTS + } + + It "Performs a show on the configuration" { + $output = az k8s-configuration show -c $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type "connectedClusters" -n $configurationName + $? | Should -BeTrue + $output | Should -Not -BeNullOrEmpty + } + + It "Runs an update on the configuration on the cluster" { + Set-ItResult -Skipped -Because "Update is not a valid scenario for now" + + az k8s-configuration update -c $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type connectedClusters -n $configurationName --enable-helm-operator + $? | Should -BeTrue + + $output = az k8s-configuration show --cluster-name $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type connectedClusters -n $configurationName + $? | Should -BeTrue + + $helmOperatorEnabled = ($output | ConvertFrom-Json).enableHelmOperator + $helmOperatorEnabled.ToString() -eq "True" | Should -BeTrue + + # Loop and retry until the configuration updates + $n = 0 + do { + $helmOperatorEnabled = (Get-ConfigData $configurationName).spec.enableHelmOperator + if ($helmOperatorEnabled -And $helmOperatorEnabled.ToString() -eq "True") { + if (Get-ConfigStatus $configurationName -Match $SUCCESS_MESSAGE) { + break + } + } + Start-Sleep -Seconds 10 + $n += 1 + } while ($n -le $MAX_RETRY_ATTEMPTS) + $n | Should -BeLessOrEqual $MAX_RETRY_ATTEMPTS + } + + It "Lists the configurations on the cluster" { + $output = az k8s-configuration list -c $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type connectedClusters + $? | Should -BeTrue + + $configExists = $output | ConvertFrom-Json | Where-Object { $_.id -Match $configurationName } + $configExists | Should -Not -BeNullOrEmpty + } + + It "Deletes the configuration from the cluster" { + az k8s-configuration delete -c $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type connectedClusters -n $configurationName + $? | Should -BeTrue + + # Configuration should be removed from the resource model + az k8s-configuration show -c $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type connectedClusters -n $configurationName + $? | Should -BeFalse + } + + It "Performs another list after the delete" { + $output = az k8s-configuration list -c $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type connectedClusters + $configExists = $output | ConvertFrom-Json | Where-Object { $_.id -Match $configurationName } + $configExists | Should -BeNullOrEmpty + } +} \ No newline at end of file diff --git a/testing/test/configurations/Constants.ps1 b/testing/test/configurations/Constants.ps1 new file mode 100644 index 00000000000..f1e8c6ffdc3 --- /dev/null +++ b/testing/test/configurations/Constants.ps1 @@ -0,0 +1,8 @@ +$ENVCONFIG = Get-Content -Path $PSScriptRoot\..\..\settings.json | ConvertFrom-Json +$SUCCESS_MESSAGE = "Successfully installed the operator" +$FAILED_MESSAGE = "Failed the install of the operator" +$TMP_DIRECTORY = "$PSScriptRoot\..\..\tmp" + +$POD_RUNNING = "Running" + +$MAX_RETRY_ATTEMPTS = 18 \ No newline at end of file diff --git a/testing/test/configurations/Helper.ps1 b/testing/test/configurations/Helper.ps1 new file mode 100644 index 00000000000..842e2da84aa --- /dev/null +++ b/testing/test/configurations/Helper.ps1 @@ -0,0 +1,45 @@ +function Get-ConfigData { + param( + [string]$configName + ) + + $output = kubectl get gitconfigs -A -o json | ConvertFrom-Json + return $output.items | Where-Object { $_.metadata.name -eq $configurationName } +} + +function Get-ConfigStatus { + param( + [string]$configName + ) + + $configData = Get-ConfigData $configName + if ($configData -ne $null) { + return $configData.status.status + } + return $null +} + +function Get-PodStatus { + param( + [string]$podName, + [string]$Namespace + ) + + $allPodData = kubectl get pods -n $Namespace -o json | ConvertFrom-Json + $podData = $allPodData.items | Where-Object { $_.metadata.name -Match $podName } + return $podData.status.phase +} + +function Secret-Exists { + param( + [string]$secretName, + [string]$Namespace + ) + + $allSecretData = kubectl get secrets -n $Namespace -o json | ConvertFrom-Json + $secretData = $allSecretData.items | Where-Object { $_.metadata.name -Match $secretName } + if ($secretData.Length -ge 1) { + return $true + } + return $false +} \ No newline at end of file diff --git a/testing/test/extensions/data/azure_ml/test_cert.pem b/testing/test/extensions/data/azure_ml/test_cert.pem new file mode 100644 index 00000000000..a8cfe1294b4 --- /dev/null +++ b/testing/test/extensions/data/azure_ml/test_cert.pem @@ -0,0 +1,32 @@ +-----BEGIN CERTIFICATE----- +MIIFlzCCA3+gAwIBAgIULOv9pTNMMZNMXFF3ctTJZEcPfQgwDQYJKoZIhvcNAQEL +BQAwWzELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM +GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDEUMBIGA1UEAwwLdGVzdGluZi5jb20w +HhcNMjIwOTA4MDMxNDA0WhcNMjMwOTA4MDMxNDA0WjBbMQswCQYDVQQGEwJBVTET +MBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50ZXJuZXQgV2lkZ2l0cyBQ +dHkgTHRkMRQwEgYDVQQDDAt0ZXN0aW5mLmNvbTCCAiIwDQYJKoZIhvcNAQEBBQAD +ggIPADCCAgoCggIBAM1dfkb2+o/VzazOSSXlv03rRaV4/+wKALuOGrZGJbGzm4DJ +WyN6+zogIKIbDMPUm4eB1Jh9vSQ0RciwhCUogPel7HYQhlFtyfq4I2PReXm7MInm +ZGpkFZdHliYFMQv5O1FDN4SjHbpKpEomUIngj+j5IRuQ+5wksJfEE4GKBdTv0zSJ +kvIW3o4qf3kYUuoT53g93HqUQKg6LKZ0ra7som1F1dVuOgYXmyirrdFm2SJ6y4Z7 +Fuu7/bNHJAeUIcaw/3FZrKAdYst5MO6BquDPQ3LUmIW9MPO7ynfR0MKMXFvHmQmt +ve9IMRF52FkKqO6mut3kMEhQqE9iiEcLZDUghRwzxIgYU1cHw1jBCTxois4f6HdY +zK9fK9TZoI1ftWoFcofhV4uiB8NwsRQqFeAsMrd2qCbMjLoHysMbKl3ORwtG+sl0 +ti3DhiLQbedfBHzy0xtaZvkauP6+qoGYYjGiDQjP+acvCrjjSwVpWWhu44EWFWnc +Iszjp2AaG4LagaV+ZjTHDa4mkyz/dI3EDZ6wDmHCYdbI57yJhXqjSooVYNE4FYwo +KDXVT+42mtBGIA+e/pRFYE1bUsuIubhuKVLNd081W+rsS3Juv6KWMNnahUm8Mv1P +unwQKrtJtc31rEXGjsVp4E95ncbu/0aAoczCnYlXp1pLNevnNfRdeoOWnNLzAgMB +AAGjUzBRMB0GA1UdDgQWBBQNlOeOEqZh7hojtGoKETkk+Cdl/zAfBgNVHSMEGDAW +gBQNlOeOEqZh7hojtGoKETkk+Cdl/zAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3 +DQEBCwUAA4ICAQB3Dmv2Ftxp0vTDBZMUXjOKqpyuznpT5Y7LzoHxWKBXnwCGVuOo +1P1sse/QD87No+jAi3lPtQo1+GVbnN8kkIPAu8Cu3ogrlMr9938ogWz1/x33D2Nh +DLk2ZDg7UxsGxIKvMCV9MopZGQ2HCN7M43iXJ5dpZ82F6kMHZpGdigAnbTzh8iGu +vro1SiBCwPwlv+VW7VvU0OpGgBQlVj8CMON01/lWYZd4vXrv5iox59l/b1HcNrYN +Pe2CvZAmqx4Wnar8HLV6TAFPqvFf/6kAeWXt89mKaGx/LTy632sXeBHVnL62o6OD +WUjLECjTF1LSI/tzgoQUKIJ70FysIayUoSDcyb+jpb0dKBoi1vUQOS3R5gH35Z0c +fiGXvsDCtK8UvU5V4W8msBtEF1TB1vk5rrhAIGecHyY87iQnF/Lue6CvLQ6jhga6 +A7RRaKf05Stz+pyG3AF3P6iAyFK72k9LIpb+iGvlDZ2yWAMcqEuTRGeo3G3xQabN +VGG0thneWrQ7KicRXNavMgytnHs73mHGTMgffg4njcNPuio8ewcwxhDycYJpr4gz +NqtdgFPBzx9nQjrK6ZBDSzbkkLFNLnr+OSB9pvlpVnC957qqZ1L0GrJIfN/kbCF3 +TG7u3bkbTjlrvuxY1FAqrXesZrlBpx5cxLrO8XOuSlFB0q2pJSwfIv6Jbw== +-----END CERTIFICATE----- \ No newline at end of file diff --git a/testing/test/extensions/data/azure_ml/test_key.pem b/testing/test/extensions/data/azure_ml/test_key.pem new file mode 100644 index 00000000000..e238b5c2225 --- /dev/null +++ b/testing/test/extensions/data/azure_ml/test_key.pem @@ -0,0 +1,52 @@ +-----BEGIN PRIVATE KEY----- +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 +hsfzUDNwvb4vnu9Yq3m//0pk/pMs6g== +-----END PRIVATE KEY----- \ No newline at end of file diff --git a/testing/test/extensions/public/AzureDefender.Tests.ps1 b/testing/test/extensions/public/AzureDefender.Tests.ps1 new file mode 100644 index 00000000000..419d226cb2e --- /dev/null +++ b/testing/test/extensions/public/AzureDefender.Tests.ps1 @@ -0,0 +1,71 @@ +Describe 'Azure Defender Testing' { + BeforeAll { + $extensionType = "microsoft.azuredefender.kubernetes" + $extensionName = "microsoft.azuredefender.kubernetes" + $extensionAgentNamespace = "azuredefender" + + . $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 connectedClusters --extension-type $extensionType -n $extensionName --no-wait + $? | Should -BeTrue + + $output = az $Env:K8sExtensionName show -c $($ENVCONFIG.arcClusterName) -g $($ENVCONFIG.resourceGroup) --cluster-type connectedClusters -n $extensionName + $? | Should -BeTrue + + $isAutoUpgradeMinorVersion = ($output | ConvertFrom-Json).autoUpgradeMinorVersion + $isAutoUpgradeMinorVersion.ToString() -eq "True" | Should -BeTrue + + # Loop and retry until the extension installs + $n = 0 + do + { + # Only check the extension config, not the pod since this doesn't bring up pods + $output = az $Env:K8sExtensionName show -c $($ENVCONFIG.arcClusterName) -g $($ENVCONFIG.resourceGroup) --cluster-type connectedClusters -n $extensionName + $provisioningState = ($output | ConvertFrom-Json).provisioningState + Write-Host "Got ProvisioningState: $provisioningState for the extension" + if (Has-ExtensionData $extensionName) { + break + } + Start-Sleep -Seconds 10 + $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 connectedClusters -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 connectedClusters + $? | 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 connectedClusters -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 connectedClusters -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 connectedClusters + $? | Should -BeTrue + $output | Should -Not -BeNullOrEmpty + + $extensionExists = $output | ConvertFrom-Json | Where-Object { $_.extensionType -eq $extensionName } + $extensionExists | Should -BeNullOrEmpty + } +} diff --git a/testing/test/extensions/public/AzureMLKubernetes.Tests.ps1 b/testing/test/extensions/public/AzureMLKubernetes.Tests.ps1 new file mode 100644 index 00000000000..d38627219c1 --- /dev/null +++ b/testing/test/extensions/public/AzureMLKubernetes.Tests.ps1 @@ -0,0 +1,221 @@ +Describe 'AzureML Kubernetes Testing' { + BeforeAll { + $extensionType = "Microsoft.AzureML.Kubernetes" + $extensionName = "azureml-kubernetes-connector" + $extensionAgentNamespace = "azureml" + $relayResourceIDKey = "relayserver.hybridConnectionResourceID" + $serviceBusResourceIDKey = "servicebus.resourceID" + $mockUpdateKey = "mockTest" + $mockProtectedUpdateKey = "mockProtectedTest" + + . $PSScriptRoot/../../helper/Constants.ps1 + . $PSScriptRoot/../../helper/Helper.ps1 + } + + It 'Creates the extension and checks that it onboards correctly with inference and SSL enabled' { + $sslKeyPemFile = Join-Path (Join-Path (Join-Path (Split-Path $PSScriptRoot -Parent) "data") "azure_ml") "test_key.pem" + $sslCertPemFile = Join-Path (Join-Path (Join-Path (Split-Path $PSScriptRoot -Parent) "data") "azure_ml") "test_cert.pem" + az $Env:K8sExtensionName create -c $($ENVCONFIG.arcClusterName) -g $($ENVCONFIG.resourceGroup) --cluster-type connectedClusters --extension-type $extensionType -n $extensionName --release-train stable --config enableInference=true identity.proxy.remoteEnabled=True identity.proxy.remoteHost=https://master.experiments.azureml-test.net inferenceRouterServiceType=nodePort sslCname=testinf.com --config-protected sslKeyPemFile=$sslKeyPemFile sslCertPemFile=$sslCertPemFile --no-wait + $? | Should -BeTrue + + $output = az $Env:K8sExtensionName show -c $($ENVCONFIG.arcClusterName) -g $($ENVCONFIG.resourceGroup) --cluster-type connectedClusters -n $extensionName + $? | Should -BeTrue + + $isAutoUpgradeMinorVersion = ($output | ConvertFrom-Json).autoUpgradeMinorVersion + $isAutoUpgradeMinorVersion.ToString() -eq "True" | Should -BeTrue + + # Loop and retry until the extension installs + $n = 0 + do + { + if (Has-ExtensionData $extensionName) { + break + } + Start-Sleep -Seconds 20 + $n += 1 + } while ($n -le $MAX_RETRY_ATTEMPTS) + $n | Should -BeLessOrEqual $MAX_RETRY_ATTEMPTS + + # check if relay is populated + $relayResourceID = Get-ExtensionConfigurationSettings $extensionName $relayResourceIDKey + $relayResourceID | Should -Not -BeNullOrEmpty + } + + It "Performs a show on the extension" { + $output = az $Env:K8sExtensionName show -c $($ENVCONFIG.arcClusterName) -g $($ENVCONFIG.resourceGroup) --cluster-type connectedClusters -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 connectedClusters + $? | Should -BeTrue + + $output | Should -Not -BeNullOrEmpty + $extensionExists = $output | ConvertFrom-Json | Where-Object { $_.extensionType -eq $extensionType } + $extensionExists | Should -Not -BeNullOrEmpty + } + + It "Wait for the extension to be ready" { + # Loop and retry until the extension installed + $n = 0 + do + { + + $output = az $Env:K8sExtensionName show -c $($ENVCONFIG.arcClusterName) -g $($ENVCONFIG.resourceGroup) --cluster-type connectedClusters -n $extensionName + $? | Should -BeTrue + + $provisioningState = ($output | ConvertFrom-Json).provisioningState + Write-Host "Provisioning state: $provisioningState" + if ($provisioningState -eq "Succeeded") { + break + } + Start-Sleep -Seconds 20 + $n += 1 + } while ($n -le $MAX_RETRY_ATTEMPTS) + $n | Should -BeLessOrEqual $MAX_RETRY_ATTEMPTS + } + + It "Perform Update extension" { + $sslKeyPemFile = Join-Path (Join-Path (Join-Path (Split-Path $PSScriptRoot -Parent) "data") "azure_ml") "test_key.pem" + $sslCertPemFile = Join-Path (Join-Path (Join-Path (Split-Path $PSScriptRoot -Parent) "data") "azure_ml") "test_cert.pem" + az $Env:K8sExtensionName update -c $($ENVCONFIG.arcClusterName) -g $($ENVCONFIG.resourceGroup) --cluster-type connectedClusters -n $extensionName --config "$($mockUpdateKey)=true" --config-protected "$($mockProtectedUpdateKey)=true" sslKeyPemFile=$sslKeyPemFile sslCertPemFile=$sslCertPemFile --no-wait + $? | Should -BeTrue + + # Loop and retry until the extension updated + $n = 0 + do + { + + $output = az $Env:K8sExtensionName show -c $($ENVCONFIG.arcClusterName) -g $($ENVCONFIG.resourceGroup) --cluster-type connectedClusters -n $extensionName + $? | Should -BeTrue + + $provisioningState = ($output | ConvertFrom-Json).provisioningState + Write-Host "Provisioning state: $provisioningState" + if ($provisioningState -eq "Succeeded") { + break + } + Start-Sleep -Seconds 20 + $n += 1 + } while ($n -le $MAX_RETRY_ATTEMPTS) + $n | Should -BeLessOrEqual $MAX_RETRY_ATTEMPTS + + $mockedUpdateData = Get-ExtensionConfigurationSettings $extensionName $mockUpdateKey + $mockedUpdateData | Should -Not -BeNullOrEmpty + } + + It "Deletes the extension from the cluster with inference enabled" { + # cleanup the relay and servicebus + $relayResourceID = Get-ExtensionConfigurationSettings $extensionName $relayResourceIDKey + $relayNamespaceName = $relayResourceID.split("/")[8] + az relay namespace delete --resource-group $ENVCONFIG.resourceGroup --name $relayNamespaceName + + $output = az $Env:K8sExtensionName delete -c $($ENVCONFIG.arcClusterName) -g $($ENVCONFIG.resourceGroup) --cluster-type connectedClusters -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 connectedClusters -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 connectedClusters + $? | Should -BeTrue + $output | Should -Not -BeNullOrEmpty + + $extensionExists = $output | ConvertFrom-Json | Where-Object { $_.extensionType -eq $extensionName } + $extensionExists | Should -BeNullOrEmpty + } + + # It 'Creates the extension and checks that it onboards correctly with training enabled' { + # az $Env:K8sExtensionName create -c $($ENVCONFIG.arcClusterName) -g $($ENVCONFIG.resourceGroup) --cluster-type connectedClusters --extension-type $extensionType -n $extensionName --release-train staging --config enableTraining=true + # $? | Should -BeTrue + + # $output = az $Env:K8sExtensionName show -c $($ENVCONFIG.arcClusterName) -g $($ENVCONFIG.resourceGroup) --cluster-type connectedClusters -n $extensionName + # $? | Should -BeTrue + + # $isAutoUpgradeMinorVersion = ($output | ConvertFrom-Json).autoUpgradeMinorVersion + # $isAutoUpgradeMinorVersion.ToString() -eq "True" | Should -BeTrue + + # # Loop and retry until the extension installs + # $n = 0 + # do + # { + # if (Has-ExtensionData $extensionName) { + # break + # } + # Start-Sleep -Seconds 20 + # $n += 1 + # } while ($n -le $MAX_RETRY_ATTEMPTS) + # $n | Should -BeLessOrEqual $MAX_RETRY_ATTEMPTS + + # # check if relay is populated + # $relayResourceID = Get-ExtensionConfigurationSettings $extensionName $relayResourceIDKey + # $relayResourceID | Should -Not -BeNullOrEmpty + # } + + # It "Deletes the extension from the cluster" { + # # cleanup the relay and servicebus + # $relayResourceID = Get-ExtensionConfigurationSettings $extensionName $relayResourceIDKey + # $serviceBusResourceID = Get-ExtensionConfigurationSettings $extensionName $serviceBusResourceIDKey + # $relayNamespaceName = $relayResourceID.split("/")[8] + # $serviceBusNamespaceName = $serviceBusResourceID.split("/")[8] + # az relay namespace delete --resource-group $ENVCONFIG.resourceGroup --name $relayNamespaceName + # az servicebus namespace delete --resource-group $ENVCONFIG.resourceGroup --name $serviceBusNamespaceName + + # $output = az $Env:K8sExtensionName delete -c $($ENVCONFIG.arcClusterName) -g $($ENVCONFIG.resourceGroup) --cluster-type connectedClusters -n $extensionName + # $? | Should -BeTrue + + # # Extension should not be found on the cluster + # $output = az $Env:K8sExtensionName show -c $($ENVCONFIG.arcClusterName) -g $($ENVCONFIG.resourceGroup) --cluster-type connectedClusters -n $extensionName + # $? | Should -BeFalse + # $output | Should -BeNullOrEmpty + # } + + # It 'Creates the extension and checks that it onboards correctly with inference enabled' { + # az $Env:K8sExtensionName create -c $($ENVCONFIG.arcClusterName) -g $($ENVCONFIG.resourceGroup) --cluster-type connectedClusters --extension-type $extensionType -n $extensionName --release-train staging --config enableInference=true identity.proxy.remoteEnabled=True identity.proxy.remoteHost=https://master.experiments.azureml-test.net allowInsecureConnections=True inferenceLoadBalancerHA=false + # $? | Should -BeTrue + + # $output = az $Env:K8sExtensionName show -c $($ENVCONFIG.arcClusterName) -g $($ENVCONFIG.resourceGroup) --cluster-type connectedClusters -n $extensionName + # $? | Should -BeTrue + + # $isAutoUpgradeMinorVersion = ($output | ConvertFrom-Json).autoUpgradeMinorVersion + # $isAutoUpgradeMinorVersion.ToString() -eq "True" | Should -BeTrue + + # # Loop and retry until the extension installs + # $n = 0 + # do + # { + # if (Has-ExtensionData $extensionName) { + # break + # } + # Start-Sleep -Seconds 20 + # $n += 1 + # } while ($n -le $MAX_RETRY_ATTEMPTS) + # $n | Should -BeLessOrEqual $MAX_RETRY_ATTEMPTS + + # # check if relay is populated + # $relayResourceID = Get-ExtensionConfigurationSettings $extensionName $relayResourceIDKey + # $relayResourceID | Should -Not -BeNullOrEmpty + # } + + # It "Deletes the extension from the cluster with inference enabled" { + # # cleanup the relay and servicebus + # $relayResourceID = Get-ExtensionConfigurationSettings $extensionName $relayResourceIDKey + # $serviceBusResourceID = Get-ExtensionConfigurationSettings $extensionName $serviceBusResourceIDKey + # $relayNamespaceName = $relayResourceID.split("/")[8] + # $serviceBusNamespaceName = $serviceBusResourceID.split("/")[8] + # az relay namespace delete --resource-group $ENVCONFIG.resourceGroup --name $relayNamespaceName + # az servicebus namespace delete --resource-group $ENVCONFIG.resourceGroup --name $serviceBusNamespaceName + + # $output = az $Env:K8sExtensionName delete -c $($ENVCONFIG.arcClusterName) -g $($ENVCONFIG.resourceGroup) --cluster-type connectedClusters -n $extensionName + # $? | Should -BeTrue + + # # Extension should not be found on the cluster + # $output = az $Env:K8sExtensionName show -c $($ENVCONFIG.arcClusterName) -g $($ENVCONFIG.resourceGroup) --cluster-type connectedClusters -n $extensionName + # $? | Should -BeFalse + # $output | Should -BeNullOrEmpty + # } +} diff --git a/testing/test/extensions/public/AzureMonitor.Tests.ps1 b/testing/test/extensions/public/AzureMonitor.Tests.ps1 new file mode 100644 index 00000000000..9748755eea0 --- /dev/null +++ b/testing/test/extensions/public/AzureMonitor.Tests.ps1 @@ -0,0 +1,68 @@ +Describe 'Azure Monitor Testing' { + BeforeAll { + $extensionType = "microsoft.azuremonitor.containers" + $extensionName = "azuremonitor-containers" + $extensionAgentName = "omsagent" + $extensionAgentNamespace = "kube-system" + + . $PSScriptRoot/../../helper/Constants.ps1 + . $PSScriptRoot/../../helper/Helper.ps1 + } + + It 'Creates the extension and checks that it onboards correctly' { + az $Env:K8sExtensionName create -c $($ENVCONFIG.arcClusterName) -g $($ENVCONFIG.resourceGroup) --cluster-type connectedClusters --extension-type $extensionType -n $extensionName --no-wait + $? | Should -BeTrue + + $output = az $Env:K8sExtensionName show -c $($ENVCONFIG.arcClusterName) -g $($ENVCONFIG.resourceGroup) --cluster-type connectedClusters -n $extensionName + $? | Should -BeTrue + + $isAutoUpgradeMinorVersion = ($output | ConvertFrom-Json).autoUpgradeMinorVersion + $isAutoUpgradeMinorVersion.ToString() -eq "True" | Should -BeTrue + + # Loop and retry until the extension installs + $n = 0 + do + { + if (Has-ExtensionData $extensionName) { + break + } + Start-Sleep -Seconds 10 + $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 connectedClusters -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 connectedClusters + $? | 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 connectedClusters -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 connectedClusters -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 connectedClusters + $? | Should -BeTrue + $output | Should -Not -BeNullOrEmpty + + $extensionExists = $output | ConvertFrom-Json | Where-Object { $_.extensionType -eq $extensionName } + $extensionExists | Should -BeNullOrEmpty + } +} diff --git a/testing/test/extensions/public/AzurePolicy.Tests.ps1 b/testing/test/extensions/public/AzurePolicy.Tests.ps1 new file mode 100644 index 00000000000..8f68426f4ed --- /dev/null +++ b/testing/test/extensions/public/AzurePolicy.Tests.ps1 @@ -0,0 +1,74 @@ +Describe 'Azure Policy Testing' { + BeforeAll { + $extensionType = "microsoft.policyinsights" + $extensionName = "policy" + $extensionAgentName = "azure-policy" + $extensionAgentNamespace = "kube-system" + + . $PSScriptRoot/../../helper/Constants.ps1 + . $PSScriptRoot/../../helper/Helper.ps1 + } + + It 'Creates the extension and checks that it onboards correctly' { + az $Env:K8sExtensionName create -c $($ENVCONFIG.arcClusterName) -g $($ENVCONFIG.resourceGroup) --cluster-type connectedClusters --extension-type $extensionType -n $extensionName --no-wait + $? | Should -BeTrue + + $output = az $Env:K8sExtensionName show -c $($ENVCONFIG.arcClusterName) -g $($ENVCONFIG.resourceGroup) --cluster-type connectedClusters -n $extensionName + $? | Should -BeTrue + + $isAutoUpgradeMinorVersion = ($output | ConvertFrom-Json).autoUpgradeMinorVersion + $isAutoUpgradeMinorVersion.ToString() -eq "True" | Should -BeTrue + + # Check that we get the principal id back for the created identity + $principalId = ($output | ConvertFrom-Json).identity.principalId + $principalId | Should -Not -BeNullOrEmpty + + # Loop and retry until the extension installs + $n = 0 + do + { + # Only check the extension config, not the pod since this doesn't bring up pods + $output = Invoke-Expression "az $Env:K8sExtensionName show -c $($ENVCONFIG.arcClusterName) -g $($ENVCONFIG.resourceGroup) --cluster-type connectedClusters -n $extensionName" -ErrorVariable badOut + if (Has-ExtensionData $extensionName){ + break + } + Start-Sleep -Seconds 10 + $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 connectedClusters -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 connectedClusters + $? | 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 connectedClusters -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 connectedClusters -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 connectedClusters + $? | Should -BeTrue + $output | Should -Not -BeNullOrEmpty + + $extensionExists = $output | ConvertFrom-Json | Where-Object { $_.extensionType -eq $extensionName } + $extensionExists | Should -BeNullOrEmpty + } +} diff --git a/testing/test/extensions/public/Cassandra.Tests.ps1 b/testing/test/extensions/public/Cassandra.Tests.ps1 new file mode 100644 index 00000000000..e579c43ee30 --- /dev/null +++ b/testing/test/extensions/public/Cassandra.Tests.ps1 @@ -0,0 +1,98 @@ +Describe 'Cassandra Testing' { + BeforeAll { + $extensionType = "cassandradatacentersoperator" + $extensionName = "cassandra" + + . $PSScriptRoot/../../helper/Constants.ps1 + . $PSScriptRoot/../../helper/Helper.ps1 + } + + It 'Creates the extension and checks that it onboards correctly' { + az $Env:K8sExtensionName create -c $($ENVCONFIG.arcClusterName) -g $($ENVCONFIG.resourceGroup) --cluster-type connectedClusters --extension-type $extensionType -n $extensionName --no-wait + $? | Should -BeTrue + + $output = az $Env:K8sExtensionName show -c $($ENVCONFIG.arcClusterName) -g $($ENVCONFIG.resourceGroup) --cluster-type connectedClusters -n $extensionName + $? | Should -BeTrue + + $isAutoUpgradeMinorVersion = ($output | ConvertFrom-Json).autoUpgradeMinorVersion + $isAutoUpgradeMinorVersion.ToString() -eq "True" | Should -BeTrue + + # Check that we get the principal id back for the created identity + $principalId = ($output | ConvertFrom-Json).identity.principalId + $principalId | Should -Not -BeNullOrEmpty + + # Loop and retry until the extension installs + $n = 0 + do + { + # Only check the extension config, not the pod since this doesn't bring up pods + $output = az $Env:K8sExtensionName show -c $($ENVCONFIG.arcClusterName) -g $($ENVCONFIG.resourceGroup) --cluster-type connectedClusters -n $extensionName + $provisioningState = ($output | ConvertFrom-Json).provisioningState + Write-Host "Got ProvisioningState: $provisioningState for the extension" + if ((Has-ExtensionData $extensionName) -And ($provisioningState -eq "Succeeded")) { + break + } + Start-Sleep -Seconds 10 + $n += 1 + } while ($n -le 20) + $n | Should -BeLessOrEqual 20 + } + + It "Performs a show on the extension" { + $output = az $Env:K8sExtensionName show -c $($ENVCONFIG.arcClusterName) -g $($ENVCONFIG.resourceGroup) --cluster-type connectedClusters -n $extensionName + $? | Should -BeTrue + $output | Should -Not -BeNullOrEmpty + } + + It "Runs an update on the extension on the cluster" { + $output = az $Env:K8sExtensionName update -c $($ENVCONFIG.arcClusterName) -g $($ENVCONFIG.resourceGroup) --cluster-type connectedClusters -n $extensionName --auto-upgrade false --no-wait + $? | Should -BeTrue + + $output = az $Env:K8sExtensionName show -c $($ENVCONFIG.arcClusterName) -g $($ENVCONFIG.resourceGroup) --cluster-type connectedClusters -n $extensionName + $? | Should -BeTrue + + $isAutoUpgradeMinorVersion = ($output | ConvertFrom-Json).autoUpgradeMinorVersion + $isAutoUpgradeMinorVersion.ToString() -eq "False" | Should -BeTrue + + # Loop and retry until the extension config updates + $n = 0 + do + { + $isAutoUpgradeMinorVersion = (Get-ExtensionData $extensionName).spec.autoUpgradeMinorVersion + if (!$isAutoUpgradeMinorVersion) { #autoUpgradeMinorVersion doesn't exist in ExtensionConfig CRD if false + break + } + Start-Sleep -Seconds 10 + $n += 1 + } while ($n -le $MAX_RETRY_ATTEMPTS) + $n | Should -BeLessOrEqual $MAX_RETRY_ATTEMPTS + } + + It "Lists the extensions on the cluster" { + $output = az $Env:K8sExtensionName list -c $($ENVCONFIG.arcClusterName) -g $($ENVCONFIG.resourceGroup) --cluster-type connectedClusters + $? | 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 connectedClusters -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 connectedClusters -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 connectedClusters + $? | Should -BeTrue + $output | Should -Not -BeNullOrEmpty + + $extensionExists = $output | ConvertFrom-Json | Where-Object { $_.extensionType -eq $extensionName } + $extensionExists | Should -BeNullOrEmpty + } +} diff --git a/testing/test/extensions/public/OpenServiceMesh.Tests.ps1 b/testing/test/extensions/public/OpenServiceMesh.Tests.ps1 new file mode 100644 index 00000000000..75756a8ad1f --- /dev/null +++ b/testing/test/extensions/public/OpenServiceMesh.Tests.ps1 @@ -0,0 +1,72 @@ +Describe 'Azure OpenServiceMesh Testing' { + BeforeAll { + $extensionType = "microsoft.openservicemesh" + $extensionName = "openservicemesh" + $extensionVersion = "1.0.0" + $extensionAgentName = "osm-controller" + $extensionAgentNamespace = "arc-osm-system" + $releaseTrain = "pilot" + + . $PSScriptRoot/../../helper/Constants.ps1 + . $PSScriptRoot/../../helper/Helper.ps1 + } + + # Should Not BeNullOrEmpty checks if the command returns JSON output + + It 'Creates the extension and checks that it onboards correctly' { + az $Env:K8sExtensionName create -c $($ENVCONFIG.arcClusterName) -g $($ENVCONFIG.resourceGroup) --cluster-type connectedClusters --extension-type $extensionType -n $extensionName --release-train $releaseTrain --version $extensionVersion --no-wait + $? | Should -BeTrue + + $output = az $Env:K8sExtensionName show -c $($ENVCONFIG.arcClusterName) -g $($ENVCONFIG.resourceGroup) --cluster-type connectedClusters -n $extensionName + $? | Should -BeTrue + + $isAutoUpgradeMinorVersion = ($output | ConvertFrom-Json).autoUpgradeMinorVersion + $isAutoUpgradeMinorVersion.ToString() -eq "False" | Should -BeTrue + + # Loop and retry until the extension installs + $n = 0 + do + { + if (Has-ExtensionData $extensionName) { + break + } + Start-Sleep -Seconds 10 + $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 connectedClusters -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 connectedClusters + $? | 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 connectedClusters -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 connectedClusters -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 connectedClusters + $? | Should -BeTrue + $output | Should -Not -BeNullOrEmpty + + $extensionExists = $output | ConvertFrom-Json | Where-Object { $_.extensionType -eq $extensionName } + $extensionExists | Should -BeNullOrEmpty + } +} diff --git a/testing/test/helper/Constants.ps1 b/testing/test/helper/Constants.ps1 new file mode 100644 index 00000000000..3ecd3621bc5 --- /dev/null +++ b/testing/test/helper/Constants.ps1 @@ -0,0 +1,7 @@ +$ENVCONFIG = Get-Content -Path $PSScriptRoot/../../settings.json | ConvertFrom-Json +$SUCCESS_MESSAGE = "Successfully installed the extension" +$FAILED_MESSAGE = "Failed to install the extension" + +$POD_RUNNING = "Running" + +$MAX_RETRY_ATTEMPTS = 10 \ No newline at end of file diff --git a/testing/test/helper/Helper.ps1 b/testing/test/helper/Helper.ps1 new file mode 100644 index 00000000000..4ff949e7ab4 --- /dev/null +++ b/testing/test/helper/Helper.ps1 @@ -0,0 +1,72 @@ +function Get-ExtensionData { + param( + [string]$extensionName + ) + + $output = kubectl get extensionconfigs -A -o json | ConvertFrom-Json + return $output.items | Where-Object { $_.metadata.name -eq $extensionName } +} + +function Has-ExtensionData { + param( + [string]$extensionName + ) + $extensionData = Get-ExtensionData $extensionName + if ($extensionData) { + return $true + } + return $false +} + + +function Has-Identity-Provisioned { + $output = kubectl get azureclusteridentityrequests -n azure-arc container-insights-clusteridentityrequest -o json | ConvertFrom-Json + return ($null -ne $output.status.expirationTime) -and ($null -ne $output.status.tokenReference.dataName) -and ($null -ne $output.status.tokenReference.secretName) +} + +function Get-ExtensionStatus { + param( + [string]$extensionName + ) + + $extensionData = Get-ExtensionData $extensionName + if ($extensionData) { + return $extensionData.status.status + } + return $null +} + +function Get-PodStatus { + param( + [string]$podName, + [string]$Namespace + ) + + $allPodData = kubectl get pods -n $Namespace -o json | ConvertFrom-Json + $podData = $allPodData.items | Where-Object { $_.metadata.name -Match $podName } + if ($podData.Length -gt 1) { + return $podData[0].status.phase + } + return $podData.status.phase +} + +function Get-ExtensionConfigurationSettings { + param( + [string]$extensionName, + [string]$configKey + ) + + $extensionData = Get-ExtensionData $extensionName + if ($extensionData) { + return $extensionData.spec.parameter."$configKey" + } + return $null +} + +function Check-Error { + param( + [string]$output + ) + $hasError = $output -CMatch "ERROR" + return $hasError +} \ No newline at end of file From 0a6208d9642891eb9013a9ac701d2e91bcbb0bd2 Mon Sep 17 00:00:00 2001 From: Deeksha Sharma Date: Thu, 29 Sep 2022 14:21:25 -0700 Subject: [PATCH 04/16] adding the api version to the operation definition in the client factory --- .../azext_k8s_extension/_client_factory.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/k8s-extension/azext_k8s_extension/_client_factory.py b/src/k8s-extension/azext_k8s_extension/_client_factory.py index 0ecf3a7ee15..36e450bf244 100644 --- a/src/k8s-extension/azext_k8s_extension/_client_factory.py +++ b/src/k8s-extension/azext_k8s_extension/_client_factory.py @@ -13,24 +13,24 @@ def cf_k8s_extension(cli_ctx, **kwargs): return get_mgmt_service_client(cli_ctx, SourceControlConfigurationClient, **kwargs) -def cf_k8s_extension_operation(cli_ctx, _): +def cf_k8s_extension_operation(cli_ctx, *_): return cf_k8s_extension(cli_ctx).extensions -def cf_k8s_cluster_extension_types_operation(cli_ctx, _): - return cf_k8s_extension(cli_ctx).cluster_extension_types +def cf_k8s_cluster_extension_types_operation(cli_ctx, *_): + return cf_k8s_extension(cli_ctx, api_version=consts.EXTENSION_TYPE_API_VERSION).cluster_extension_types -def cf_k8s_cluster_extension_type_operation(cli_ctx, _): - return cf_k8s_extension(cli_ctx, consts.EXTENSION_TYPE_API_VERSION).cluster_extension_type +def cf_k8s_cluster_extension_type_operation(cli_ctx, *_): + return cf_k8s_extension(cli_ctx, api_version=consts.EXTENSION_TYPE_API_VERSION).cluster_extension_type -def cf_k8s_location_extension_types_operation(cli_ctx, _): - return cf_k8s_extension(cli_ctx, consts.EXTENSION_TYPE_API_VERSION).location_extension_types +def cf_k8s_location_extension_types_operation(cli_ctx, *_): + return cf_k8s_extension(cli_ctx, api_version=consts.EXTENSION_TYPE_API_VERSION).location_extension_types -def cf_k8s_extension_type_versions_operation(cli_ctx, _): - return cf_k8s_extension(cli_ctx, consts.EXTENSION_TYPE_API_VERSION).extension_type_versions +def cf_k8s_extension_type_versions_operation(cli_ctx, *_): + return cf_k8s_extension(cli_ctx, api_version=consts.EXTENSION_TYPE_API_VERSION).extension_type_versions def cf_resource_groups(cli_ctx, subscription_id=None): From 9c6835c6fbc58cec1318e7d4390eff3eb89a2904 Mon Sep 17 00:00:00 2001 From: Deeksha Sharma Date: Thu, 29 Sep 2022 14:55:20 -0700 Subject: [PATCH 05/16] bump k8s-extension version to 1.3.6 --- src/k8s-extension/HISTORY.rst | 7 +++++++ src/k8s-extension/setup.py | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/k8s-extension/HISTORY.rst b/src/k8s-extension/HISTORY.rst index a9a7bd52f7f..d220fc47465 100644 --- a/src/k8s-extension/HISTORY.rst +++ b/src/k8s-extension/HISTORY.rst @@ -2,6 +2,13 @@ Release History =============== + +1.3.6 +++++++++++++++++++ +* k8s-extension fix to address the error TypeError: cf_k8s_extension() takes 1 positional argument but 2 were given while running all az k8s-extension extension-types commands +* microsoft.azuremonitor.containers: Update DCR creation to Clusters resource group instead of workspace +* microsoft.dataprotection.kubernetes: Authoring a new k8s partner extension for the BCDR solution of AKS clusters + 1.3.5 ++++++++++++++++++ * Use the api-version 2022-04-02-preview in the CLI command az k8s-extension extension-types list diff --git a/src/k8s-extension/setup.py b/src/k8s-extension/setup.py index eb57c01de8a..4975bf63bd3 100644 --- a/src/k8s-extension/setup.py +++ b/src/k8s-extension/setup.py @@ -33,7 +33,7 @@ # TODO: Add any additional SDK dependencies here DEPENDENCIES = [] -VERSION = "1.3.5" +VERSION = "1.3.6" with open("README.rst", "r", encoding="utf-8") as f: README = f.read() From 700bee5d75ab4dc18f4e4f9f361ccb465aa9066a Mon Sep 17 00:00:00 2001 From: Deeksha Sharma Date: Fri, 30 Sep 2022 14:00:03 -0700 Subject: [PATCH 06/16] adding tests for all 4 extension types calls --- .../public/ExtensionTypes.Tests.ps1 | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 testing/test/extensions/public/ExtensionTypes.Tests.ps1 diff --git a/testing/test/extensions/public/ExtensionTypes.Tests.ps1 b/testing/test/extensions/public/ExtensionTypes.Tests.ps1 new file mode 100644 index 00000000000..d9634024432 --- /dev/null +++ b/testing/test/extensions/public/ExtensionTypes.Tests.ps1 @@ -0,0 +1,33 @@ +Describe 'Extension Types Testing' { + BeforeAll { + $extensionType = "cassandradatacentersoperator" + $location = "eastus2euap" + + . $PSScriptRoot/../../helper/Constants.ps1 + . $PSScriptRoot/../../helper/Helper.ps1 + } + + It 'Performs a show extension types call' { + $output = az $Env:K8sExtensionName extension-types show -c $($ENVCONFIG.arcClusterName) -g $($ENVCONFIG.resourceGroup) --cluster-type connectedClusters --extension-type $extensionType + $? | Should -BeTrue + $output | Should -Not -BeNullOrEmpty + } + + It "Performs a cluster-scoped list extension types call" { + $output = az $Env:K8sExtensionName extension-types list -c $($ENVCONFIG.arcClusterName) -g $($ENVCONFIG.resourceGroup) --cluster-type connectedClusters + $? | Should -BeTrue + $output | Should -Not -BeNullOrEmpty + } + + It "Performs a location-scoped list extension types call" { + $output = az $Env:K8sExtensionName extension-types list-by-location --location $location + $? | Should -BeTrue + $output | Should -Not -BeNullOrEmpty + } + + It "Performs a location-scoped list extension type versions call" { + $output = az $Env:K8sExtensionName extension-types list-versions --location $location --extension-type $extensionType + $? | Should -BeTrue + $output | Should -Not -BeNullOrEmpty + } +} From efd86d4e1497b9edf125f469760e143d21243db8 Mon Sep 17 00:00:00 2001 From: Deeksha Sharma Date: Fri, 30 Sep 2022 14:05:31 -0700 Subject: [PATCH 07/16] adding to test config file --- testing/pipeline/k8s-custom-pipelines.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/testing/pipeline/k8s-custom-pipelines.yml b/testing/pipeline/k8s-custom-pipelines.yml index 0592b58ec53..ef1f5aa8577 100644 --- a/testing/pipeline/k8s-custom-pipelines.yml +++ b/testing/pipeline/k8s-custom-pipelines.yml @@ -41,6 +41,10 @@ stages: parameters: jobName: Cassandra path: ./test/extensions/public/Cassandra.Tests.ps1 + - template: ./templates/run-test.yml + parameters: + jobName: ExtensionTypes + path: ./test/extensions/public/ExtensionTypes.Tests.ps1 - template: ./templates/run-test.yml parameters: jobName: OpenServiceMesh From 8dcef494a72158ddb4ab79a60fa60bb18ed412f7 Mon Sep 17 00:00:00 2001 From: Deeksha Sharma Date: Fri, 30 Sep 2022 16:32:37 -0700 Subject: [PATCH 08/16] updating the api version for extension types to be the correct version expected by the service --- src/k8s-extension/azext_k8s_extension/consts.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/k8s-extension/azext_k8s_extension/consts.py b/src/k8s-extension/azext_k8s_extension/consts.py index 2044f06bf4d..c69df7a73f0 100644 --- a/src/k8s-extension/azext_k8s_extension/consts.py +++ b/src/k8s-extension/azext_k8s_extension/consts.py @@ -25,4 +25,4 @@ APPLIANCE_API_VERSION = "2021-10-31-preview" HYBRIDCONTAINERSERVICE_API_VERSION = "2022-05-01-preview" -EXTENSION_TYPE_API_VERSION = "2022-04-02-preview" +EXTENSION_TYPE_API_VERSION = "2022-01-15-preview" From 2ecb63af6e647d1c60f96f2f209a464a3ee07274 Mon Sep 17 00:00:00 2001 From: Bavneet Singh <33008256+bavneetsingh16@users.noreply.github.com> Date: Mon, 3 Oct 2022 11:57:17 -0700 Subject: [PATCH 09/16] add test case for flux extension (#184) --- testing/pipeline/k8s-custom-pipelines.yml | 4 ++ testing/test/extensions/public/Flux.Tests.ps1 | 63 +++++++++++++++++++ 2 files changed, 67 insertions(+) create mode 100644 testing/test/extensions/public/Flux.Tests.ps1 diff --git a/testing/pipeline/k8s-custom-pipelines.yml b/testing/pipeline/k8s-custom-pipelines.yml index 0592b58ec53..4d87a15224d 100644 --- a/testing/pipeline/k8s-custom-pipelines.yml +++ b/testing/pipeline/k8s-custom-pipelines.yml @@ -45,6 +45,10 @@ stages: parameters: jobName: OpenServiceMesh path: ./test/extensions/public/OpenServiceMesh.Tests.ps1 + - template: ./templates/run-test.yml + parameters: + jobName: Flux + path: ./test/extensions/public/Flux.Tests.ps1 - job: BuildPublishExtension pool: vmImage: 'ubuntu-latest' diff --git a/testing/test/extensions/public/Flux.Tests.ps1 b/testing/test/extensions/public/Flux.Tests.ps1 new file mode 100644 index 00000000000..7faa6aa9c70 --- /dev/null +++ b/testing/test/extensions/public/Flux.Tests.ps1 @@ -0,0 +1,63 @@ +Describe 'Azure Flux Testing' { + BeforeAll { + $extensionType = "microsoft.flux" + $extensionName = "flux" + $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 --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 From 4ae3aa69c89584faffdfeac65730a904f9972b2a Mon Sep 17 00:00:00 2001 From: Deeksha Sharma Date: Wed, 5 Oct 2022 16:39:26 -0700 Subject: [PATCH 10/16] bump k8s-extension version to 1.3.6 --- src/k8s-extension/HISTORY.rst | 2 ++ src/k8s-extension/setup.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/k8s-extension/HISTORY.rst b/src/k8s-extension/HISTORY.rst index d220fc47465..1de8cc95b23 100644 --- a/src/k8s-extension/HISTORY.rst +++ b/src/k8s-extension/HISTORY.rst @@ -5,6 +5,8 @@ Release History 1.3.6 ++++++++++++++++++ +* k8s-extension updating the api version for extension type calls +* k8s-extension adding tests for all extension type calls * k8s-extension fix to address the error TypeError: cf_k8s_extension() takes 1 positional argument but 2 were given while running all az k8s-extension extension-types commands * microsoft.azuremonitor.containers: Update DCR creation to Clusters resource group instead of workspace * microsoft.dataprotection.kubernetes: Authoring a new k8s partner extension for the BCDR solution of AKS clusters diff --git a/src/k8s-extension/setup.py b/src/k8s-extension/setup.py index 4975bf63bd3..b0f1d82c1fd 100644 --- a/src/k8s-extension/setup.py +++ b/src/k8s-extension/setup.py @@ -33,7 +33,7 @@ # TODO: Add any additional SDK dependencies here DEPENDENCIES = [] -VERSION = "1.3.6" +VERSION = "1.3.7" with open("README.rst", "r", encoding="utf-8") as f: README = f.read() From 0213a712f264941cdc6c157e20796b7a76263592 Mon Sep 17 00:00:00 2001 From: Deeksha Sharma Date: Thu, 6 Oct 2022 10:15:10 -0700 Subject: [PATCH 11/16] bump k8s-extension version to 1.3.6 --- src/k8s-extension/setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/k8s-extension/setup.py b/src/k8s-extension/setup.py index b0f1d82c1fd..4975bf63bd3 100644 --- a/src/k8s-extension/setup.py +++ b/src/k8s-extension/setup.py @@ -33,7 +33,7 @@ # TODO: Add any additional SDK dependencies here DEPENDENCIES = [] -VERSION = "1.3.7" +VERSION = "1.3.6" with open("README.rst", "r", encoding="utf-8") as f: README = f.read() From ef3e79f56edf3054fd1b32c97ab7bc87d121f126 Mon Sep 17 00:00:00 2001 From: Deeksha Sharma Date: Mon, 17 Oct 2022 12:15:28 -0700 Subject: [PATCH 12/16] adding upstream test for extension types --- .../recordings/test_k8s_extension_types.yaml | 290 ++++++++++++++++++ .../test_k8s_extension_types_scenario.py | 40 +++ 2 files changed, 330 insertions(+) create mode 100644 src/k8s-extension/azext_k8s_extension/tests/latest/recordings/test_k8s_extension_types.yaml create mode 100644 src/k8s-extension/azext_k8s_extension/tests/latest/test_k8s_extension_types_scenario.py diff --git a/src/k8s-extension/azext_k8s_extension/tests/latest/recordings/test_k8s_extension_types.yaml b/src/k8s-extension/azext_k8s_extension/tests/latest/recordings/test_k8s_extension_types.yaml new file mode 100644 index 00000000000..a1d59fe92da --- /dev/null +++ b/src/k8s-extension/azext_k8s_extension/tests/latest/recordings/test_k8s_extension_types.yaml @@ -0,0 +1,290 @@ +interactions: +- request: + body: null + headers: + Accept: + - application/json + Accept-Encoding: + - gzip, deflate + CommandName: + - k8s-extension extension-types show + Connection: + - keep-alive + ParameterSetName: + - -g -c --cluster-type --extension-type + User-Agent: + - AZURECLI/2.40.0 (PIP) azsdk-python-azure-mgmt-kubernetesconfiguration/2.0.0 + Python/3.10.0 (Windows-10-10.0.22621-SP0) + method: GET + uri: https://management.azure.com/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest-rg/providers/Microsoft.Kubernetes/connectedClusters/kind-clitest-cluster/providers/Microsoft.KubernetesConfiguration/extensionTypes/cassandradatacentersoperator?api-version=2022-01-15-preview + response: + body: + string: '{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest-rg/providers/Microsoft.Kubernetes/connectedClusters/kind-clitest-cluster/providers/Microsoft.KubernetesConfiguration/extensionTypes/cassandradatacentersoperator","name":"cassandradatacentersoperator","type":"Microsoft.KubernetesConfiguration/extensionTypes","properties":{"supportedScopes":{"defaultScope":"namespace","clusterScopeSettings":null},"releaseTrains":["stable"],"clusterTypes":["managedclusters","appliances"]}}' + headers: + api-supported-versions: + - 2021-05-01-preview, 2022-01-15-preview + cache-control: + - no-cache + content-length: + - '505' + content-type: + - application/json; charset=utf-8 + date: + - Mon, 17 Oct 2022 19:14:48 GMT + expires: + - '-1' + pragma: + - no-cache + strict-transport-security: + - max-age=31536000; includeSubDomains + transfer-encoding: + - chunked + vary: + - Accept-Encoding + x-content-type-options: + - nosniff + status: + code: 200 + message: OK +- request: + body: null + headers: + Accept: + - application/json + Accept-Encoding: + - gzip, deflate + CommandName: + - k8s-extension extension-types list + Connection: + - keep-alive + ParameterSetName: + - -g -c --cluster-type + User-Agent: + - AZURECLI/2.40.0 (PIP) azsdk-python-azure-mgmt-kubernetesconfiguration/2.0.0 + Python/3.10.0 (Windows-10-10.0.22621-SP0) + method: GET + uri: https://management.azure.com/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest-rg/providers/Microsoft.Kubernetes/connectedClusters/kind-clitest-cluster/providers/Microsoft.KubernetesConfiguration/extensionTypes?api-version=2022-01-15-preview + response: + body: + string: '{"value":[{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest-rg/providers/Microsoft.Kubernetes/connectedClusters/kind-clitest-cluster/providers/Microsoft.KubernetesConfiguration/extensionTypes/azuremonitor-containers","name":"azuremonitor-containers","type":"Microsoft.KubernetesConfiguration/extensionTypes","properties":{"supportedScopes":{"defaultScope":"cluster","clusterScopeSettings":{"allowMultipleInstances":false,"defaultReleaseNamespace":"kube-system"}},"releaseTrains":[],"clusterTypes":[]}},{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest-rg/providers/Microsoft.Kubernetes/connectedClusters/kind-clitest-cluster/providers/Microsoft.KubernetesConfiguration/extensionTypes/microsoft.policy","name":"microsoft.policy","type":"Microsoft.KubernetesConfiguration/extensionTypes","properties":{"supportedScopes":{"defaultScope":"cluster","clusterScopeSettings":{"allowMultipleInstances":false,"defaultReleaseNamespace":"kube-system"}},"releaseTrains":[],"clusterTypes":[]}},{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest-rg/providers/Microsoft.Kubernetes/connectedClusters/kind-clitest-cluster/providers/Microsoft.KubernetesConfiguration/extensionTypes/microsoft.openservicemesh","name":"microsoft.openservicemesh","type":"Microsoft.KubernetesConfiguration/extensionTypes","properties":{"supportedScopes":{"defaultScope":"cluster","clusterScopeSettings":{"allowMultipleInstances":false,"defaultReleaseNamespace":"arc-osm-system"}},"releaseTrains":[],"clusterTypes":["connectedclusters","provisionedclusters"]}},{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest-rg/providers/Microsoft.Kubernetes/connectedClusters/kind-clitest-cluster/providers/Microsoft.KubernetesConfiguration/extensionTypes/cassandradatacentersoperator","name":"cassandradatacentersoperator","type":"Microsoft.KubernetesConfiguration/extensionTypes","properties":{"supportedScopes":{"defaultScope":"namespace","clusterScopeSettings":null},"releaseTrains":[],"clusterTypes":["managedclusters","appliances"]}},{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest-rg/providers/Microsoft.Kubernetes/connectedClusters/kind-clitest-cluster/providers/Microsoft.KubernetesConfiguration/extensionTypes/microsoft.apimanagement.gateway","name":"microsoft.apimanagement.gateway","type":"Microsoft.KubernetesConfiguration/extensionTypes","properties":{"supportedScopes":{"defaultScope":"cluster","clusterScopeSettings":{"allowMultipleInstances":false,"defaultReleaseNamespace":"gateway"}},"releaseTrains":[],"clusterTypes":[]}},{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest-rg/providers/Microsoft.Kubernetes/connectedClusters/kind-clitest-cluster/providers/Microsoft.KubernetesConfiguration/extensionTypes/microsoft.web.appservice","name":"microsoft.web.appservice","type":"Microsoft.KubernetesConfiguration/extensionTypes","properties":{"supportedScopes":{"defaultScope":"cluster","clusterScopeSettings":{"allowMultipleInstances":true,"defaultReleaseNamespace":"appservice"}},"releaseTrains":[],"clusterTypes":[]}},{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest-rg/providers/Microsoft.Kubernetes/connectedClusters/kind-clitest-cluster/providers/Microsoft.KubernetesConfiguration/extensionTypes/ansibletoweroperator","name":"ansibletoweroperator","type":"Microsoft.KubernetesConfiguration/extensionTypes","properties":{"supportedScopes":{"defaultScope":"cluster","clusterScopeSettings":{"allowMultipleInstances":true,"defaultReleaseNamespace":"awx-operator"}},"releaseTrains":[],"clusterTypes":[]}},{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest-rg/providers/Microsoft.Kubernetes/connectedClusters/kind-clitest-cluster/providers/Microsoft.KubernetesConfiguration/extensionTypes/microsoft.azuredefender.kubernetes","name":"microsoft.azuredefender.kubernetes","type":"Microsoft.KubernetesConfiguration/extensionTypes","properties":{"supportedScopes":{"defaultScope":"cluster","clusterScopeSettings":{"allowMultipleInstances":false,"defaultReleaseNamespace":"mdc"}},"releaseTrains":[],"clusterTypes":["connectedclusters","provisionedclusters"]}},{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest-rg/providers/Microsoft.Kubernetes/connectedClusters/kind-clitest-cluster/providers/Microsoft.KubernetesConfiguration/extensionTypes/microsoft.eventgrid","name":"microsoft.eventgrid","type":"Microsoft.KubernetesConfiguration/extensionTypes","properties":{"supportedScopes":{"defaultScope":"cluster","clusterScopeSettings":{"allowMultipleInstances":false,"defaultReleaseNamespace":"eventgrid-system"}},"releaseTrains":[],"clusterTypes":[]}},{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest-rg/providers/Microsoft.Kubernetes/connectedClusters/kind-clitest-cluster/providers/Microsoft.KubernetesConfiguration/extensionTypes/microsoft.azureml.kubernetes","name":"microsoft.azureml.kubernetes","type":"Microsoft.KubernetesConfiguration/extensionTypes","properties":{"supportedScopes":{"defaultScope":"namespace","clusterScopeSettings":{"allowMultipleInstances":false,"defaultReleaseNamespace":"azure-ml"}},"releaseTrains":[],"clusterTypes":["managedclusters"]}},{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest-rg/providers/Microsoft.Kubernetes/connectedClusters/kind-clitest-cluster/providers/Microsoft.KubernetesConfiguration/extensionTypes/dapr","name":"dapr","type":"Microsoft.KubernetesConfiguration/extensionTypes","properties":{"supportedScopes":{"defaultScope":"cluster","clusterScopeSettings":{"allowMultipleInstances":false,"defaultReleaseNamespace":"dapr-system"}},"releaseTrains":[],"clusterTypes":["managedclusters"]}},{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest-rg/providers/Microsoft.Kubernetes/connectedClusters/kind-clitest-cluster/providers/Microsoft.KubernetesConfiguration/extensionTypes/microsoft.azurenw.mobilenetwork","name":"microsoft.azurenw.mobilenetwork","type":"Microsoft.KubernetesConfiguration/extensionTypes","properties":{"supportedScopes":{"defaultScope":"namespace","clusterScopeSettings":{"allowMultipleInstances":false,"defaultReleaseNamespace":"azurenw-mn"}},"releaseTrains":[],"clusterTypes":[]}},{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest-rg/providers/Microsoft.Kubernetes/connectedClusters/kind-clitest-cluster/providers/Microsoft.KubernetesConfiguration/extensionTypes/microsoft.arcdataservices","name":"microsoft.arcdataservices","type":"Microsoft.KubernetesConfiguration/extensionTypes","properties":{"supportedScopes":{"defaultScope":"cluster","clusterScopeSettings":{"allowMultipleInstances":false,"defaultReleaseNamespace":"arc"}},"releaseTrains":[],"clusterTypes":["connectedclusters"]}},{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest-rg/providers/Microsoft.Kubernetes/connectedClusters/kind-clitest-cluster/providers/Microsoft.KubernetesConfiguration/extensionTypes/microsoft.scvmm","name":"microsoft.scvmm","type":"Microsoft.KubernetesConfiguration/extensionTypes","properties":{"supportedScopes":{"defaultScope":"cluster","clusterScopeSettings":{"allowMultipleInstances":false,"defaultReleaseNamespace":"azure-vmmoperator"}},"releaseTrains":[],"clusterTypes":["ConnectedClusters","Appliances"]}},{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest-rg/providers/Microsoft.Kubernetes/connectedClusters/kind-clitest-cluster/providers/Microsoft.KubernetesConfiguration/extensionTypes/microsoft.avs","name":"microsoft.avs","type":"Microsoft.KubernetesConfiguration/extensionTypes","properties":{"supportedScopes":{"defaultScope":"cluster","clusterScopeSettings":{"allowMultipleInstances":false,"defaultReleaseNamespace":"azure-avsoperator"}},"releaseTrains":[],"clusterTypes":["ConnectedClusters","Appliances"]}},{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest-rg/providers/Microsoft.Kubernetes/connectedClusters/kind-clitest-cluster/providers/Microsoft.KubernetesConfiguration/extensionTypes/microsoft.vmware","name":"microsoft.vmware","type":"Microsoft.KubernetesConfiguration/extensionTypes","properties":{"supportedScopes":{"defaultScope":"cluster","clusterScopeSettings":{"allowMultipleInstances":false,"defaultReleaseNamespace":"azure-vmwareoperator"}},"releaseTrains":[],"clusterTypes":["ConnectedClusters","Appliances"]}},{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest-rg/providers/Microsoft.Kubernetes/connectedClusters/kind-clitest-cluster/providers/Microsoft.KubernetesConfiguration/extensionTypes/microsoft.azstackhci.operator","name":"microsoft.azstackhci.operator","type":"Microsoft.KubernetesConfiguration/extensionTypes","properties":{"supportedScopes":{"defaultScope":"cluster","clusterScopeSettings":{"allowMultipleInstances":false,"defaultReleaseNamespace":"azstackhci-operator"}},"releaseTrains":[],"clusterTypes":["Appliances"]}},{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest-rg/providers/Microsoft.Kubernetes/connectedClusters/kind-clitest-cluster/providers/Microsoft.KubernetesConfiguration/extensionTypes/microsoft.azurenw.networkfunction","name":"microsoft.azurenw.networkfunction","type":"Microsoft.KubernetesConfiguration/extensionTypes","properties":{"supportedScopes":{"defaultScope":"namespace","clusterScopeSettings":{"allowMultipleInstances":false,"defaultReleaseNamespace":"azure-networkfunction"}},"releaseTrains":[],"clusterTypes":[]}},{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest-rg/providers/Microsoft.Kubernetes/connectedClusters/kind-clitest-cluster/providers/Microsoft.KubernetesConfiguration/extensionTypes/microsoft.azuremonitor.containers","name":"microsoft.azuremonitor.containers","type":"Microsoft.KubernetesConfiguration/extensionTypes","properties":{"supportedScopes":{"defaultScope":"cluster","clusterScopeSettings":{"allowMultipleInstances":false,"defaultReleaseNamespace":"kube-system"}},"releaseTrains":[],"clusterTypes":["ConnectedCluster","ProvisionedClusters"]}},{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest-rg/providers/Microsoft.Kubernetes/connectedClusters/kind-clitest-cluster/providers/Microsoft.KubernetesConfiguration/extensionTypes/microsoft.unitycloud.konductor","name":"microsoft.unitycloud.konductor","type":"Microsoft.KubernetesConfiguration/extensionTypes","properties":{"supportedScopes":{"defaultScope":"cluster","clusterScopeSettings":{"allowMultipleInstances":false,"defaultReleaseNamespace":"konductor"}},"releaseTrains":[],"clusterTypes":[]}},{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest-rg/providers/Microsoft.Kubernetes/connectedClusters/kind-clitest-cluster/providers/Microsoft.KubernetesConfiguration/extensionTypes/microsoft.policyinsights","name":"microsoft.policyinsights","type":"Microsoft.KubernetesConfiguration/extensionTypes","properties":{"supportedScopes":{"defaultScope":"cluster","clusterScopeSettings":{"allowMultipleInstances":false,"defaultReleaseNamespace":"kube-system"}},"releaseTrains":[],"clusterTypes":[]}},{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest-rg/providers/Microsoft.Kubernetes/connectedClusters/kind-clitest-cluster/providers/Microsoft.KubernetesConfiguration/extensionTypes/microsoft.cnab","name":"microsoft.cnab","type":"Microsoft.KubernetesConfiguration/extensionTypes","properties":{"supportedScopes":{"defaultScope":"cluster","clusterScopeSettings":{"allowMultipleInstances":false,"defaultReleaseNamespace":"cnab-operator"}},"releaseTrains":[],"clusterTypes":[]}},{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest-rg/providers/Microsoft.Kubernetes/connectedClusters/kind-clitest-cluster/providers/Microsoft.KubernetesConfiguration/extensionTypes/microsoft.azure.hybridnetwork","name":"microsoft.azure.hybridnetwork","type":"Microsoft.KubernetesConfiguration/extensionTypes","properties":{"supportedScopes":{"defaultScope":"namespace","clusterScopeSettings":{"allowMultipleInstances":false,"defaultReleaseNamespace":"azurehybridnetwork"}},"releaseTrains":[],"clusterTypes":["appliances","provisionedclusters","connectedclusters"]}},{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest-rg/providers/Microsoft.Kubernetes/connectedClusters/kind-clitest-cluster/providers/Microsoft.KubernetesConfiguration/extensionTypes/microsoft.azurebackup.backupagent","name":"microsoft.azurebackup.backupagent","type":"Microsoft.KubernetesConfiguration/extensionTypes","properties":{"supportedScopes":{"defaultScope":"namespace","clusterScopeSettings":{"allowMultipleInstances":false,"defaultReleaseNamespace":"azurebackup"}},"releaseTrains":[],"clusterTypes":["Connectedclusters","Managedclusters","Appliances","ProvisionedClusters"]}},{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest-rg/providers/Microsoft.Kubernetes/connectedClusters/kind-clitest-cluster/providers/Microsoft.KubernetesConfiguration/extensionTypes/microsoft.aksedgeoperator","name":"microsoft.aksedgeoperator","type":"Microsoft.KubernetesConfiguration/extensionTypes","properties":{"supportedScopes":{"defaultScope":"cluster","clusterScopeSettings":{"allowMultipleInstances":false,"defaultReleaseNamespace":"aksedge-operator-system"}},"releaseTrains":[],"clusterTypes":[]}},{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest-rg/providers/Microsoft.Kubernetes/connectedClusters/kind-clitest-cluster/providers/Microsoft.KubernetesConfiguration/extensionTypes/microsoft.flux","name":"microsoft.flux","type":"Microsoft.KubernetesConfiguration/extensionTypes","properties":{"supportedScopes":{"defaultScope":"cluster","clusterScopeSettings":{"allowMultipleInstances":false,"defaultReleaseNamespace":"flux-system"}},"releaseTrains":[],"clusterTypes":["connectedclusters","managedclusters","provisionedclusters"]}},{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest-rg/providers/Microsoft.Kubernetes/connectedClusters/kind-clitest-cluster/providers/Microsoft.KubernetesConfiguration/extensionTypes/microsoft.azurebackup.dataprotectionplugin","name":"microsoft.azurebackup.dataprotectionplugin","type":"Microsoft.KubernetesConfiguration/extensionTypes","properties":{"supportedScopes":{"defaultScope":"namespace","clusterScopeSettings":null},"releaseTrains":[],"clusterTypes":["Connectedclusters","Managedclusters","Appliances","ProvisionedClusters"]}},{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest-rg/providers/Microsoft.Kubernetes/connectedClusters/kind-clitest-cluster/providers/Microsoft.KubernetesConfiguration/extensionTypes/microsoft.containerregistry.connectedregistry","name":"microsoft.containerregistry.connectedregistry","type":"Microsoft.KubernetesConfiguration/extensionTypes","properties":{"supportedScopes":{"defaultScope":"cluster","clusterScopeSettings":{"allowMultipleInstances":false,"defaultReleaseNamespace":"connected-registry"}},"releaseTrains":[],"clusterTypes":[]}},{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest-rg/providers/Microsoft.Kubernetes/connectedClusters/kind-clitest-cluster/providers/Microsoft.KubernetesConfiguration/extensionTypes/microsoft.dapr","name":"microsoft.dapr","type":"Microsoft.KubernetesConfiguration/extensionTypes","properties":{"supportedScopes":{"defaultScope":"cluster","clusterScopeSettings":{"allowMultipleInstances":false,"defaultReleaseNamespace":"dapr-system"}},"releaseTrains":[],"clusterTypes":["connectedCluster","managedClusters","Appliances","ProvisionedClusters"]}},{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest-rg/providers/Microsoft.Kubernetes/connectedClusters/kind-clitest-cluster/providers/Microsoft.KubernetesConfiguration/extensionTypes/microsoft.hybridaksoperator","name":"microsoft.hybridaksoperator","type":"Microsoft.KubernetesConfiguration/extensionTypes","properties":{"supportedScopes":{"defaultScope":"cluster","clusterScopeSettings":{"allowMultipleInstances":false,"defaultReleaseNamespace":"hybridaks-operator-system"}},"releaseTrains":[],"clusterTypes":["Appliances"]}},{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest-rg/providers/Microsoft.Kubernetes/connectedClusters/kind-clitest-cluster/providers/Microsoft.KubernetesConfiguration/extensionTypes/microsoft.azurekeyvaultsecretsprovider","name":"microsoft.azurekeyvaultsecretsprovider","type":"Microsoft.KubernetesConfiguration/extensionTypes","properties":{"supportedScopes":{"defaultScope":"cluster","clusterScopeSettings":{"allowMultipleInstances":false,"defaultReleaseNamespace":"kube-system"}},"releaseTrains":[],"clusterTypes":["connectedclusters","provisionedclusters"]}},{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest-rg/providers/Microsoft.Kubernetes/connectedClusters/kind-clitest-cluster/providers/Microsoft.KubernetesConfiguration/extensionTypes/microsoft.testbonsaiextension","name":"microsoft.testbonsaiextension","type":"Microsoft.KubernetesConfiguration/extensionTypes","properties":{"supportedScopes":{"defaultScope":"cluster","clusterScopeSettings":{"allowMultipleInstances":true,"defaultReleaseNamespace":"kube-system"}},"releaseTrains":[],"clusterTypes":[]}},{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest-rg/providers/Microsoft.Kubernetes/connectedClusters/kind-clitest-cluster/providers/Microsoft.KubernetesConfiguration/extensionTypes/cassandradatacentersoperator1","name":"cassandradatacentersoperator1","type":"Microsoft.KubernetesConfiguration/extensionTypes","properties":{"supportedScopes":{"defaultScope":"namespace","clusterScopeSettings":null},"releaseTrains":[],"clusterTypes":["managedclusters","appliances"]}},{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest-rg/providers/Microsoft.Kubernetes/connectedClusters/kind-clitest-cluster/providers/Microsoft.KubernetesConfiguration/extensionTypes/microsoft.azurevote.previewstandard","name":"microsoft.azurevote.previewstandard","type":"Microsoft.KubernetesConfiguration/extensionTypes","properties":{"supportedScopes":{"defaultScope":"namespace","clusterScopeSettings":{"allowMultipleInstances":false,"defaultReleaseNamespace":"vote"}},"releaseTrains":[],"clusterTypes":["managedclusters"]}},{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest-rg/providers/Microsoft.Kubernetes/connectedClusters/kind-clitest-cluster/providers/Microsoft.KubernetesConfiguration/extensionTypes/microsoft.servicelinker.connection","name":"microsoft.servicelinker.connection","type":"Microsoft.KubernetesConfiguration/extensionTypes","properties":{"supportedScopes":{"defaultScope":"cluster","clusterScopeSettings":{"allowMultipleInstances":true,"defaultReleaseNamespace":"default"}},"releaseTrains":[],"clusterTypes":["managedclusters"]}},{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest-rg/providers/Microsoft.Kubernetes/connectedClusters/kind-clitest-cluster/providers/Microsoft.KubernetesConfiguration/extensionTypes/deislabs.akri","name":"deislabs.akri","type":"Microsoft.KubernetesConfiguration/extensionTypes","properties":{"supportedScopes":{"defaultScope":"cluster","clusterScopeSettings":{"allowMultipleInstances":false,"defaultReleaseNamespace":"akri"}},"releaseTrains":[],"clusterTypes":[]}},{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest-rg/providers/Microsoft.Kubernetes/connectedClusters/kind-clitest-cluster/providers/Microsoft.KubernetesConfiguration/extensionTypes/microsoft.arcextensionusage","name":"microsoft.arcextensionusage","type":"Microsoft.KubernetesConfiguration/extensionTypes","properties":{"supportedScopes":{"defaultScope":"cluster","clusterScopeSettings":{"allowMultipleInstances":false,"defaultReleaseNamespace":"arc-osm-system"}},"releaseTrains":[],"clusterTypes":["managedclusters","connectedclusters"]}},{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest-rg/providers/Microsoft.Kubernetes/connectedClusters/kind-clitest-cluster/providers/Microsoft.KubernetesConfiguration/extensionTypes/microsoft.azure.mobilenetwork.packetcoremonitor","name":"microsoft.azure.mobilenetwork.packetcoremonitor","type":"Microsoft.KubernetesConfiguration/extensionTypes","properties":{"supportedScopes":{"defaultScope":"cluster","clusterScopeSettings":{"allowMultipleInstances":false,"defaultReleaseNamespace":"packet-core-monitor"}},"releaseTrains":[],"clusterTypes":["Connectedclusters","Managedclusters","Appliances"]}},{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest-rg/providers/Microsoft.Kubernetes/connectedClusters/kind-clitest-cluster/providers/Microsoft.KubernetesConfiguration/extensionTypes/microsoft.extensionsusage","name":"microsoft.extensionsusage","type":"Microsoft.KubernetesConfiguration/extensionTypes","properties":{"supportedScopes":{"defaultScope":"cluster","clusterScopeSettings":{"allowMultipleInstances":false,"defaultReleaseNamespace":"azure-extensions-usage-system"}},"releaseTrains":[],"clusterTypes":["Connectedclusters","Managedclusters"]}},{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest-rg/providers/Microsoft.Kubernetes/connectedClusters/kind-clitest-cluster/providers/Microsoft.KubernetesConfiguration/extensionTypes/microsoft.connectedopenstack","name":"microsoft.connectedopenstack","type":"Microsoft.KubernetesConfiguration/extensionTypes","properties":{"supportedScopes":{"defaultScope":"cluster","clusterScopeSettings":{"allowMultipleInstances":false,"defaultReleaseNamespace":"connectedopenstack"}},"releaseTrains":[],"clusterTypes":["Appliances","connectedCluster"]}},{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest-rg/providers/Microsoft.Kubernetes/connectedClusters/kind-clitest-cluster/providers/Microsoft.KubernetesConfiguration/extensionTypes/microsoft.networkcloud.userrp","name":"microsoft.networkcloud.userrp","type":"Microsoft.KubernetesConfiguration/extensionTypes","properties":{"supportedScopes":{"defaultScope":"cluster","clusterScopeSettings":{"allowMultipleInstances":false,"defaultReleaseNamespace":"nc-rp"}},"releaseTrains":[],"clusterTypes":["Connectedclusters","Managedcluster"]}},{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest-rg/providers/Microsoft.Kubernetes/connectedClusters/kind-clitest-cluster/providers/Microsoft.KubernetesConfiguration/extensionTypes/microsoft.aziot.edge","name":"microsoft.aziot.edge","type":"Microsoft.KubernetesConfiguration/extensionTypes","properties":{"supportedScopes":{"defaultScope":"cluster","clusterScopeSettings":{"allowMultipleInstances":false,"defaultReleaseNamespace":"aziotedge-system"}},"releaseTrains":[],"clusterTypes":[]}},{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest-rg/providers/Microsoft.Kubernetes/connectedClusters/kind-clitest-cluster/providers/Microsoft.KubernetesConfiguration/extensionTypes/microsoft.appliance.management.operator","name":"microsoft.appliance.management.operator","type":"Microsoft.KubernetesConfiguration/extensionTypes","properties":{"supportedScopes":{"defaultScope":"cluster","clusterScopeSettings":{"allowMultipleInstances":false,"defaultReleaseNamespace":"kva-management"}},"releaseTrains":[],"clusterTypes":["Appliances"]}},{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest-rg/providers/Microsoft.Kubernetes/connectedClusters/kind-clitest-cluster/providers/Microsoft.KubernetesConfiguration/extensionTypes/microsoft.networkcloud.userrp.dev","name":"microsoft.networkcloud.userrp.dev","type":"Microsoft.KubernetesConfiguration/extensionTypes","properties":{"supportedScopes":{"defaultScope":"cluster","clusterScopeSettings":{"allowMultipleInstances":false,"defaultReleaseNamespace":"nc-rp"}},"releaseTrains":[],"clusterTypes":["Connectedclusters","Managedcluster"]}},{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest-rg/providers/Microsoft.Kubernetes/connectedClusters/kind-clitest-cluster/providers/Microsoft.KubernetesConfiguration/extensionTypes/microsoft.networkcloud.clustermanager.dev","name":"microsoft.networkcloud.clustermanager.dev","type":"Microsoft.KubernetesConfiguration/extensionTypes","properties":{"supportedScopes":{"defaultScope":"cluster","clusterScopeSettings":{"allowMultipleInstances":false,"defaultReleaseNamespace":"nc-cluster-manager-extension"}},"releaseTrains":[],"clusterTypes":["Connectedclusters","Managedclusters"]}},{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest-rg/providers/Microsoft.Kubernetes/connectedClusters/kind-clitest-cluster/providers/Microsoft.KubernetesConfiguration/extensionTypes/microsoft.networkcloud.platformcluster.dev","name":"microsoft.networkcloud.platformcluster.dev","type":"Microsoft.KubernetesConfiguration/extensionTypes","properties":{"supportedScopes":{"defaultScope":"cluster","clusterScopeSettings":{"allowMultipleInstances":false,"defaultReleaseNamespace":"nc-system"}},"releaseTrains":[],"clusterTypes":["Connectedclusters","Managedclusters"]}},{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest-rg/providers/Microsoft.Kubernetes/connectedClusters/kind-clitest-cluster/providers/Microsoft.KubernetesConfiguration/extensionTypes/microsoft.managednetworkfabric","name":"microsoft.managednetworkfabric","type":"Microsoft.KubernetesConfiguration/extensionTypes","properties":{"supportedScopes":{"defaultScope":"cluster","clusterScopeSettings":{"allowMultipleInstances":false,"defaultReleaseNamespace":"managednetworkfabric"}},"releaseTrains":[],"clusterTypes":[]}},{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest-rg/providers/Microsoft.Kubernetes/connectedClusters/kind-clitest-cluster/providers/Microsoft.KubernetesConfiguration/extensionTypes/ + microsoft.azurebackup.mockplugin","name":" microsoft.azurebackup.mockplugin","type":"Microsoft.KubernetesConfiguration/extensionTypes","properties":{"supportedScopes":{"defaultScope":" + namespace","clusterScopeSettings":{"allowMultipleInstances":false,"defaultReleaseNamespace":null}},"releaseTrains":[],"clusterTypes":["Connectedclusters","Managedclusters","Appliances","ProvisionedClusters"]}},{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest-rg/providers/Microsoft.Kubernetes/connectedClusters/kind-clitest-cluster/providers/Microsoft.KubernetesConfiguration/extensionTypes/microsoft.azurebackup.mockplugin","name":"microsoft.azurebackup.mockplugin","type":"Microsoft.KubernetesConfiguration/extensionTypes","properties":{"supportedScopes":{"defaultScope":" + namespace","clusterScopeSettings":{"allowMultipleInstances":false,"defaultReleaseNamespace":null}},"releaseTrains":[],"clusterTypes":["Connectedclusters","Managedclusters","Appliances","ProvisionedClusters"]}},{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest-rg/providers/Microsoft.Kubernetes/connectedClusters/kind-clitest-cluster/providers/Microsoft.KubernetesConfiguration/extensionTypes/microsoft.networkcloud.userrp.staging","name":"microsoft.networkcloud.userrp.staging","type":"Microsoft.KubernetesConfiguration/extensionTypes","properties":{"supportedScopes":{"defaultScope":"cluster","clusterScopeSettings":{"allowMultipleInstances":false,"defaultReleaseNamespace":"nc-rp"}},"releaseTrains":[],"clusterTypes":["Connectedclusters","Managedcluster"]}}],"nextLink":"https://management.azure.com/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest-rg/providers/Microsoft.Kubernetes/ConnectedClusters/kind-clitest-cluster/providers/Microsoft.KubernetesConfiguration/extensionTypes?api-version=2022-01-15-preview&continuationToken=JTVCJTdCJTIydG9rZW4lMjIlM0ElMjIlMkJSSUQlM0F%2Bc1VvbEFONFRzMHkxRHowQUFBQUFBQSUzRCUzRCUyM1JUJTNBMSUyM1RSQyUzQTUwJTIzSVNWJTNBMiUyM0lFTyUzQTY1NTUxJTIzUUNGJTNBOCUyMiUyQyUyMnJhbmdlJTIyJTNBJTdCJTIybWluJTIyJTNBJTIyJTIyJTJDJTIybWF4JTIyJTNBJTIyMDVDMURGRkZGRkZGRkMlMjIlN0QlN0QlNUQ%3D"}' + headers: + api-supported-versions: + - 2021-05-01-preview, 2022-01-15-preview + cache-control: + - no-cache + content-length: + - '28395' + content-type: + - application/json; charset=utf-8 + date: + - Mon, 17 Oct 2022 19:14:50 GMT + expires: + - '-1' + pragma: + - no-cache + strict-transport-security: + - max-age=31536000; includeSubDomains + transfer-encoding: + - chunked + vary: + - Accept-Encoding + x-content-type-options: + - nosniff + status: + code: 200 + message: OK +- request: + body: null + headers: + Accept: + - application/json + Accept-Encoding: + - gzip, deflate + CommandName: + - k8s-extension extension-types list + Connection: + - keep-alive + ParameterSetName: + - -g -c --cluster-type + User-Agent: + - AZURECLI/2.40.0 (PIP) azsdk-python-azure-mgmt-kubernetesconfiguration/2.0.0 + Python/3.10.0 (Windows-10-10.0.22621-SP0) + method: GET + uri: https://management.azure.com/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest-rg/providers/Microsoft.Kubernetes/ConnectedClusters/kind-clitest-cluster/providers/Microsoft.KubernetesConfiguration/extensionTypes?api-version=2022-01-15-preview&continuationToken=JTVCJTdCJTIydG9rZW4lMjIlM0ElMjIlMkJSSUQlM0F%2Bc1VvbEFONFRzMHkxRHowQUFBQUFBQSUzRCUzRCUyM1JUJTNBMSUyM1RSQyUzQTUwJTIzSVNWJTNBMiUyM0lFTyUzQTY1NTUxJTIzUUNGJTNBOCUyMiUyQyUyMnJhbmdlJTIyJTNBJTdCJTIybWluJTIyJTNBJTIyJTIyJTJDJTIybWF4JTIyJTNBJTIyMDVDMURGRkZGRkZGRkMlMjIlN0QlN0QlNUQ%3D + response: + body: + string: '{"value":[{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest-rg/providers/Microsoft.Kubernetes/ConnectedClusters/kind-clitest-cluster/providers/Microsoft.KubernetesConfiguration/extensionTypes/microsoft.networkcloud.platformcluster.sandbox","name":"microsoft.networkcloud.platformcluster.sandbox","type":"Microsoft.KubernetesConfiguration/extensionTypes","properties":{"supportedScopes":{"defaultScope":"cluster","clusterScopeSettings":{"allowMultipleInstances":false,"defaultReleaseNamespace":"nc-system"}},"releaseTrains":[],"clusterTypes":["Connectedclusters","Managedclusters"]}},{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest-rg/providers/Microsoft.Kubernetes/ConnectedClusters/kind-clitest-cluster/providers/Microsoft.KubernetesConfiguration/extensionTypes/microsoft.networkcloud.userrp.test","name":"microsoft.networkcloud.userrp.test","type":"Microsoft.KubernetesConfiguration/extensionTypes","properties":{"supportedScopes":{"defaultScope":"cluster","clusterScopeSettings":{"allowMultipleInstances":false,"defaultReleaseNamespace":"nc-rp"}},"releaseTrains":[],"clusterTypes":["Connectedclusters","Managedcluster"]}},{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest-rg/providers/Microsoft.Kubernetes/ConnectedClusters/kind-clitest-cluster/providers/Microsoft.KubernetesConfiguration/extensionTypes/microsoft.networkcloud.platformcluster.test","name":"microsoft.networkcloud.platformcluster.test","type":"Microsoft.KubernetesConfiguration/extensionTypes","properties":{"supportedScopes":{"defaultScope":"cluster","clusterScopeSettings":{"allowMultipleInstances":false,"defaultReleaseNamespace":"nc-system"}},"releaseTrains":[],"clusterTypes":["Connectedclusters","Managedclusters"]}},{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest-rg/providers/Microsoft.Kubernetes/ConnectedClusters/kind-clitest-cluster/providers/Microsoft.KubernetesConfiguration/extensionTypes/microsoft.networkcloud.clustermanager.test","name":"microsoft.networkcloud.clustermanager.test","type":"Microsoft.KubernetesConfiguration/extensionTypes","properties":{"supportedScopes":{"defaultScope":"cluster","clusterScopeSettings":{"allowMultipleInstances":false,"defaultReleaseNamespace":"nc-cluster-manager-extension"}},"releaseTrains":[],"clusterTypes":["Connectedclusters","Managedclusters"]}},{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest-rg/providers/Microsoft.Kubernetes/ConnectedClusters/kind-clitest-cluster/providers/Microsoft.KubernetesConfiguration/extensionTypes/microsoft.networkcloud.clustermanager.sandbox","name":"microsoft.networkcloud.clustermanager.sandbox","type":"Microsoft.KubernetesConfiguration/extensionTypes","properties":{"supportedScopes":{"defaultScope":"cluster","clusterScopeSettings":{"allowMultipleInstances":false,"defaultReleaseNamespace":"nc-system"}},"releaseTrains":[],"clusterTypes":["Connectedclusters","Managedclusters"]}},{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest-rg/providers/Microsoft.Kubernetes/ConnectedClusters/kind-clitest-cluster/providers/Microsoft.KubernetesConfiguration/extensionTypes/microsoft.networkcloud.clustermanager.staging","name":"microsoft.networkcloud.clustermanager.staging","type":"Microsoft.KubernetesConfiguration/extensionTypes","properties":{"supportedScopes":{"defaultScope":"cluster","clusterScopeSettings":{"allowMultipleInstances":false,"defaultReleaseNamespace":"nc-cluster-manager-extension"}},"releaseTrains":[],"clusterTypes":["Connectedclusters","Managedclusters"]}},{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest-rg/providers/Microsoft.Kubernetes/ConnectedClusters/kind-clitest-cluster/providers/Microsoft.KubernetesConfiguration/extensionTypes/microsoft.networkcloud.platformcluster.staging","name":"microsoft.networkcloud.platformcluster.staging","type":"Microsoft.KubernetesConfiguration/extensionTypes","properties":{"supportedScopes":{"defaultScope":"cluster","clusterScopeSettings":{"allowMultipleInstances":false,"defaultReleaseNamespace":"nc-system"}},"releaseTrains":[],"clusterTypes":["Connectedclusters","Managedclusters"]}},{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest-rg/providers/Microsoft.Kubernetes/ConnectedClusters/kind-clitest-cluster/providers/Microsoft.KubernetesConfiguration/extensionTypes/microsoft.az.edge.mqtt","name":"microsoft.az.edge.mqtt","type":"Microsoft.KubernetesConfiguration/extensionTypes","properties":{"supportedScopes":{"defaultScope":"cluster","clusterScopeSettings":{"allowMultipleInstances":false,"defaultReleaseNamespace":"azedge-system"}},"releaseTrains":[],"clusterTypes":[]}},{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest-rg/providers/Microsoft.Kubernetes/ConnectedClusters/kind-clitest-cluster/providers/Microsoft.KubernetesConfiguration/extensionTypes/microsoft.azedge.mqtt","name":"microsoft.azedge.mqtt","type":"Microsoft.KubernetesConfiguration/extensionTypes","properties":{"supportedScopes":{"defaultScope":"cluster","clusterScopeSettings":{"allowMultipleInstances":false,"defaultReleaseNamespace":"azedge-system"}},"releaseTrains":[],"clusterTypes":[]}},{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest-rg/providers/Microsoft.Kubernetes/ConnectedClusters/kind-clitest-cluster/providers/Microsoft.KubernetesConfiguration/extensionTypes/microsoft.networkcloud.platformcluster.euap","name":"microsoft.networkcloud.platformcluster.euap","type":"Microsoft.KubernetesConfiguration/extensionTypes","properties":{"supportedScopes":{"defaultScope":"cluster","clusterScopeSettings":{"allowMultipleInstances":false,"defaultReleaseNamespace":"nc-system"}},"releaseTrains":[],"clusterTypes":["Connectedclusters","Managedclusters"]}},{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest-rg/providers/Microsoft.Kubernetes/ConnectedClusters/kind-clitest-cluster/providers/Microsoft.KubernetesConfiguration/extensionTypes/microsoft.networkcloud.platformcluster.prod","name":"microsoft.networkcloud.platformcluster.prod","type":"Microsoft.KubernetesConfiguration/extensionTypes","properties":{"supportedScopes":{"defaultScope":"cluster","clusterScopeSettings":{"allowMultipleInstances":false,"defaultReleaseNamespace":"nc-system"}},"releaseTrains":[],"clusterTypes":["Connectedclusters","Managedclusters"]}},{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest-rg/providers/Microsoft.Kubernetes/ConnectedClusters/kind-clitest-cluster/providers/Microsoft.KubernetesConfiguration/extensionTypes/microsoft.networkcloud.clustermanager.prod","name":"microsoft.networkcloud.clustermanager.prod","type":"Microsoft.KubernetesConfiguration/extensionTypes","properties":{"supportedScopes":{"defaultScope":"cluster","clusterScopeSettings":{"allowMultipleInstances":false,"defaultReleaseNamespace":"nc-cluster-manager-extension"}},"releaseTrains":[],"clusterTypes":["Connectedclusters","Managedclusters"]}},{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest-rg/providers/Microsoft.Kubernetes/ConnectedClusters/kind-clitest-cluster/providers/Microsoft.KubernetesConfiguration/extensionTypes/microsoft.networkcloud.clustermanager.euap","name":"microsoft.networkcloud.clustermanager.euap","type":"Microsoft.KubernetesConfiguration/extensionTypes","properties":{"supportedScopes":{"defaultScope":"cluster","clusterScopeSettings":{"allowMultipleInstances":false,"defaultReleaseNamespace":"nc-cluster-manager-extension"}},"releaseTrains":[],"clusterTypes":["Connectedclusters","Managedclusters"]}},{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest-rg/providers/Microsoft.Kubernetes/ConnectedClusters/kind-clitest-cluster/providers/Microsoft.KubernetesConfiguration/extensionTypes/microsoft.networkcloud.userrp.prod","name":"microsoft.networkcloud.userrp.prod","type":"Microsoft.KubernetesConfiguration/extensionTypes","properties":{"supportedScopes":{"defaultScope":"cluster","clusterScopeSettings":{"allowMultipleInstances":false,"defaultReleaseNamespace":"nc-rp"}},"releaseTrains":[],"clusterTypes":["Connectedclusters","Managedcluster"]}},{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest-rg/providers/Microsoft.Kubernetes/ConnectedClusters/kind-clitest-cluster/providers/Microsoft.KubernetesConfiguration/extensionTypes/microsoft.networkcloud.userrp.euap","name":"microsoft.networkcloud.userrp.euap","type":"Microsoft.KubernetesConfiguration/extensionTypes","properties":{"supportedScopes":{"defaultScope":"cluster","clusterScopeSettings":{"allowMultipleInstances":false,"defaultReleaseNamespace":"nc-rp"}},"releaseTrains":[],"clusterTypes":["Connectedclusters","Managedcluster"]}},{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest-rg/providers/Microsoft.Kubernetes/ConnectedClusters/kind-clitest-cluster/providers/Microsoft.KubernetesConfiguration/extensionTypes/cassandradatacentersoperatorv2","name":"cassandradatacentersoperatorv2","type":"Microsoft.KubernetesConfiguration/extensionTypes","properties":{"supportedScopes":{"defaultScope":"namespace","clusterScopeSettings":null},"releaseTrains":[],"clusterTypes":["Connectedclusters","Managedclusters","Appliances","ProvisionedClusters"]}},{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest-rg/providers/Microsoft.Kubernetes/ConnectedClusters/kind-clitest-cluster/providers/Microsoft.KubernetesConfiguration/extensionTypes/microsoft.app.environment","name":"microsoft.app.environment","type":"Microsoft.KubernetesConfiguration/extensionTypes","properties":{"supportedScopes":{"defaultScope":"cluster","clusterScopeSettings":{"allowMultipleInstances":false,"defaultReleaseNamespace":"appservice"}},"releaseTrains":[],"clusterTypes":[]}},{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest-rg/providers/Microsoft.Kubernetes/ConnectedClusters/kind-clitest-cluster/providers/Microsoft.KubernetesConfiguration/extensionTypes/cassandradatacentersoperatorv3","name":"cassandradatacentersoperatorv3","type":"Microsoft.KubernetesConfiguration/extensionTypes","properties":{"supportedScopes":{"defaultScope":"namespace","clusterScopeSettings":null},"releaseTrains":[],"clusterTypes":[]}},{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest-rg/providers/Microsoft.Kubernetes/ConnectedClusters/kind-clitest-cluster/providers/Microsoft.KubernetesConfiguration/extensionTypes/microsoft.defender.containers","name":"microsoft.defender.containers","type":"Microsoft.KubernetesConfiguration/extensionTypes","properties":{"supportedScopes":{"defaultScope":"cluster","clusterScopeSettings":{"allowMultipleInstances":false,"defaultReleaseNamespace":"mdc"}},"releaseTrains":[],"clusterTypes":["Connectedclusters"]}},{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest-rg/providers/Microsoft.Kubernetes/ConnectedClusters/kind-clitest-cluster/providers/Microsoft.KubernetesConfiguration/extensionTypes/microsoft.contoso.clusters","name":"microsoft.contoso.clusters","type":"Microsoft.KubernetesConfiguration/extensionTypes","properties":{"supportedScopes":{"defaultScope":"namespace","clusterScopeSettings":null},"releaseTrains":[],"clusterTypes":[]}},{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest-rg/providers/Microsoft.Kubernetes/ConnectedClusters/kind-clitest-cluster/providers/Microsoft.KubernetesConfiguration/extensionTypes/microsoft.contoso.towers","name":"microsoft.contoso.towers","type":"Microsoft.KubernetesConfiguration/extensionTypes","properties":{"supportedScopes":{"defaultScope":"cluster","clusterScopeSettings":{"allowMultipleInstances":true,"defaultReleaseNamespace":"ansible"}},"releaseTrains":[],"clusterTypes":[]}},{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest-rg/providers/Microsoft.Kubernetes/ConnectedClusters/kind-clitest-cluster/providers/Microsoft.KubernetesConfiguration/extensionTypes/microsoft.azurebackup.kubernetes.test","name":"microsoft.azurebackup.kubernetes.test","type":"Microsoft.KubernetesConfiguration/extensionTypes","properties":{"supportedScopes":{"defaultScope":"cluster","clusterScopeSettings":{"allowMultipleInstances":false,"defaultReleaseNamespace":"azbackup"}},"releaseTrains":[],"clusterTypes":["Managedclusters"]}},{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest-rg/providers/Microsoft.Kubernetes/ConnectedClusters/kind-clitest-cluster/providers/Microsoft.KubernetesConfiguration/extensionTypes/microsoft.azstor","name":"microsoft.azstor","type":"Microsoft.KubernetesConfiguration/extensionTypes","properties":{"supportedScopes":{"defaultScope":"cluster","clusterScopeSettings":{"allowMultipleInstances":false,"defaultReleaseNamespace":"azstor"}},"releaseTrains":[],"clusterTypes":["Connectedclusters","Managedclusters","Appliances"]}},{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest-rg/providers/Microsoft.Kubernetes/ConnectedClusters/kind-clitest-cluster/providers/Microsoft.KubernetesConfiguration/extensionTypes/microsoft.networkcloud.platformruntime.dev","name":"microsoft.networkcloud.platformruntime.dev","type":"Microsoft.KubernetesConfiguration/extensionTypes","properties":{"supportedScopes":{"defaultScope":"cluster","clusterScopeSettings":{"allowMultipleInstances":false,"defaultReleaseNamespace":"nc-system"}},"releaseTrains":[],"clusterTypes":["Connectedclusters","Managedclusters"]}},{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest-rg/providers/Microsoft.Kubernetes/ConnectedClusters/kind-clitest-cluster/providers/Microsoft.KubernetesConfiguration/extensionTypes/microsoft.networkcloud.platformruntime.test","name":"microsoft.networkcloud.platformruntime.test","type":"Microsoft.KubernetesConfiguration/extensionTypes","properties":{"supportedScopes":{"defaultScope":"cluster","clusterScopeSettings":{"allowMultipleInstances":false,"defaultReleaseNamespace":"nc-system"}},"releaseTrains":[],"clusterTypes":["Connectedclusters","Managedclusters"]}},{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest-rg/providers/Microsoft.Kubernetes/ConnectedClusters/kind-clitest-cluster/providers/Microsoft.KubernetesConfiguration/extensionTypes/microsoft.networkcloud.platformruntime.staging","name":"microsoft.networkcloud.platformruntime.staging","type":"Microsoft.KubernetesConfiguration/extensionTypes","properties":{"supportedScopes":{"defaultScope":"cluster","clusterScopeSettings":{"allowMultipleInstances":false,"defaultReleaseNamespace":"nc-system"}},"releaseTrains":[],"clusterTypes":["Connectedclusters","Managedclusters"]}},{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest-rg/providers/Microsoft.Kubernetes/ConnectedClusters/kind-clitest-cluster/providers/Microsoft.KubernetesConfiguration/extensionTypes/microsoft.networkcloud.platformruntime.euap","name":"microsoft.networkcloud.platformruntime.euap","type":"Microsoft.KubernetesConfiguration/extensionTypes","properties":{"supportedScopes":{"defaultScope":"cluster","clusterScopeSettings":{"allowMultipleInstances":false,"defaultReleaseNamespace":"nc-system"}},"releaseTrains":[],"clusterTypes":["Connectedclusters","Managedclusters"]}},{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest-rg/providers/Microsoft.Kubernetes/ConnectedClusters/kind-clitest-cluster/providers/Microsoft.KubernetesConfiguration/extensionTypes/microsoft.networkcloud.platformruntime.prod","name":"microsoft.networkcloud.platformruntime.prod","type":"Microsoft.KubernetesConfiguration/extensionTypes","properties":{"supportedScopes":{"defaultScope":"cluster","clusterScopeSettings":{"allowMultipleInstances":false,"defaultReleaseNamespace":"nc-system"}},"releaseTrains":[],"clusterTypes":["Connectedclusters","Managedclusters"]}},{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest-rg/providers/Microsoft.Kubernetes/ConnectedClusters/kind-clitest-cluster/providers/Microsoft.KubernetesConfiguration/extensionTypes/microsoft.networkcloud.platformruntime.sandbox","name":"microsoft.networkcloud.platformruntime.sandbox","type":"Microsoft.KubernetesConfiguration/extensionTypes","properties":{"supportedScopes":{"defaultScope":"cluster","clusterScopeSettings":{"allowMultipleInstances":false,"defaultReleaseNamespace":"nc-system"}},"releaseTrains":[],"clusterTypes":["Connectedclusters","Managedclusters"]}},{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest-rg/providers/Microsoft.Kubernetes/ConnectedClusters/kind-clitest-cluster/providers/Microsoft.KubernetesConfiguration/extensionTypes/microsoft.networkfabricserviceextension","name":"microsoft.networkfabricserviceextension","type":"Microsoft.KubernetesConfiguration/extensionTypes","properties":{"supportedScopes":{"defaultScope":"cluster","clusterScopeSettings":{"allowMultipleInstances":false,"defaultReleaseNamespace":"managednetworkfabricservices"}},"releaseTrains":[],"clusterTypes":[]}},{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest-rg/providers/Microsoft.Kubernetes/ConnectedClusters/kind-clitest-cluster/providers/Microsoft.KubernetesConfiguration/extensionTypes/microsoft.policyinsightshybridakstest","name":"microsoft.policyinsightshybridakstest","type":"Microsoft.KubernetesConfiguration/extensionTypes","properties":{"supportedScopes":{"defaultScope":"cluster","clusterScopeSettings":{"allowMultipleInstances":false,"defaultReleaseNamespace":"kube-system"}},"releaseTrains":[],"clusterTypes":["Connectedclusters","ProvisionedClusters"]}}],"nextLink":null}' + headers: + api-supported-versions: + - 2021-05-01-preview, 2022-01-15-preview + cache-control: + - no-cache + content-length: + - '17847' + content-type: + - application/json; charset=utf-8 + date: + - Mon, 17 Oct 2022 19:14:52 GMT + expires: + - '-1' + pragma: + - no-cache + strict-transport-security: + - max-age=31536000; includeSubDomains + transfer-encoding: + - chunked + vary: + - Accept-Encoding + x-content-type-options: + - nosniff + status: + code: 200 + message: OK +- request: + body: null + headers: + Accept: + - application/json + Accept-Encoding: + - gzip, deflate + CommandName: + - k8s-extension extension-types list-by-location + Connection: + - keep-alive + ParameterSetName: + - --location + User-Agent: + - AZURECLI/2.40.0 (PIP) azsdk-python-azure-mgmt-kubernetesconfiguration/2.0.0 + Python/3.10.0 (Windows-10-10.0.22621-SP0) + method: GET + uri: https://management.azure.com/subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.KubernetesConfiguration/locations/eastus2euap/extensionTypes?api-version=2022-01-15-preview + response: + body: + string: '{"value":[{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.KubernetesConfiguration/locations/eastus2euap/extensionTypes/azuremonitor-containers","name":"azuremonitor-containers","type":"Microsoft.KubernetesConfiguration/extensionTypes","properties":{"supportedScopes":{"defaultScope":"cluster","clusterScopeSettings":{"allowMultipleInstances":false,"defaultReleaseNamespace":"kube-system"}},"releaseTrains":[],"clusterTypes":[]}},{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.KubernetesConfiguration/locations/eastus2euap/extensionTypes/microsoft.policy","name":"microsoft.policy","type":"Microsoft.KubernetesConfiguration/extensionTypes","properties":{"supportedScopes":{"defaultScope":"cluster","clusterScopeSettings":{"allowMultipleInstances":false,"defaultReleaseNamespace":"kube-system"}},"releaseTrains":[],"clusterTypes":[]}},{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.KubernetesConfiguration/locations/eastus2euap/extensionTypes/microsoft.openservicemesh","name":"microsoft.openservicemesh","type":"Microsoft.KubernetesConfiguration/extensionTypes","properties":{"supportedScopes":{"defaultScope":"cluster","clusterScopeSettings":{"allowMultipleInstances":false,"defaultReleaseNamespace":"arc-osm-system"}},"releaseTrains":[],"clusterTypes":["connectedclusters","provisionedclusters"]}},{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.KubernetesConfiguration/locations/eastus2euap/extensionTypes/cassandradatacentersoperator","name":"cassandradatacentersoperator","type":"Microsoft.KubernetesConfiguration/extensionTypes","properties":{"supportedScopes":{"defaultScope":"namespace","clusterScopeSettings":null},"releaseTrains":[],"clusterTypes":["managedclusters","appliances"]}},{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.KubernetesConfiguration/locations/eastus2euap/extensionTypes/microsoft.apimanagement.gateway","name":"microsoft.apimanagement.gateway","type":"Microsoft.KubernetesConfiguration/extensionTypes","properties":{"supportedScopes":{"defaultScope":"cluster","clusterScopeSettings":{"allowMultipleInstances":false,"defaultReleaseNamespace":"gateway"}},"releaseTrains":[],"clusterTypes":[]}},{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.KubernetesConfiguration/locations/eastus2euap/extensionTypes/microsoft.web.appservice","name":"microsoft.web.appservice","type":"Microsoft.KubernetesConfiguration/extensionTypes","properties":{"supportedScopes":{"defaultScope":"cluster","clusterScopeSettings":{"allowMultipleInstances":true,"defaultReleaseNamespace":"appservice"}},"releaseTrains":[],"clusterTypes":[]}},{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.KubernetesConfiguration/locations/eastus2euap/extensionTypes/ansibletoweroperator","name":"ansibletoweroperator","type":"Microsoft.KubernetesConfiguration/extensionTypes","properties":{"supportedScopes":{"defaultScope":"cluster","clusterScopeSettings":{"allowMultipleInstances":true,"defaultReleaseNamespace":"awx-operator"}},"releaseTrains":[],"clusterTypes":[]}},{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.KubernetesConfiguration/locations/eastus2euap/extensionTypes/microsoft.azuredefender.kubernetes","name":"microsoft.azuredefender.kubernetes","type":"Microsoft.KubernetesConfiguration/extensionTypes","properties":{"supportedScopes":{"defaultScope":"cluster","clusterScopeSettings":{"allowMultipleInstances":false,"defaultReleaseNamespace":"mdc"}},"releaseTrains":[],"clusterTypes":["connectedclusters","provisionedclusters"]}},{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.KubernetesConfiguration/locations/eastus2euap/extensionTypes/microsoft.eventgrid","name":"microsoft.eventgrid","type":"Microsoft.KubernetesConfiguration/extensionTypes","properties":{"supportedScopes":{"defaultScope":"cluster","clusterScopeSettings":{"allowMultipleInstances":false,"defaultReleaseNamespace":"eventgrid-system"}},"releaseTrains":[],"clusterTypes":[]}},{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.KubernetesConfiguration/locations/eastus2euap/extensionTypes/microsoft.azureml.kubernetes","name":"microsoft.azureml.kubernetes","type":"Microsoft.KubernetesConfiguration/extensionTypes","properties":{"supportedScopes":{"defaultScope":"namespace","clusterScopeSettings":{"allowMultipleInstances":false,"defaultReleaseNamespace":"azure-ml"}},"releaseTrains":[],"clusterTypes":["managedclusters"]}},{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.KubernetesConfiguration/locations/eastus2euap/extensionTypes/dapr","name":"dapr","type":"Microsoft.KubernetesConfiguration/extensionTypes","properties":{"supportedScopes":{"defaultScope":"cluster","clusterScopeSettings":{"allowMultipleInstances":false,"defaultReleaseNamespace":"dapr-system"}},"releaseTrains":[],"clusterTypes":["managedclusters"]}},{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.KubernetesConfiguration/locations/eastus2euap/extensionTypes/microsoft.azurenw.mobilenetwork","name":"microsoft.azurenw.mobilenetwork","type":"Microsoft.KubernetesConfiguration/extensionTypes","properties":{"supportedScopes":{"defaultScope":"namespace","clusterScopeSettings":{"allowMultipleInstances":false,"defaultReleaseNamespace":"azurenw-mn"}},"releaseTrains":[],"clusterTypes":[]}},{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.KubernetesConfiguration/locations/eastus2euap/extensionTypes/microsoft.arcdataservices","name":"microsoft.arcdataservices","type":"Microsoft.KubernetesConfiguration/extensionTypes","properties":{"supportedScopes":{"defaultScope":"cluster","clusterScopeSettings":{"allowMultipleInstances":false,"defaultReleaseNamespace":"arc"}},"releaseTrains":[],"clusterTypes":["connectedclusters"]}},{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.KubernetesConfiguration/locations/eastus2euap/extensionTypes/microsoft.scvmm","name":"microsoft.scvmm","type":"Microsoft.KubernetesConfiguration/extensionTypes","properties":{"supportedScopes":{"defaultScope":"cluster","clusterScopeSettings":{"allowMultipleInstances":false,"defaultReleaseNamespace":"azure-vmmoperator"}},"releaseTrains":[],"clusterTypes":["ConnectedClusters","Appliances"]}},{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.KubernetesConfiguration/locations/eastus2euap/extensionTypes/microsoft.avs","name":"microsoft.avs","type":"Microsoft.KubernetesConfiguration/extensionTypes","properties":{"supportedScopes":{"defaultScope":"cluster","clusterScopeSettings":{"allowMultipleInstances":false,"defaultReleaseNamespace":"azure-avsoperator"}},"releaseTrains":[],"clusterTypes":["ConnectedClusters","Appliances"]}},{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.KubernetesConfiguration/locations/eastus2euap/extensionTypes/microsoft.vmware","name":"microsoft.vmware","type":"Microsoft.KubernetesConfiguration/extensionTypes","properties":{"supportedScopes":{"defaultScope":"cluster","clusterScopeSettings":{"allowMultipleInstances":false,"defaultReleaseNamespace":"azure-vmwareoperator"}},"releaseTrains":[],"clusterTypes":["ConnectedClusters","Appliances"]}},{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.KubernetesConfiguration/locations/eastus2euap/extensionTypes/microsoft.azstackhci.operator","name":"microsoft.azstackhci.operator","type":"Microsoft.KubernetesConfiguration/extensionTypes","properties":{"supportedScopes":{"defaultScope":"cluster","clusterScopeSettings":{"allowMultipleInstances":false,"defaultReleaseNamespace":"azstackhci-operator"}},"releaseTrains":[],"clusterTypes":["Appliances"]}},{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.KubernetesConfiguration/locations/eastus2euap/extensionTypes/microsoft.azurenw.networkfunction","name":"microsoft.azurenw.networkfunction","type":"Microsoft.KubernetesConfiguration/extensionTypes","properties":{"supportedScopes":{"defaultScope":"namespace","clusterScopeSettings":{"allowMultipleInstances":false,"defaultReleaseNamespace":"azure-networkfunction"}},"releaseTrains":[],"clusterTypes":[]}},{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.KubernetesConfiguration/locations/eastus2euap/extensionTypes/microsoft.azuremonitor.containers","name":"microsoft.azuremonitor.containers","type":"Microsoft.KubernetesConfiguration/extensionTypes","properties":{"supportedScopes":{"defaultScope":"cluster","clusterScopeSettings":{"allowMultipleInstances":false,"defaultReleaseNamespace":"kube-system"}},"releaseTrains":[],"clusterTypes":["ConnectedCluster","ProvisionedClusters"]}},{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.KubernetesConfiguration/locations/eastus2euap/extensionTypes/microsoft.unitycloud.konductor","name":"microsoft.unitycloud.konductor","type":"Microsoft.KubernetesConfiguration/extensionTypes","properties":{"supportedScopes":{"defaultScope":"cluster","clusterScopeSettings":{"allowMultipleInstances":false,"defaultReleaseNamespace":"konductor"}},"releaseTrains":[],"clusterTypes":[]}},{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.KubernetesConfiguration/locations/eastus2euap/extensionTypes/microsoft.policyinsights","name":"microsoft.policyinsights","type":"Microsoft.KubernetesConfiguration/extensionTypes","properties":{"supportedScopes":{"defaultScope":"cluster","clusterScopeSettings":{"allowMultipleInstances":false,"defaultReleaseNamespace":"kube-system"}},"releaseTrains":[],"clusterTypes":[]}},{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.KubernetesConfiguration/locations/eastus2euap/extensionTypes/microsoft.cnab","name":"microsoft.cnab","type":"Microsoft.KubernetesConfiguration/extensionTypes","properties":{"supportedScopes":{"defaultScope":"cluster","clusterScopeSettings":{"allowMultipleInstances":false,"defaultReleaseNamespace":"cnab-operator"}},"releaseTrains":[],"clusterTypes":[]}},{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.KubernetesConfiguration/locations/eastus2euap/extensionTypes/microsoft.azure.hybridnetwork","name":"microsoft.azure.hybridnetwork","type":"Microsoft.KubernetesConfiguration/extensionTypes","properties":{"supportedScopes":{"defaultScope":"namespace","clusterScopeSettings":{"allowMultipleInstances":false,"defaultReleaseNamespace":"azurehybridnetwork"}},"releaseTrains":[],"clusterTypes":["appliances","provisionedclusters","connectedclusters"]}},{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.KubernetesConfiguration/locations/eastus2euap/extensionTypes/microsoft.azurebackup.backupagent","name":"microsoft.azurebackup.backupagent","type":"Microsoft.KubernetesConfiguration/extensionTypes","properties":{"supportedScopes":{"defaultScope":"namespace","clusterScopeSettings":{"allowMultipleInstances":false,"defaultReleaseNamespace":"azurebackup"}},"releaseTrains":[],"clusterTypes":["Connectedclusters","Managedclusters","Appliances","ProvisionedClusters"]}},{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.KubernetesConfiguration/locations/eastus2euap/extensionTypes/microsoft.aksedgeoperator","name":"microsoft.aksedgeoperator","type":"Microsoft.KubernetesConfiguration/extensionTypes","properties":{"supportedScopes":{"defaultScope":"cluster","clusterScopeSettings":{"allowMultipleInstances":false,"defaultReleaseNamespace":"aksedge-operator-system"}},"releaseTrains":[],"clusterTypes":[]}},{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.KubernetesConfiguration/locations/eastus2euap/extensionTypes/microsoft.flux","name":"microsoft.flux","type":"Microsoft.KubernetesConfiguration/extensionTypes","properties":{"supportedScopes":{"defaultScope":"cluster","clusterScopeSettings":{"allowMultipleInstances":false,"defaultReleaseNamespace":"flux-system"}},"releaseTrains":[],"clusterTypes":["connectedclusters","managedclusters","provisionedclusters"]}},{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.KubernetesConfiguration/locations/eastus2euap/extensionTypes/microsoft.azurebackup.dataprotectionplugin","name":"microsoft.azurebackup.dataprotectionplugin","type":"Microsoft.KubernetesConfiguration/extensionTypes","properties":{"supportedScopes":{"defaultScope":"namespace","clusterScopeSettings":null},"releaseTrains":[],"clusterTypes":["Connectedclusters","Managedclusters","Appliances","ProvisionedClusters"]}},{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.KubernetesConfiguration/locations/eastus2euap/extensionTypes/microsoft.containerregistry.connectedregistry","name":"microsoft.containerregistry.connectedregistry","type":"Microsoft.KubernetesConfiguration/extensionTypes","properties":{"supportedScopes":{"defaultScope":"cluster","clusterScopeSettings":{"allowMultipleInstances":false,"defaultReleaseNamespace":"connected-registry"}},"releaseTrains":[],"clusterTypes":[]}},{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.KubernetesConfiguration/locations/eastus2euap/extensionTypes/microsoft.dapr","name":"microsoft.dapr","type":"Microsoft.KubernetesConfiguration/extensionTypes","properties":{"supportedScopes":{"defaultScope":"cluster","clusterScopeSettings":{"allowMultipleInstances":false,"defaultReleaseNamespace":"dapr-system"}},"releaseTrains":[],"clusterTypes":["connectedCluster","managedClusters","Appliances","ProvisionedClusters"]}},{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.KubernetesConfiguration/locations/eastus2euap/extensionTypes/microsoft.hybridaksoperator","name":"microsoft.hybridaksoperator","type":"Microsoft.KubernetesConfiguration/extensionTypes","properties":{"supportedScopes":{"defaultScope":"cluster","clusterScopeSettings":{"allowMultipleInstances":false,"defaultReleaseNamespace":"hybridaks-operator-system"}},"releaseTrains":[],"clusterTypes":["Appliances"]}},{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.KubernetesConfiguration/locations/eastus2euap/extensionTypes/microsoft.azurekeyvaultsecretsprovider","name":"microsoft.azurekeyvaultsecretsprovider","type":"Microsoft.KubernetesConfiguration/extensionTypes","properties":{"supportedScopes":{"defaultScope":"cluster","clusterScopeSettings":{"allowMultipleInstances":false,"defaultReleaseNamespace":"kube-system"}},"releaseTrains":[],"clusterTypes":["connectedclusters","provisionedclusters"]}},{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.KubernetesConfiguration/locations/eastus2euap/extensionTypes/microsoft.testbonsaiextension","name":"microsoft.testbonsaiextension","type":"Microsoft.KubernetesConfiguration/extensionTypes","properties":{"supportedScopes":{"defaultScope":"cluster","clusterScopeSettings":{"allowMultipleInstances":true,"defaultReleaseNamespace":"kube-system"}},"releaseTrains":[],"clusterTypes":[]}},{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.KubernetesConfiguration/locations/eastus2euap/extensionTypes/cassandradatacentersoperator1","name":"cassandradatacentersoperator1","type":"Microsoft.KubernetesConfiguration/extensionTypes","properties":{"supportedScopes":{"defaultScope":"namespace","clusterScopeSettings":null},"releaseTrains":[],"clusterTypes":["managedclusters","appliances"]}},{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.KubernetesConfiguration/locations/eastus2euap/extensionTypes/microsoft.azurevote.previewstandard","name":"microsoft.azurevote.previewstandard","type":"Microsoft.KubernetesConfiguration/extensionTypes","properties":{"supportedScopes":{"defaultScope":"namespace","clusterScopeSettings":{"allowMultipleInstances":false,"defaultReleaseNamespace":"vote"}},"releaseTrains":[],"clusterTypes":["managedclusters"]}},{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.KubernetesConfiguration/locations/eastus2euap/extensionTypes/microsoft.servicelinker.connection","name":"microsoft.servicelinker.connection","type":"Microsoft.KubernetesConfiguration/extensionTypes","properties":{"supportedScopes":{"defaultScope":"cluster","clusterScopeSettings":{"allowMultipleInstances":true,"defaultReleaseNamespace":"default"}},"releaseTrains":[],"clusterTypes":["managedclusters"]}},{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.KubernetesConfiguration/locations/eastus2euap/extensionTypes/deislabs.akri","name":"deislabs.akri","type":"Microsoft.KubernetesConfiguration/extensionTypes","properties":{"supportedScopes":{"defaultScope":"cluster","clusterScopeSettings":{"allowMultipleInstances":false,"defaultReleaseNamespace":"akri"}},"releaseTrains":[],"clusterTypes":[]}},{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.KubernetesConfiguration/locations/eastus2euap/extensionTypes/microsoft.arcextensionusage","name":"microsoft.arcextensionusage","type":"Microsoft.KubernetesConfiguration/extensionTypes","properties":{"supportedScopes":{"defaultScope":"cluster","clusterScopeSettings":{"allowMultipleInstances":false,"defaultReleaseNamespace":"arc-osm-system"}},"releaseTrains":[],"clusterTypes":["managedclusters","connectedclusters"]}},{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.KubernetesConfiguration/locations/eastus2euap/extensionTypes/microsoft.azure.mobilenetwork.packetcoremonitor","name":"microsoft.azure.mobilenetwork.packetcoremonitor","type":"Microsoft.KubernetesConfiguration/extensionTypes","properties":{"supportedScopes":{"defaultScope":"cluster","clusterScopeSettings":{"allowMultipleInstances":false,"defaultReleaseNamespace":"packet-core-monitor"}},"releaseTrains":[],"clusterTypes":["Connectedclusters","Managedclusters","Appliances"]}},{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.KubernetesConfiguration/locations/eastus2euap/extensionTypes/microsoft.extensionsusage","name":"microsoft.extensionsusage","type":"Microsoft.KubernetesConfiguration/extensionTypes","properties":{"supportedScopes":{"defaultScope":"cluster","clusterScopeSettings":{"allowMultipleInstances":false,"defaultReleaseNamespace":"azure-extensions-usage-system"}},"releaseTrains":[],"clusterTypes":["Connectedclusters","Managedclusters"]}},{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.KubernetesConfiguration/locations/eastus2euap/extensionTypes/microsoft.connectedopenstack","name":"microsoft.connectedopenstack","type":"Microsoft.KubernetesConfiguration/extensionTypes","properties":{"supportedScopes":{"defaultScope":"cluster","clusterScopeSettings":{"allowMultipleInstances":false,"defaultReleaseNamespace":"connectedopenstack"}},"releaseTrains":[],"clusterTypes":["Appliances","connectedCluster"]}},{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.KubernetesConfiguration/locations/eastus2euap/extensionTypes/microsoft.networkcloud.userrp","name":"microsoft.networkcloud.userrp","type":"Microsoft.KubernetesConfiguration/extensionTypes","properties":{"supportedScopes":{"defaultScope":"cluster","clusterScopeSettings":{"allowMultipleInstances":false,"defaultReleaseNamespace":"nc-rp"}},"releaseTrains":[],"clusterTypes":["Connectedclusters","Managedcluster"]}},{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.KubernetesConfiguration/locations/eastus2euap/extensionTypes/microsoft.aziot.edge","name":"microsoft.aziot.edge","type":"Microsoft.KubernetesConfiguration/extensionTypes","properties":{"supportedScopes":{"defaultScope":"cluster","clusterScopeSettings":{"allowMultipleInstances":false,"defaultReleaseNamespace":"aziotedge-system"}},"releaseTrains":[],"clusterTypes":[]}},{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.KubernetesConfiguration/locations/eastus2euap/extensionTypes/microsoft.appliance.management.operator","name":"microsoft.appliance.management.operator","type":"Microsoft.KubernetesConfiguration/extensionTypes","properties":{"supportedScopes":{"defaultScope":"cluster","clusterScopeSettings":{"allowMultipleInstances":false,"defaultReleaseNamespace":"kva-management"}},"releaseTrains":[],"clusterTypes":["Appliances"]}},{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.KubernetesConfiguration/locations/eastus2euap/extensionTypes/microsoft.networkcloud.userrp.dev","name":"microsoft.networkcloud.userrp.dev","type":"Microsoft.KubernetesConfiguration/extensionTypes","properties":{"supportedScopes":{"defaultScope":"cluster","clusterScopeSettings":{"allowMultipleInstances":false,"defaultReleaseNamespace":"nc-rp"}},"releaseTrains":[],"clusterTypes":["Connectedclusters","Managedcluster"]}},{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.KubernetesConfiguration/locations/eastus2euap/extensionTypes/microsoft.networkcloud.clustermanager.dev","name":"microsoft.networkcloud.clustermanager.dev","type":"Microsoft.KubernetesConfiguration/extensionTypes","properties":{"supportedScopes":{"defaultScope":"cluster","clusterScopeSettings":{"allowMultipleInstances":false,"defaultReleaseNamespace":"nc-cluster-manager-extension"}},"releaseTrains":[],"clusterTypes":["Connectedclusters","Managedclusters"]}},{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.KubernetesConfiguration/locations/eastus2euap/extensionTypes/microsoft.networkcloud.platformcluster.dev","name":"microsoft.networkcloud.platformcluster.dev","type":"Microsoft.KubernetesConfiguration/extensionTypes","properties":{"supportedScopes":{"defaultScope":"cluster","clusterScopeSettings":{"allowMultipleInstances":false,"defaultReleaseNamespace":"nc-system"}},"releaseTrains":[],"clusterTypes":["Connectedclusters","Managedclusters"]}},{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.KubernetesConfiguration/locations/eastus2euap/extensionTypes/microsoft.managednetworkfabric","name":"microsoft.managednetworkfabric","type":"Microsoft.KubernetesConfiguration/extensionTypes","properties":{"supportedScopes":{"defaultScope":"cluster","clusterScopeSettings":{"allowMultipleInstances":false,"defaultReleaseNamespace":"managednetworkfabric"}},"releaseTrains":[],"clusterTypes":[]}},{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.KubernetesConfiguration/locations/eastus2euap/extensionTypes/ + microsoft.azurebackup.mockplugin","name":" microsoft.azurebackup.mockplugin","type":"Microsoft.KubernetesConfiguration/extensionTypes","properties":{"supportedScopes":{"defaultScope":" + namespace","clusterScopeSettings":{"allowMultipleInstances":false,"defaultReleaseNamespace":null}},"releaseTrains":[],"clusterTypes":["Connectedclusters","Managedclusters","Appliances","ProvisionedClusters"]}},{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.KubernetesConfiguration/locations/eastus2euap/extensionTypes/microsoft.azurebackup.mockplugin","name":"microsoft.azurebackup.mockplugin","type":"Microsoft.KubernetesConfiguration/extensionTypes","properties":{"supportedScopes":{"defaultScope":" + namespace","clusterScopeSettings":{"allowMultipleInstances":false,"defaultReleaseNamespace":null}},"releaseTrains":[],"clusterTypes":["Connectedclusters","Managedclusters","Appliances","ProvisionedClusters"]}},{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.KubernetesConfiguration/locations/eastus2euap/extensionTypes/microsoft.networkcloud.userrp.staging","name":"microsoft.networkcloud.userrp.staging","type":"Microsoft.KubernetesConfiguration/extensionTypes","properties":{"supportedScopes":{"defaultScope":"cluster","clusterScopeSettings":{"allowMultipleInstances":false,"defaultReleaseNamespace":"nc-rp"}},"releaseTrains":[],"clusterTypes":["Connectedclusters","Managedcluster"]}}],"nextLink":"https://management.azure.com/subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.KubernetesConfiguration/locations/eastus2euap/extensionTypes?api-version=2022-01-15-preview&continuationToken=JTVCJTdCJTIydG9rZW4lMjIlM0ElMjIlMkJSSUQlM0F%2Bc1VvbEFONFRzMHkxRHowQUFBQUFBQSUzRCUzRCUyM1JUJTNBMSUyM1RSQyUzQTUwJTIzSVNWJTNBMiUyM0lFTyUzQTY1NTUxJTIzUUNGJTNBOCUyMiUyQyUyMnJhbmdlJTIyJTNBJTdCJTIybWluJTIyJTNBJTIyJTIyJTJDJTIybWF4JTIyJTNBJTIyMDVDMURGRkZGRkZGRkMlMjIlN0QlN0QlNUQ%3D"}' + headers: + api-supported-versions: + - 2021-05-01-preview, 2022-01-15-preview + cache-control: + - no-cache + content-length: + - '24621' + content-type: + - application/json; charset=utf-8 + date: + - Mon, 17 Oct 2022 19:14:53 GMT + expires: + - '-1' + pragma: + - no-cache + strict-transport-security: + - max-age=31536000; includeSubDomains + transfer-encoding: + - chunked + vary: + - Accept-Encoding + x-content-type-options: + - nosniff + status: + code: 200 + message: OK +- request: + body: null + headers: + Accept: + - application/json + Accept-Encoding: + - gzip, deflate + CommandName: + - k8s-extension extension-types list-by-location + Connection: + - keep-alive + ParameterSetName: + - --location + User-Agent: + - AZURECLI/2.40.0 (PIP) azsdk-python-azure-mgmt-kubernetesconfiguration/2.0.0 + Python/3.10.0 (Windows-10-10.0.22621-SP0) + method: GET + uri: https://management.azure.com/subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.KubernetesConfiguration/locations/eastus2euap/extensionTypes?api-version=2022-01-15-preview&continuationToken=JTVCJTdCJTIydG9rZW4lMjIlM0ElMjIlMkJSSUQlM0F%2Bc1VvbEFONFRzMHkxRHowQUFBQUFBQSUzRCUzRCUyM1JUJTNBMSUyM1RSQyUzQTUwJTIzSVNWJTNBMiUyM0lFTyUzQTY1NTUxJTIzUUNGJTNBOCUyMiUyQyUyMnJhbmdlJTIyJTNBJTdCJTIybWluJTIyJTNBJTIyJTIyJTJDJTIybWF4JTIyJTNBJTIyMDVDMURGRkZGRkZGRkMlMjIlN0QlN0QlNUQ%3D + response: + body: + string: '{"value":[{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.KubernetesConfiguration/locations/eastus2euap/extensionTypes/microsoft.networkcloud.platformcluster.sandbox","name":"microsoft.networkcloud.platformcluster.sandbox","type":"Microsoft.KubernetesConfiguration/extensionTypes","properties":{"supportedScopes":{"defaultScope":"cluster","clusterScopeSettings":{"allowMultipleInstances":false,"defaultReleaseNamespace":"nc-system"}},"releaseTrains":[],"clusterTypes":["Connectedclusters","Managedclusters"]}},{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.KubernetesConfiguration/locations/eastus2euap/extensionTypes/microsoft.networkcloud.userrp.test","name":"microsoft.networkcloud.userrp.test","type":"Microsoft.KubernetesConfiguration/extensionTypes","properties":{"supportedScopes":{"defaultScope":"cluster","clusterScopeSettings":{"allowMultipleInstances":false,"defaultReleaseNamespace":"nc-rp"}},"releaseTrains":[],"clusterTypes":["Connectedclusters","Managedcluster"]}},{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.KubernetesConfiguration/locations/eastus2euap/extensionTypes/microsoft.networkcloud.platformcluster.test","name":"microsoft.networkcloud.platformcluster.test","type":"Microsoft.KubernetesConfiguration/extensionTypes","properties":{"supportedScopes":{"defaultScope":"cluster","clusterScopeSettings":{"allowMultipleInstances":false,"defaultReleaseNamespace":"nc-system"}},"releaseTrains":[],"clusterTypes":["Connectedclusters","Managedclusters"]}},{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.KubernetesConfiguration/locations/eastus2euap/extensionTypes/microsoft.networkcloud.clustermanager.test","name":"microsoft.networkcloud.clustermanager.test","type":"Microsoft.KubernetesConfiguration/extensionTypes","properties":{"supportedScopes":{"defaultScope":"cluster","clusterScopeSettings":{"allowMultipleInstances":false,"defaultReleaseNamespace":"nc-cluster-manager-extension"}},"releaseTrains":[],"clusterTypes":["Connectedclusters","Managedclusters"]}},{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.KubernetesConfiguration/locations/eastus2euap/extensionTypes/microsoft.networkcloud.clustermanager.sandbox","name":"microsoft.networkcloud.clustermanager.sandbox","type":"Microsoft.KubernetesConfiguration/extensionTypes","properties":{"supportedScopes":{"defaultScope":"cluster","clusterScopeSettings":{"allowMultipleInstances":false,"defaultReleaseNamespace":"nc-system"}},"releaseTrains":[],"clusterTypes":["Connectedclusters","Managedclusters"]}},{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.KubernetesConfiguration/locations/eastus2euap/extensionTypes/microsoft.networkcloud.clustermanager.staging","name":"microsoft.networkcloud.clustermanager.staging","type":"Microsoft.KubernetesConfiguration/extensionTypes","properties":{"supportedScopes":{"defaultScope":"cluster","clusterScopeSettings":{"allowMultipleInstances":false,"defaultReleaseNamespace":"nc-cluster-manager-extension"}},"releaseTrains":[],"clusterTypes":["Connectedclusters","Managedclusters"]}},{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.KubernetesConfiguration/locations/eastus2euap/extensionTypes/microsoft.networkcloud.platformcluster.staging","name":"microsoft.networkcloud.platformcluster.staging","type":"Microsoft.KubernetesConfiguration/extensionTypes","properties":{"supportedScopes":{"defaultScope":"cluster","clusterScopeSettings":{"allowMultipleInstances":false,"defaultReleaseNamespace":"nc-system"}},"releaseTrains":[],"clusterTypes":["Connectedclusters","Managedclusters"]}},{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.KubernetesConfiguration/locations/eastus2euap/extensionTypes/microsoft.az.edge.mqtt","name":"microsoft.az.edge.mqtt","type":"Microsoft.KubernetesConfiguration/extensionTypes","properties":{"supportedScopes":{"defaultScope":"cluster","clusterScopeSettings":{"allowMultipleInstances":false,"defaultReleaseNamespace":"azedge-system"}},"releaseTrains":[],"clusterTypes":[]}},{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.KubernetesConfiguration/locations/eastus2euap/extensionTypes/microsoft.azedge.mqtt","name":"microsoft.azedge.mqtt","type":"Microsoft.KubernetesConfiguration/extensionTypes","properties":{"supportedScopes":{"defaultScope":"cluster","clusterScopeSettings":{"allowMultipleInstances":false,"defaultReleaseNamespace":"azedge-system"}},"releaseTrains":[],"clusterTypes":[]}},{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.KubernetesConfiguration/locations/eastus2euap/extensionTypes/microsoft.networkcloud.platformcluster.euap","name":"microsoft.networkcloud.platformcluster.euap","type":"Microsoft.KubernetesConfiguration/extensionTypes","properties":{"supportedScopes":{"defaultScope":"cluster","clusterScopeSettings":{"allowMultipleInstances":false,"defaultReleaseNamespace":"nc-system"}},"releaseTrains":[],"clusterTypes":["Connectedclusters","Managedclusters"]}},{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.KubernetesConfiguration/locations/eastus2euap/extensionTypes/microsoft.networkcloud.platformcluster.prod","name":"microsoft.networkcloud.platformcluster.prod","type":"Microsoft.KubernetesConfiguration/extensionTypes","properties":{"supportedScopes":{"defaultScope":"cluster","clusterScopeSettings":{"allowMultipleInstances":false,"defaultReleaseNamespace":"nc-system"}},"releaseTrains":[],"clusterTypes":["Connectedclusters","Managedclusters"]}},{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.KubernetesConfiguration/locations/eastus2euap/extensionTypes/microsoft.networkcloud.clustermanager.prod","name":"microsoft.networkcloud.clustermanager.prod","type":"Microsoft.KubernetesConfiguration/extensionTypes","properties":{"supportedScopes":{"defaultScope":"cluster","clusterScopeSettings":{"allowMultipleInstances":false,"defaultReleaseNamespace":"nc-cluster-manager-extension"}},"releaseTrains":[],"clusterTypes":["Connectedclusters","Managedclusters"]}},{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.KubernetesConfiguration/locations/eastus2euap/extensionTypes/microsoft.networkcloud.clustermanager.euap","name":"microsoft.networkcloud.clustermanager.euap","type":"Microsoft.KubernetesConfiguration/extensionTypes","properties":{"supportedScopes":{"defaultScope":"cluster","clusterScopeSettings":{"allowMultipleInstances":false,"defaultReleaseNamespace":"nc-cluster-manager-extension"}},"releaseTrains":[],"clusterTypes":["Connectedclusters","Managedclusters"]}},{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.KubernetesConfiguration/locations/eastus2euap/extensionTypes/microsoft.networkcloud.userrp.prod","name":"microsoft.networkcloud.userrp.prod","type":"Microsoft.KubernetesConfiguration/extensionTypes","properties":{"supportedScopes":{"defaultScope":"cluster","clusterScopeSettings":{"allowMultipleInstances":false,"defaultReleaseNamespace":"nc-rp"}},"releaseTrains":[],"clusterTypes":["Connectedclusters","Managedcluster"]}},{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.KubernetesConfiguration/locations/eastus2euap/extensionTypes/microsoft.networkcloud.userrp.euap","name":"microsoft.networkcloud.userrp.euap","type":"Microsoft.KubernetesConfiguration/extensionTypes","properties":{"supportedScopes":{"defaultScope":"cluster","clusterScopeSettings":{"allowMultipleInstances":false,"defaultReleaseNamespace":"nc-rp"}},"releaseTrains":[],"clusterTypes":["Connectedclusters","Managedcluster"]}},{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.KubernetesConfiguration/locations/eastus2euap/extensionTypes/cassandradatacentersoperatorv2","name":"cassandradatacentersoperatorv2","type":"Microsoft.KubernetesConfiguration/extensionTypes","properties":{"supportedScopes":{"defaultScope":"namespace","clusterScopeSettings":null},"releaseTrains":[],"clusterTypes":["Connectedclusters","Managedclusters","Appliances","ProvisionedClusters"]}},{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.KubernetesConfiguration/locations/eastus2euap/extensionTypes/microsoft.app.environment","name":"microsoft.app.environment","type":"Microsoft.KubernetesConfiguration/extensionTypes","properties":{"supportedScopes":{"defaultScope":"cluster","clusterScopeSettings":{"allowMultipleInstances":false,"defaultReleaseNamespace":"appservice"}},"releaseTrains":[],"clusterTypes":[]}},{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.KubernetesConfiguration/locations/eastus2euap/extensionTypes/cassandradatacentersoperatorv3","name":"cassandradatacentersoperatorv3","type":"Microsoft.KubernetesConfiguration/extensionTypes","properties":{"supportedScopes":{"defaultScope":"namespace","clusterScopeSettings":null},"releaseTrains":[],"clusterTypes":[]}},{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.KubernetesConfiguration/locations/eastus2euap/extensionTypes/microsoft.defender.containers","name":"microsoft.defender.containers","type":"Microsoft.KubernetesConfiguration/extensionTypes","properties":{"supportedScopes":{"defaultScope":"cluster","clusterScopeSettings":{"allowMultipleInstances":false,"defaultReleaseNamespace":"mdc"}},"releaseTrains":[],"clusterTypes":["Connectedclusters"]}},{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.KubernetesConfiguration/locations/eastus2euap/extensionTypes/microsoft.contoso.clusters","name":"microsoft.contoso.clusters","type":"Microsoft.KubernetesConfiguration/extensionTypes","properties":{"supportedScopes":{"defaultScope":"namespace","clusterScopeSettings":null},"releaseTrains":[],"clusterTypes":[]}},{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.KubernetesConfiguration/locations/eastus2euap/extensionTypes/microsoft.contoso.towers","name":"microsoft.contoso.towers","type":"Microsoft.KubernetesConfiguration/extensionTypes","properties":{"supportedScopes":{"defaultScope":"cluster","clusterScopeSettings":{"allowMultipleInstances":true,"defaultReleaseNamespace":"ansible"}},"releaseTrains":[],"clusterTypes":[]}},{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.KubernetesConfiguration/locations/eastus2euap/extensionTypes/microsoft.azurebackup.kubernetes.test","name":"microsoft.azurebackup.kubernetes.test","type":"Microsoft.KubernetesConfiguration/extensionTypes","properties":{"supportedScopes":{"defaultScope":"cluster","clusterScopeSettings":{"allowMultipleInstances":false,"defaultReleaseNamespace":"azbackup"}},"releaseTrains":[],"clusterTypes":["Managedclusters"]}},{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.KubernetesConfiguration/locations/eastus2euap/extensionTypes/microsoft.azstor","name":"microsoft.azstor","type":"Microsoft.KubernetesConfiguration/extensionTypes","properties":{"supportedScopes":{"defaultScope":"cluster","clusterScopeSettings":{"allowMultipleInstances":false,"defaultReleaseNamespace":"azstor"}},"releaseTrains":[],"clusterTypes":["Connectedclusters","Managedclusters","Appliances"]}},{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.KubernetesConfiguration/locations/eastus2euap/extensionTypes/microsoft.networkcloud.platformruntime.dev","name":"microsoft.networkcloud.platformruntime.dev","type":"Microsoft.KubernetesConfiguration/extensionTypes","properties":{"supportedScopes":{"defaultScope":"cluster","clusterScopeSettings":{"allowMultipleInstances":false,"defaultReleaseNamespace":"nc-system"}},"releaseTrains":[],"clusterTypes":["Connectedclusters","Managedclusters"]}},{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.KubernetesConfiguration/locations/eastus2euap/extensionTypes/microsoft.networkcloud.platformruntime.test","name":"microsoft.networkcloud.platformruntime.test","type":"Microsoft.KubernetesConfiguration/extensionTypes","properties":{"supportedScopes":{"defaultScope":"cluster","clusterScopeSettings":{"allowMultipleInstances":false,"defaultReleaseNamespace":"nc-system"}},"releaseTrains":[],"clusterTypes":["Connectedclusters","Managedclusters"]}},{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.KubernetesConfiguration/locations/eastus2euap/extensionTypes/microsoft.networkcloud.platformruntime.staging","name":"microsoft.networkcloud.platformruntime.staging","type":"Microsoft.KubernetesConfiguration/extensionTypes","properties":{"supportedScopes":{"defaultScope":"cluster","clusterScopeSettings":{"allowMultipleInstances":false,"defaultReleaseNamespace":"nc-system"}},"releaseTrains":[],"clusterTypes":["Connectedclusters","Managedclusters"]}},{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.KubernetesConfiguration/locations/eastus2euap/extensionTypes/microsoft.networkcloud.platformruntime.euap","name":"microsoft.networkcloud.platformruntime.euap","type":"Microsoft.KubernetesConfiguration/extensionTypes","properties":{"supportedScopes":{"defaultScope":"cluster","clusterScopeSettings":{"allowMultipleInstances":false,"defaultReleaseNamespace":"nc-system"}},"releaseTrains":[],"clusterTypes":["Connectedclusters","Managedclusters"]}},{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.KubernetesConfiguration/locations/eastus2euap/extensionTypes/microsoft.networkcloud.platformruntime.prod","name":"microsoft.networkcloud.platformruntime.prod","type":"Microsoft.KubernetesConfiguration/extensionTypes","properties":{"supportedScopes":{"defaultScope":"cluster","clusterScopeSettings":{"allowMultipleInstances":false,"defaultReleaseNamespace":"nc-system"}},"releaseTrains":[],"clusterTypes":["Connectedclusters","Managedclusters"]}},{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.KubernetesConfiguration/locations/eastus2euap/extensionTypes/microsoft.networkcloud.platformruntime.sandbox","name":"microsoft.networkcloud.platformruntime.sandbox","type":"Microsoft.KubernetesConfiguration/extensionTypes","properties":{"supportedScopes":{"defaultScope":"cluster","clusterScopeSettings":{"allowMultipleInstances":false,"defaultReleaseNamespace":"nc-system"}},"releaseTrains":[],"clusterTypes":["Connectedclusters","Managedclusters"]}},{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.KubernetesConfiguration/locations/eastus2euap/extensionTypes/microsoft.networkfabricserviceextension","name":"microsoft.networkfabricserviceextension","type":"Microsoft.KubernetesConfiguration/extensionTypes","properties":{"supportedScopes":{"defaultScope":"cluster","clusterScopeSettings":{"allowMultipleInstances":false,"defaultReleaseNamespace":"managednetworkfabricservices"}},"releaseTrains":[],"clusterTypes":[]}},{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.KubernetesConfiguration/locations/eastus2euap/extensionTypes/microsoft.policyinsightshybridakstest","name":"microsoft.policyinsightshybridakstest","type":"Microsoft.KubernetesConfiguration/extensionTypes","properties":{"supportedScopes":{"defaultScope":"cluster","clusterScopeSettings":{"allowMultipleInstances":false,"defaultReleaseNamespace":"kube-system"}},"releaseTrains":[],"clusterTypes":["Connectedclusters","ProvisionedClusters"]}}],"nextLink":null}' + headers: + api-supported-versions: + - 2021-05-01-preview, 2022-01-15-preview + cache-control: + - no-cache + content-length: + - '15553' + content-type: + - application/json; charset=utf-8 + date: + - Mon, 17 Oct 2022 19:14:54 GMT + expires: + - '-1' + pragma: + - no-cache + strict-transport-security: + - max-age=31536000; includeSubDomains + transfer-encoding: + - chunked + vary: + - Accept-Encoding + x-content-type-options: + - nosniff + status: + code: 200 + message: OK +- request: + body: null + headers: + Accept: + - application/json + Accept-Encoding: + - gzip, deflate + CommandName: + - k8s-extension extension-types list-versions + Connection: + - keep-alive + ParameterSetName: + - --location --extension-type + User-Agent: + - AZURECLI/2.40.0 (PIP) azsdk-python-azure-mgmt-kubernetesconfiguration/2.0.0 + Python/3.10.0 (Windows-10-10.0.22621-SP0) + method: GET + uri: https://management.azure.com/subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.KubernetesConfiguration/locations/eastus2euap/extensionTypes/cassandradatacentersoperator/versions?api-version=2022-01-15-preview + response: + body: + string: '{"value":[],"nextLink":null}' + headers: + api-supported-versions: + - 2021-05-01-preview, 2022-01-15-preview + cache-control: + - no-cache + content-length: + - '28' + content-type: + - application/json; charset=utf-8 + date: + - Mon, 17 Oct 2022 19:14:56 GMT + expires: + - '-1' + pragma: + - no-cache + strict-transport-security: + - max-age=31536000; includeSubDomains + transfer-encoding: + - chunked + vary: + - Accept-Encoding + x-content-type-options: + - nosniff + status: + code: 200 + message: OK +version: 1 diff --git a/src/k8s-extension/azext_k8s_extension/tests/latest/test_k8s_extension_types_scenario.py b/src/k8s-extension/azext_k8s_extension/tests/latest/test_k8s_extension_types_scenario.py new file mode 100644 index 00000000000..390b1aaab4d --- /dev/null +++ b/src/k8s-extension/azext_k8s_extension/tests/latest/test_k8s_extension_types_scenario.py @@ -0,0 +1,40 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + +# pylint: disable=line-too-long + +import os +from azure.cli.testsdk import (ScenarioTest, record_only) + + +TEST_DIR = os.path.abspath(os.path.join(os.path.abspath(__file__), '..')) + + +class K8sExtensionTypesScenarioTest(ScenarioTest): + @record_only() + def test_k8s_extension_types(self): + extension_type = 'cassandradatacentersoperator' + self.kwargs.update({ + 'rg': 'clitest-rg', #K8sPartnerExtensionTest', + 'cluster_name': 'kind-clitest-cluster',#'k8s-extension-cluster-32469-arc', + 'cluster_type': 'connectedClusters', + 'extension_type': extension_type, + 'location': 'eastus2euap' + }) + + self.cmd('k8s-extension extension-types show -g {rg} -c {cluster_name} --cluster-type {cluster_type} ' + '--extension-type {extension_type}', checks=[ + self.check('name', '{extension_type}') + ]) + + extensionTypes_list = self.cmd('k8s-extension extension-types list -g {rg} -c {cluster_name} ' + '--cluster-type {cluster_type}').get_output_in_json() + assert len(extensionTypes_list) > 0 + + extensionTypes_locationList = self.cmd('k8s-extension extension-types list-by-location --location ' + '{location}').get_output_in_json() + assert len(extensionTypes_locationList) > 0 + + self.cmd('k8s-extension extension-types list-versions --location {location} --extension-type {extension_type}') From 929928f24d9a8266f0484919eeef95f99c75fa1b Mon Sep 17 00:00:00 2001 From: Deeksha Sharma Date: Mon, 17 Oct 2022 12:56:48 -0700 Subject: [PATCH 13/16] updating history.rst --- src/k8s-extension/HISTORY.rst | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/k8s-extension/HISTORY.rst b/src/k8s-extension/HISTORY.rst index 1de8cc95b23..ed03f785809 100644 --- a/src/k8s-extension/HISTORY.rst +++ b/src/k8s-extension/HISTORY.rst @@ -5,9 +5,8 @@ Release History 1.3.6 ++++++++++++++++++ -* k8s-extension updating the api version for extension type calls -* k8s-extension adding tests for all extension type calls -* k8s-extension fix to address the error TypeError: cf_k8s_extension() takes 1 positional argument but 2 were given while running all az k8s-extension extension-types commands +* Update the api version and add tests for extension type calls +* Fix the TypeError: cf_k8s_extension() takes 1 positional argument but 2 were given while running all az k8s-extension extension-types commands * microsoft.azuremonitor.containers: Update DCR creation to Clusters resource group instead of workspace * microsoft.dataprotection.kubernetes: Authoring a new k8s partner extension for the BCDR solution of AKS clusters From cbd30bf5aa3e45aeba694d3bea3040425b7f5e48 Mon Sep 17 00:00:00 2001 From: Shubham Sharma Date: Thu, 10 Nov 2022 10:37:23 +0530 Subject: [PATCH 14/16] [Dapr] Prompt user for existing Dapr installation during extension create (#188) * Add more validations and user prompt for existing installation scenario Signed-off-by: Shubham Sharma * Add Dapr test' Signed-off-by: Shubham Sharma * Handle stateful set Signed-off-by: Shubham Sharma * Update default handling Signed-off-by: Shubham Sharma * Fix HA handling Signed-off-by: Shubham Sharma * Add placement service todo Signed-off-by: Shubham Sharma * Add non-interactive mode Signed-off-by: Shubham Sharma * Fix lint Signed-off-by: Shubham Sharma * Update tests Signed-off-by: Shubham Sharma * Reset configuration for StatefulSet during k8s upgrade Signed-off-by: Shubham Sharma * Fix lint Signed-off-by: Shubham Sharma * Retrigger tests Signed-off-by: Shubham Sharma * Add changes to manage ha and placement params Signed-off-by: Shubham Sharma * Update message Signed-off-by: Shubham Sharma * nits Signed-off-by: Shubham Sharma Signed-off-by: Shubham Sharma --- .../partner_extensions/Dapr.py | 128 ++++++++++++++++-- .../latest/test_k8s_extension_scenario.py | 2 +- testing/test/extensions/public/Dapr.Tests.ps1 | 63 +++++++++ 3 files changed, 178 insertions(+), 15 deletions(-) create mode 100644 testing/test/extensions/public/Dapr.Tests.ps1 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 From 155fb0113f04fb23d56d833bc400fed4f397dd37 Mon Sep 17 00:00:00 2001 From: Bavneet Singh Date: Tue, 15 Nov 2022 12:18:57 -0800 Subject: [PATCH 15/16] bump k8s-extension version to 1.3.7 --- src/k8s-extension/HISTORY.rst | 4 ++++ src/k8s-extension/setup.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/k8s-extension/HISTORY.rst b/src/k8s-extension/HISTORY.rst index ed03f785809..bc7a6700258 100644 --- a/src/k8s-extension/HISTORY.rst +++ b/src/k8s-extension/HISTORY.rst @@ -3,6 +3,10 @@ Release History =============== +1.3.7 +++++++++++++++++++ +* microsoft.dapr: prompt user for existing dapr installation during extension create + 1.3.6 ++++++++++++++++++ * Update the api version and add tests for extension type calls diff --git a/src/k8s-extension/setup.py b/src/k8s-extension/setup.py index 4975bf63bd3..b0f1d82c1fd 100644 --- a/src/k8s-extension/setup.py +++ b/src/k8s-extension/setup.py @@ -33,7 +33,7 @@ # TODO: Add any additional SDK dependencies here DEPENDENCIES = [] -VERSION = "1.3.6" +VERSION = "1.3.7" with open("README.rst", "r", encoding="utf-8") as f: README = f.read() From 705ea0a7011ed418c51ff543b85ddebe4e2da1db Mon Sep 17 00:00:00 2001 From: bavneetsingh16 Date: Fri, 18 Nov 2022 15:21:32 -0800 Subject: [PATCH 16/16] remove unnecessary test files --- testing/.gitignore | 9 - testing/Bootstrap.ps1 | 80 ------- testing/Cleanup.ps1 | 30 --- testing/README.md | 128 ---------- testing/Test.ps1 | 133 ----------- .../k8s_configuration-1.0.0-py3-none-any.whl | Bin 42351 -> 0 bytes .../bin/k8s_extension-0.4.0-py3-none-any.whl | Bin 55464 -> 0 bytes testing/docs/test_authoring.md | 142 ----------- testing/owners.txt | 2 - testing/pipeline/k8s-custom-pipelines.yml | 212 ----------------- .../pipeline/templates/build-extension.yml | 52 ----- testing/pipeline/templates/run-test.yml | 104 --------- testing/settings.template.json | 11 - .../Configuration.HTTPS.Tests.ps1 | 54 ----- .../Configuration.HelmOperator.Tests.ps1 | 137 ----------- .../Configuration.KnownHost.Tests.ps1 | 6 - .../Configuration.PrivateKey.Tests.ps1 | 86 ------- .../configurations/Configuration.Tests.ps1 | 80 ------- testing/test/configurations/Constants.ps1 | 8 - testing/test/configurations/Helper.ps1 | 45 ---- .../extensions/data/azure_ml/test_cert.pem | 32 --- .../extensions/data/azure_ml/test_key.pem | 52 ----- .../extensions/public/AzureDefender.Tests.ps1 | 71 ------ .../public/AzureMLKubernetes.Tests.ps1 | 221 ------------------ .../extensions/public/AzureMonitor.Tests.ps1 | 68 ------ .../extensions/public/AzurePolicy.Tests.ps1 | 74 ------ .../extensions/public/Cassandra.Tests.ps1 | 98 -------- testing/test/extensions/public/Dapr.Tests.ps1 | 63 ----- .../public/ExtensionTypes.Tests.ps1 | 33 --- testing/test/extensions/public/Flux.Tests.ps1 | 63 ----- .../public/OpenServiceMesh.Tests.ps1 | 72 ------ testing/test/helper/Constants.ps1 | 7 - testing/test/helper/Helper.ps1 | 72 ------ 33 files changed, 2245 deletions(-) delete mode 100644 testing/.gitignore delete mode 100644 testing/Bootstrap.ps1 delete mode 100644 testing/Cleanup.ps1 delete mode 100644 testing/README.md delete mode 100644 testing/Test.ps1 delete mode 100644 testing/bin/k8s_configuration-1.0.0-py3-none-any.whl delete mode 100644 testing/bin/k8s_extension-0.4.0-py3-none-any.whl delete mode 100644 testing/docs/test_authoring.md delete mode 100644 testing/owners.txt delete mode 100644 testing/pipeline/k8s-custom-pipelines.yml delete mode 100644 testing/pipeline/templates/build-extension.yml delete mode 100644 testing/pipeline/templates/run-test.yml delete mode 100644 testing/settings.template.json delete mode 100644 testing/test/configurations/Configuration.HTTPS.Tests.ps1 delete mode 100644 testing/test/configurations/Configuration.HelmOperator.Tests.ps1 delete mode 100644 testing/test/configurations/Configuration.KnownHost.Tests.ps1 delete mode 100644 testing/test/configurations/Configuration.PrivateKey.Tests.ps1 delete mode 100644 testing/test/configurations/Configuration.Tests.ps1 delete mode 100644 testing/test/configurations/Constants.ps1 delete mode 100644 testing/test/configurations/Helper.ps1 delete mode 100644 testing/test/extensions/data/azure_ml/test_cert.pem delete mode 100644 testing/test/extensions/data/azure_ml/test_key.pem delete mode 100644 testing/test/extensions/public/AzureDefender.Tests.ps1 delete mode 100644 testing/test/extensions/public/AzureMLKubernetes.Tests.ps1 delete mode 100644 testing/test/extensions/public/AzureMonitor.Tests.ps1 delete mode 100644 testing/test/extensions/public/AzurePolicy.Tests.ps1 delete mode 100644 testing/test/extensions/public/Cassandra.Tests.ps1 delete mode 100644 testing/test/extensions/public/Dapr.Tests.ps1 delete mode 100644 testing/test/extensions/public/ExtensionTypes.Tests.ps1 delete mode 100644 testing/test/extensions/public/Flux.Tests.ps1 delete mode 100644 testing/test/extensions/public/OpenServiceMesh.Tests.ps1 delete mode 100644 testing/test/helper/Constants.ps1 delete mode 100644 testing/test/helper/Helper.ps1 diff --git a/testing/.gitignore b/testing/.gitignore deleted file mode 100644 index 5687a0bf32d..00000000000 --- a/testing/.gitignore +++ /dev/null @@ -1,9 +0,0 @@ -settings.json -tmp/ -bin/* -!bin/connectedk8s-1.0.0-py3-none-any.whl -!bin/k8s_extension-0.4.0-py3-none-any.whl -!bin/k8s_extension_private-0.1.0-py3-none-any.whl -!bin/k8s_configuration-1.0.0-py3-none-any.whl -!bin/connectedk8s-values.yaml -*.xml \ No newline at end of file diff --git a/testing/Bootstrap.ps1 b/testing/Bootstrap.ps1 deleted file mode 100644 index 0598b139c79..00000000000 --- a/testing/Bootstrap.ps1 +++ /dev/null @@ -1,80 +0,0 @@ -param ( - [switch] $SkipInstall, - [switch] $CI -) - -# Disable confirm prompt for script -az config set core.disable_confirm_prompt=true - -# Configuring the environment -$ENVCONFIG = Get-Content -Path $PSScriptRoot/settings.json | ConvertFrom-Json - -az account set --subscription $ENVCONFIG.subscriptionId - -if (-not (Test-Path -Path $PSScriptRoot/tmp)) { - New-Item -ItemType Directory -Path $PSScriptRoot/tmp -} - -if (!$SkipInstall) { - Write-Host "Removing the old connnectedk8s extension..." - az extension remove -n connectedk8s - Write-Host "Installing connectedk8s..." - az extension add -n connectedk8s - if (!$?) { - Write-Host "Unable to install connectedk8s, exiting..." - exit 1 - } -} - -Write-Host "Onboard cluster to Azure...starting!" - -az group show --name $envConfig.resourceGroup -if (!$?) { - Write-Host "Resource group does not exist, creating it now in region 'eastus2euap'" - az group create --name $envConfig.resourceGroup --location eastus2euap - - if (!$?) { - Write-Host "Failed to create Resource Group - exiting!" - Exit 1 - } -} - -# Skip creating the AKS Cluster if this is CI -if (!$CI) { - az aks show -g $ENVCONFIG.resourceGroup -n $ENVCONFIG.aksClusterName - if (!$?) { - Write-Host "Cluster does not exist, creating it now" - az aks create -g $ENVCONFIG.resourceGroup -n $ENVCONFIG.aksClusterName --generate-ssh-keys - } else { - Write-Host "Cluster already exists, no need to create it." - } - - Write-Host "Retrieving credentials for your AKS cluster..." - - az aks get-credentials -g $ENVCONFIG.resourceGroup -n $ENVCONFIG.aksClusterName -f tmp/KUBECONFIG - if (!$?) - { - Write-Host "Cluster did not create successfully, exiting!" -ForegroundColor Red - Exit 1 - } - Write-Host "Successfully retrieved the AKS kubectl credentials" -} else { - Copy-Item $HOME/.kube/config -Destination $PSScriptRoot/tmp/KUBECONFIG -} - -az connectedk8s show -g $ENVCONFIG.resourceGroup -n $ENVCONFIG.arcClusterName -if ($?) -{ - Write-Host "Cluster is already connected, no need to re-connect" - Exit 0 -} - -Write-Host "Connecting the cluster to Arc with connectedk8s..." -$Env:KUBECONFIG="$PSScriptRoot/tmp/KUBECONFIG" -az connectedk8s connect -g $ENVCONFIG.resourceGroup -n $ENVCONFIG.arcClusterName -if (!$?) -{ - kubectl get pods -A - Exit 1 -} -Write-Host "Successfully onboarded the cluster to Azure" \ No newline at end of file diff --git a/testing/Cleanup.ps1 b/testing/Cleanup.ps1 deleted file mode 100644 index cef1e2d1bc3..00000000000 --- a/testing/Cleanup.ps1 +++ /dev/null @@ -1,30 +0,0 @@ -param ( - [switch] $CI -) - -# Disable confirm prompt for script -az config set core.disable_confirm_prompt=true - -$ENVCONFIG = Get-Content -Path $PSScriptRoot/settings.json | ConvertFrom-Json - -az account set --subscription $ENVCONFIG.subscriptionId - -$Env:KUBECONFIG="$PSScriptRoot/tmp/KUBECONFIG" -Write-Host "Removing the connectedk8s arc agents from the cluster..." -az connectedk8s delete -g $ENVCONFIG.resourceGroup -n $ENVCONFIG.arcClusterName -if (!$?) -{ - kubectl get pods -A - kubectl logs -l app.kubernetes.io/component=cluster-metadata-operator -n azure-arc -c cluster-metadata-operator - Exit 0 -} - -# Skip deleting the AKS Cluster if this is CI -if (!$CI) { - Write-Host "Deleting the AKS cluster from Azure..." - az aks delete -g $ENVCONFIG.resourceGroup -n $ENVCONFIG.aksClusterName - if (Test-Path -Path $PSScriptRoot/tmp) { - Write-Host "Deleting the tmp directory from the test directory" - Remove-Item -Path $PSScriptRoot/tmp -Force -Confirm:$false - } -} \ No newline at end of file diff --git a/testing/README.md b/testing/README.md deleted file mode 100644 index 088a819a429..00000000000 --- a/testing/README.md +++ /dev/null @@ -1,128 +0,0 @@ -# K8s Partner Extension Test Suite - -This repository serves as the integration testing suite for the `k8s-extension` Azure CLI module. - -## Testing Requirements - -All partners who wish to merge their __Custom Private Preview Release__ (owner: _Partner_) into the __Official Private Preview Release__ are required to author additional integration tests for their extension to ensure that their extension will continue to function correctly as more extensions are added into the __Official Private Preview Release__. - -For more information on creating these tests, see [Authoring Tests](docs/test_authoring.md) - -## Pre-Requisites - -In order to properly test all regression tests within the test suite, you must onboard an AKS cluster which you will use to generate your Azure Arc resource to test the extensions. Ensure that you have a resource group where you can onboard this cluster. - -### Required Installations - -The following installations are required in your environment for the integration tests to run correctly: - -1. [Helm 3](https://helm.sh/docs/intro/install/) -2. [Kubectl](https://kubernetes.io/docs/tasks/tools/install-kubectl/) -3. [Azure CLI](https://docs.microsoft.com/en-us/cli/azure/install-azure-cli) - -## Setup - -### Step 1: Install Pester - -This project contains [Pester](https://pester.dev/) test framework commands that are required for the integration tests to run. In an admin powershell terminal, run - -```powershell -Install-Module Pester -Force -SkipPublisherCheck -Import-Module Pester -PassThru -``` - -If you run into issues installing the framework, refer to the [Installation Guide](https://pester.dev/docs/introduction/installation) provided by the Pester docs. - -### Step 2: Get Test suite files - -You can either clone this repo (preferred option, since you will be adding your tests to this suite) or copy the files in this repo locally. Rest of the instructions here assume your working directory is k8spartner-extension-testing. - -### Step 3: Generate `k8s-extension` .whl package - -You can perform the generation of the `k8s-extension` .whl package by running the following command - -```bash -azdev setup -r . -e k8s-extension -azdev extension build k8s-extension -``` - -Additional guidance for build the extension .whl package can be found [here](https://github.com/Azure/azure-cli/blob/master/doc/extensions/authoring.md#building) - -### Step 4: Update the `k8s-extension`/`k8s-extension-private` .whl package - -This integration test suite references the .whl packages found in the `\bin` directory. After generating your `k8s-extension`/`k8s-extension-private` .whl package, copy your updated package into the `\bin` directory. - - -### Step 5: Create a `settings.json` - -To onboard the AKS and Arc clusters correctly, you will need to create a `settings.json` configuration. Create a new `settings.json` file by copying the contents of the `settings.template.json` into this file. Update the subscription id, resource group, and AKS and Arc cluster name fields with your specific values. - -### Step 6: Update the extension version value in `settings.json` - -To ensure that the tests point to your `k8s-extension-private` `.whl` package, change the value of the `k8s-extension-private` to match your package versioning in the format (Major.Minor.Patch.Extension). For example, the `k8s_extension_private-0.1.0.openservicemesh_5-py3-none-any.whl` whl package would have extension versions set to -```json -{ - "k8s-extension": "0.1.0", - "k8s-extension-private": "0.1.0.openservicemesh_5", - "connectedk8s": "0.3.5" -} - -``` - -_Note: Updates to the `connectedk8s` version and `k8s-extension` version can also be made by adding a different version of the `connectedk8s` and `k8s-extension` whl packages and changing the `connectedk8s` and `k8s-extension` values to match the (Major.Minor.Patch) version format shown above_ - -### Step 7: Run the Bootstrap Command -To bootstrap the environment with AKS and Arc clusters, run -```powershell -.\Bootstrap.ps1 -``` -This script will provision the AKS and Arc clusters needed to run the integration test suite - -## Testing - -### Testing All Extension Suites -To test all extension test suites, you must call `.\Test.ps1` with the `-ExtensionType` parameter set to either `Public` or `Private`. Based on this flag, the test suite will install the extension type specified below - -| `-ExtensionType` | Installs `az extension` | -| ---------------- | --------------------- | -| `Public` | `k8s-extension` | -| `Private` | `k8s-extension-private` | - -For example, when calling -```bash -.\Test.ps1 -ExtensionType Public -``` -the script will install your `k8s-extension` whl package and run the full test suite of `*.Tests.ps1` files included in the `\test\extensions` directory - -### Testing Public Extensions Only -If you only want to run the test cases against public-preview or GA extension test cases, you can use the `-OnlyPublicTests` flag to specify this -```bash -.\Test.ps1 -ExtensionType Public -OnlyPublicTests -``` - -### Testing Specific Extension Suite - -If you only want to run the test script on your specific test file, you can do so by specifying path to your extension test suite in the execution call - -```powershell -.\Test.ps1 -Path -``` -For example to call the `AzureMonitor.Tests.ps1` test suite, we run -```powershell -.\Test.ps1 -ExtensionType Public -Path .\test\extensions\public\AzureMonitor.Tests.ps1 -``` - -### Skipping Extension Re-Install - -By default the `Test.ps1` script will uninstall any old versions of `k8s-extension`/'`k8s-extension-private` and re-install the version specified in `settings.json`. If you do not want this re-installation to occur, you can specify the `-SkipInstall` flag to skip this process. - -```powershell -.\Test.ps1 -ExtensionType Public -SkipInstall -``` - -## Cleanup -To cleanup the AKS and Arc clusters you have provisioned in testing, run -```powershell -.\Cleanup.ps1 -``` -This will remove the AKS and Arc clusters as well as the `\tmp` directory that were created by the bootstrapping script. \ No newline at end of file diff --git a/testing/Test.ps1 b/testing/Test.ps1 deleted file mode 100644 index c1d4f093b2e..00000000000 --- a/testing/Test.ps1 +++ /dev/null @@ -1,133 +0,0 @@ -param ( - [string] $Path, - [switch] $SkipInstall, - [switch] $CI, - [switch] $ParallelCI, - [switch] $OnlyPublicTests, - - [Parameter(Mandatory=$True)] - [ValidateSet('k8s-extension','k8s-configuration', 'k8s-extension-private')] - [string]$Type -) - -# Disable confirm prompt for script -# Only show errors, don't show warnings -az config set core.disable_confirm_prompt=true -az config set core.only_show_errors=true - -$ENVCONFIG = Get-Content -Path $PSScriptRoot/settings.json | ConvertFrom-Json - -az account set --subscription $ENVCONFIG.subscriptionId - -$Env:KUBECONFIG="$PSScriptRoot/tmp/KUBECONFIG" -$TestFileDirectory="$PSScriptRoot/results" - -if (-not (Test-Path -Path $TestFileDirectory)) { - New-Item -ItemType Directory -Path $TestFileDirectory -} - -if ($Type -eq 'k8s-extension') { - $k8sExtensionVersion = $ENVCONFIG.extensionVersion.'k8s-extension' - $Env:K8sExtensionName = "k8s-extension" - - if (!$SkipInstall) { - Write-Host "Removing the old k8s-extension extension..." - az extension remove -n k8s-extension - Write-Host "Installing k8s-extension version $k8sExtensionVersion..." - az extension add --source ./bin/k8s_extension-$k8sExtensionVersion-py3-none-any.whl - if (!$?) { - Write-Host "Unable to find k8s-extension version $k8sExtensionVersion, exiting..." - exit 1 - } - } - if ($OnlyPublicTests) { - $testFilePath = "$PSScriptRoot/test/extensions/public" - } else { - $testFilePath = "$PSScriptRoot/test/extensions" - } -} elseif ($Type -eq 'k8s-extension-private') { - $k8sExtensionPrivateVersion = $ENVCONFIG.extensionVersion.'k8s-extension-private' - $Env:K8sExtensionName = "k8s-extension-private" - - if (!$SkipInstall) { - Write-Host "Removing the old k8s-extension-private extension..." - az extension remove -n k8s-extension-private - Write-Host "Installing k8s-extension-private version $k8sExtensionPrivateVersion..." - az extension add --source ./bin/k8s_extension_private-$k8sExtensionPrivateVersion-py3-none-any.whl - if (!$?) { - Write-Host "Unable to find k8s-extension-private version $k8sExtensionPrivateVersion, exiting..." - exit 1 - } - } - if ($OnlyPublicTests) { - $testFilePath = "$PSScriptRoot/test/extensions/public" - } else { - $testFilePath = "$PSScriptRoot/test/extensions" - } -} elseif ($Type -eq 'k8s-configuration') { - $k8sConfigurationVersion = $ENVCONFIG.extensionVersion.'k8s-configuration' - if (!$SkipInstall) { - Write-Host "Removing the old k8s-configuration extension..." - az extension remove -n k8s-configuration - Write-Host "Installing k8s-configuration version $k8sConfigurationVersion..." - az extension add --source ./bin/k8s_configuration-$k8sConfigurationVersion-py3-none-any.whl - } - $testFilePaths = "$PSScriptRoot/test/configurations" -} - -if ($ParallelCI) { - # This runs the tests in parallel during the CI pipline to speed up testing - - Write-Host "Invoking Pester to run tests from '$testFilePath'..." - $testFiles = @() - foreach ($paths in $testFilePaths) - { - $temp = Get-ChildItem $paths - $testFiles += $temp - } - $resultFileNumber = 0 - foreach ($testFile in $testFiles) - { - $resultFileNumber++ - $testName = Split-Path $testFile –leaf - Start-Job -ArgumentList $testName, $testFile, $resultFileNumber, $TestFileDirectory -Name $testName -ScriptBlock { - param($name, $testFile, $resultFileNumber, $testFileDirectory) - - Write-Host "$testFile to result file #$resultFileNumber" - $testResult = Invoke-Pester $testFile -Passthru -Output Detailed - $testResult | Export-JUnitReport -Path "$testFileDirectory/$name.xml" - } - } - - do { - Write-Host ">> Still running tests @ $(Get-Date –Format "HH:mm:ss")" –ForegroundColor Blue - Get-Job | Where-Object { $_.State -eq "Running" } | Format-Table –AutoSize - Start-Sleep –Seconds 30 - } while((Get-Job | Where-Object { $_.State -eq "Running" } | Measure-Object).Count -ge 1) - - Get-Job | Wait-Job - $failedJobs = Get-Job | Where-Object { -not ($_.State -eq "Completed")} - Get-Job | Receive-Job –AutoRemoveJob –Wait –ErrorAction 'Continue' - - if ($failedJobs.Count -gt 0) { - Write-Host "Failed Jobs" –ForegroundColor Red - $failedJobs - throw "One or more tests failed" - } -} elseif ($CI) { - if ($Path) { - $testFilePath = "$PSScriptRoot/$Path" - } - Write-Host "Invoking Pester to run tests from '$testFilePath'..." - $testResult = Invoke-Pester $testFilePath -Passthru -Output Detailed - $testName = Split-Path $testFilePath –leaf - $testResult | Export-JUnitReport -Path "$testFileDirectory/$testName.xml" -} else { - if ($Path) { - Write-Host "Invoking Pester to run tests from '$PSScriptRoot/$Path'" - Invoke-Pester -Output Detailed $PSScriptRoot/$Path - } else { - Write-Host "Invoking Pester to run tests from '$testFilePath'..." - Invoke-Pester -Output Detailed $testFilePath - } -} diff --git a/testing/bin/k8s_configuration-1.0.0-py3-none-any.whl b/testing/bin/k8s_configuration-1.0.0-py3-none-any.whl deleted file mode 100644 index cc8e8e0995f81da31f6f919f83d344cc164e9568..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 42351 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%0De{h@2i2mo+>9hAiTOe4VIP9}qG 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 diff --git a/testing/docs/test_authoring.md b/testing/docs/test_authoring.md deleted file mode 100644 index b41286ba66d..00000000000 --- a/testing/docs/test_authoring.md +++ /dev/null @@ -1,142 +0,0 @@ -# Test Authoring - -All partners are _required_ to author additional integration tests when merging their extension into the __Official Private Preview Release__. The information below outlines how to setup and author these additional tests. - -## Requirements - -All partners are required to cover standard CLI scenarios in your extensions testing suite. When adding these tests and preparing to merge your updated extension whl package, your tests along with the other tests in the test suite must pass at 100%. - -Standard CLI scenarios include: - -1. `az k8s-extension create` -2. `az k8s-extension show` -3. `az k8s-extension list` -4. `az k8s-extension update` -5. `az k8s-extension delete` - -In addition to these standard scenarios, if there are any rigorous parameter validation standards, these should also be included in this test suite. - -## Setup - -The setup process for test authoring is the same as setup for generic testing. See [Setup](../README.md#setup) for guidance. - -## Writing Tests - -This section outlines the common flow for creating and running additional extension integration tests for the `k8s-extension` package. - -The suite utilizes the [Pester](https://pester.dev/) framework. For more information on creating generic Pester tests, see the [Create a Pester Test](https://pester.dev/docs/quick-start#creating-a-pester-test) section in the Pester docs. - -### Step 1: Create Test File - -To create an integration test suite for your extension, create an extension test file in the format `.Tests.ps1` and place the file in one of the following directories -| Extension Type | Directory | -| ---------------------- | ----------------------------------- | -| General Availability | .\test\extensions\public | -| Public Preview | .\test\extensions\public | -| Private Preview | .\test\extensions\private-preview | - -For example, to create a test suite file for the Azure Monitor extension, I create the file `AzureMonitor.Tests.ps1` in the `\test\extensions\public` directory because Container Insights extension is in _Public Preview_. - -### Step 2: Setup Global Variables - -All test suite files must have the following structure for importing the environment config and declaring globals - -```powershell -Describe ' Testing' { - BeforeAll { - $extensionType = "" - $extensionName = "" - $extensionAgentName = "" - $extensionAgentNamespace = "" - - . $PSScriptRoot/../../helper/Constants.ps1 - . $PSScriptRoot/../../helper/Helper.ps1 - } -} -``` - -You can declare additional global variables for your tests by adding additional powershell variable to this `BeforeAll` block. - -_Note: Commonly used constants used by all extension test suites are stored in the `Constants.ps1` file_ - -### Step 3: Add Tests - -Adding tests to the test suite can now be performed by adding `It` blocks to the outer `Describe` block. For instance to test create on a extension in the case of AzureMonitor, I write the following test: - -```powershell -Describe 'Azure Monitor Testing' { - BeforeAll { - $extensionType = "microsoft.azuremonitor.containers" - $extensionName = "azuremonitor-containers" - $extensionAgentName = "omsagent" - $extensionAgentNamespace = "kube-system" - - . $PSScriptRoot/../../helper/Constants.ps1 - . $PSScriptRoot/../../helper/Helper.ps1 - } - - It 'Creates the extension and checks that it onboards correctly' { - $output = az k8s-extension create -c $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type connectedClusters --extension-type $extensionType -n $extensionName - $? | Should -BeTrue - - $output = az k8s-extension show -c $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type connectedClusters -n $extensionName - $? | Should -BeTrue - - $isAutoUpgradeMinorVersion = ($output | ConvertFrom-Json).autoUpgradeMinorVersion - $isAutoUpgradeMinorVersion.ToString() -eq "True" | Should -BeTrue - - # Loop and retry until the extension installs - $n = 0 - do - { - if (Get-ExtensionStatus $extensionName -eq $SUCCESS_MESSAGE) { - if (Get-PodStatus $extensionAgentName -Namespace $extensionAgentNamespace -eq $POD_RUNNING) { - break - } - } - Start-Sleep -Seconds 10 - $n += 1 - } while ($n -le $MAX_RETRY_ATTEMPTS) - $n | Should -BeLessOrEqual $MAX_RETRY_ATTEMPTS - } -} -``` - -The above test calls `az k8s-extension create` to create the `azuremonitor-containers` extension and retries checking that the extension resource was actually created on the Arc cluster and that the extension status successfully returns `$SUCCESS_MESSAGE` which is equivalent to `Successfully installed the extension`. - -## Tips/Notes - -### Accessing Extension Data - -`.\Test.ps1` assumes that the user has `kubectl` and `az` installed in their environment; therefore, tests are able to access information on the extension at the service and on the arc cluster. For instance, in the above test, we access the `extensionconfig` CRDs on the arc cluster by calling - -```powershell -kubectl get extensionconfigs -A -o json -``` - -If we want to access the extension data on the cluster with a specific `$extensionName`, we run - -```powershell -(kubectl get extensionconfigs -A -o json).items | Where-Object { $_.metadata.name -eq $extensionName } -``` - -Because some of these commands are so common, we provide the following helper commands in the `test\Helper.ps1` file - -| Command | Description | -| ------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------- | -| Get-ExtensionData | Retrieves the ExtensionConfig CRD in JSON format with `.meatadata.name` matching the `extensionName` | -| Get-ExtensionStatus | Retrieves the `.status.status` from the ExtensionConfig CRD with `.meatadata.name` matching the `extensionName` | -| Get-PodStatus -Namespace | Retrieves the `status.phase` from the first pod on the cluster with `.metadata.name` matching `extensionName` | - -### Stdout for Debugging - -To print out to the Console for debugging while writing your test cases use the `Write-Host` command. If you attempt to use the `Write-Output` command, it will not show because of the way that Pester is invoked - -```powershell -Write-Host "Some example output" -``` - -### Global Constants - -Looking at the above test, we can see that we are accessing the `ENVCONFIG` to retrieve the environment variables from the `settings.json`. All variables in the `settings.json` are accessible from the `ENVCONFIG`. The most useful ones for testing will be `ENVCONFIG.arcClusterName` and `ENVCONFIG.resourceGroup`. - diff --git a/testing/owners.txt b/testing/owners.txt deleted file mode 100644 index ead6f446410..00000000000 --- a/testing/owners.txt +++ /dev/null @@ -1,2 +0,0 @@ -joinnis -nanthi \ No newline at end of file diff --git a/testing/pipeline/k8s-custom-pipelines.yml b/testing/pipeline/k8s-custom-pipelines.yml deleted file mode 100644 index c9673e2af66..00000000000 --- a/testing/pipeline/k8s-custom-pipelines.yml +++ /dev/null @@ -1,212 +0,0 @@ -trigger: - batch: true - branches: - include: - - k8s-extension/public - - k8s-extension/private -pr: - branches: - include: - - k8s-extension/public - - k8s-extension/private - -stages: -- stage: BuildTestPublishExtension - displayName: "Build, Test, and Publish Extension" - variables: - TEST_PATH: $(Agent.BuildDirectory)/s/testing - CLI_REPO_PATH: $(Agent.BuildDirectory)/s - SUBSCRIPTION_ID: "15c06b1b-01d6-407b-bb21-740b8617dea3" - RESOURCE_GROUP: "K8sPartnerExtensionTest" - BASE_CLUSTER_NAME: "k8s-extension-cluster" - IS_PRIVATE_BRANCH: $[or(eq(variables['Build.SourceBranch'], 'refs/heads/k8s-extension/private'), eq(variables['System.PullRequest.TargetBranch'], 'k8s-extension/private'))] - jobs: - - template: ./templates/run-test.yml - parameters: - jobName: AzureDefender - path: ./test/extensions/public/AzureDefender.Tests.ps1 - - template: ./templates/run-test.yml - parameters: - jobName: AzureMLKubernetes - path: ./test/extensions/public/AzureMLKubernetes.Tests.ps1 - - template: ./templates/run-test.yml - parameters: - jobName: AzureMonitor - path: ./test/extensions/public/AzureMonitor.Tests.ps1 - - template: ./templates/run-test.yml - parameters: - jobName: AzurePolicy - path: ./test/extensions/public/AzurePolicy.Tests.ps1 - - template: ./templates/run-test.yml - parameters: - jobName: Cassandra - path: ./test/extensions/public/Cassandra.Tests.ps1 - - template: ./templates/run-test.yml - parameters: - jobName: ExtensionTypes - path: ./test/extensions/public/ExtensionTypes.Tests.ps1 - - template: ./templates/run-test.yml - parameters: - jobName: OpenServiceMesh - path: ./test/extensions/public/OpenServiceMesh.Tests.ps1 - - template: ./templates/run-test.yml - parameters: - jobName: Flux - path: ./test/extensions/public/Flux.Tests.ps1 - - job: BuildPublishExtension - pool: - vmImage: 'ubuntu-latest' - displayName: "Build and Publish the Extension Artifact" - variables: - CLI_REPO_PATH: $(Agent.BuildDirectory)/s - workingDirectory: $(CLI_REPO_PATH) - displayName: "Setup and Build Extension with azdev" - steps: - - template: ./templates/build-extension.yml - parameters: - CLI_REPO_PATH: $(CLI_REPO_PATH) - IS_PRIVATE_BRANCH: $(IS_PRIVATE_BRANCH) - - task: PublishBuildArtifacts@1 - inputs: - pathToPublish: $(CLI_REPO_PATH)/dist - -- stage: AzureCLIOfficial - displayName: "Azure Official CLI Code Checks" - dependsOn: [] - jobs: - - job: CheckLicenseHeader - displayName: "Check License" - pool: - vmImage: 'ubuntu-latest' - steps: - - task: UsePythonVersion@0 - displayName: 'Use Python 3.10' - inputs: - versionSpec: 3.10 - - bash: | - set -ev - - # prepare and activate virtualenv - python -m venv env/ - - chmod +x ./env/bin/activate - source ./env/bin/activate - - # clone azure-cli - git clone -q --single-branch -b dev https://github.com/Azure/azure-cli.git ../azure-cli - - pip install --upgrade pip - pip install -q azdev - - azdev setup -c ../azure-cli -r ./ - - azdev --version - az --version - - azdev verify license - - - job: StaticAnalysis - displayName: "Static Analysis" - pool: - vmImage: 'ubuntu-latest' - steps: - - task: UsePythonVersion@0 - displayName: 'Use Python 3.6' - inputs: - versionSpec: 3.6 - - bash: pip install wheel==0.30.0 pylint==1.9.5 flake8==3.5.0 requests - displayName: 'Install wheel, pylint, flake8, requests' - - bash: python scripts/ci/source_code_static_analysis.py - displayName: "Static Analysis" - - - job: IndexVerify - displayName: "Verify Extensions Index" - pool: - vmImage: 'ubuntu-latest' - steps: - - task: UsePythonVersion@0 - displayName: 'Use Python 3.10' - inputs: - versionSpec: 3.10 - - bash: | - #!/usr/bin/env bash - set -ev - pip install wheel==0.30.0 requests packaging - export CI="ADO" - python ./scripts/ci/test_index.py -v - displayName: "Verify Extensions Index" - - - job: SourceTests - displayName: "Integration Tests, Build Tests" - pool: - vmImage: 'ubuntu-latest' - strategy: - matrix: - Python38: - python.version: '3.8' - Python39: - python.version: '3.9' - Python310: - python.version: '3.10' - steps: - - task: UsePythonVersion@0 - displayName: 'Use Python $(python.version)' - inputs: - versionSpec: '$(python.version)' - - bash: pip install wheel==0.30.0 - displayName: 'Install wheel==0.30.0' - - bash: | - set -ev - - # prepare and activate virtualenv - pip install virtualenv - python -m virtualenv venv/ - source ./venv/bin/activate - - # clone azure-cli - git clone --single-branch -b dev https://github.com/Azure/azure-cli.git ../azure-cli - - pip install --upgrade pip - pip install azdev - - azdev --version - - azdev setup -c ../azure-cli -r ./ -e k8s-extension - azdev test k8s-extension - displayName: 'Run integration test and build test' - - - job: LintModifiedExtensions - displayName: "CLI Linter on Modified Extensions" - pool: - vmImage: 'ubuntu-latest' - steps: - - task: UsePythonVersion@0 - displayName: 'Use Python 3.10' - inputs: - versionSpec: 3.10 - - bash: | - set -ev - - # prepare and activate virtualenv - pip install virtualenv - python -m virtualenv venv/ - source ./venv/bin/activate - - # clone azure-cli - git clone --single-branch -b dev https://github.com/Azure/azure-cli.git ../azure-cli - - pip install --upgrade pip - pip install azdev - - azdev --version - - azdev setup -c ../azure-cli -r ./ -e k8s-extension - - # overwrite the default AZURE_EXTENSION_DIR set by ADO - AZURE_EXTENSION_DIR=~/.azure/cliextensions az --version - - AZURE_EXTENSION_DIR=~/.azure/cliextensions azdev linter --include-whl-extensions k8s-extension - displayName: "CLI Linter on Modified Extension" - env: - ADO_PULL_REQUEST_LATEST_COMMIT: $(System.PullRequest.SourceCommitId) - ADO_PULL_REQUEST_TARGET_BRANCH: $(System.PullRequest.TargetBranch) \ No newline at end of file diff --git a/testing/pipeline/templates/build-extension.yml b/testing/pipeline/templates/build-extension.yml deleted file mode 100644 index cf63db635a6..00000000000 --- a/testing/pipeline/templates/build-extension.yml +++ /dev/null @@ -1,52 +0,0 @@ -parameters: - CLI_REPO_PATH: "" -steps: -- bash: | - echo "Using the private preview of k8s-extension to build..." - - cp ${{ parameters.CLI_REPO_PATH }}/src/k8s-extension ${{ parameters.CLI_REPO_PATH }}/src/k8s-extension-private -r - mv ${{ parameters.CLI_REPO_PATH }}/src/k8s-extension-private/azext_k8s_extension ${{ parameters.CLI_REPO_PATH }}/src/k8s-extension-private/azext_k8s_extension_private - cp ${{ parameters.CLI_REPO_PATH }}/src/k8s-extension-private/setup_private.py ${{ parameters.CLI_REPO_PATH }}/src/k8s-extension-private/setup.py - cp ${{ parameters.CLI_REPO_PATH }}/src/k8s-extension-private/azext_k8s_extension_private/consts_private.py ${{ parameters.CLI_REPO_PATH }}/src/k8s-extension-private/azext_k8s_extension_private/consts.py - - EXTENSION_NAME="k8s-extension-private" - EXTENSION_FILE_NAME="k8s_extension_private" - - echo "##vso[task.setvariable variable=EXTENSION_NAME]$EXTENSION_NAME" - echo "##vso[task.setvariable variable=EXTENSION_FILE_NAME]$EXTENSION_FILE_NAME" - condition: and(succeeded(), eq(variables['IS_PRIVATE_BRANCH'], 'True')) - displayName: "Copy Files, Set Variables for k8s-extension-private" -- bash: | - echo "Using the public version of k8s-extension to build..." - - EXTENSION_NAME="k8s-extension" - EXTENSION_FILE_NAME="k8s_extension" - - echo "##vso[task.setvariable variable=EXTENSION_NAME]$EXTENSION_NAME" - echo "##vso[task.setvariable variable=EXTENSION_FILE_NAME]$EXTENSION_FILE_NAME" - condition: and(succeeded(), eq(variables['IS_PRIVATE_BRANCH'], 'False')) - displayName: "Copy Files, Set Variables for k8s-extension" -- task: UsePythonVersion@0 - displayName: 'Use Python 3.6' - inputs: - versionSpec: 3.6 -- bash: | - set -ev - echo "Building extension ${EXTENSION_NAME}..." - - # prepare and activate virtualenv - pip install virtualenv - python3 -m venv env/ - source env/bin/activate - - # clone azure-cli - git clone -q --single-branch -b dev https://github.com/Azure/azure-cli.git ../azure-cli - - pip install --upgrade pip - pip install -q azdev - - ls ${{ parameters.CLI_REPO_PATH }} - - azdev --version - azdev setup -c ../azure-cli -r ${{ parameters.CLI_REPO_PATH }} -e $(EXTENSION_NAME) - azdev extension build $(EXTENSION_NAME) \ No newline at end of file diff --git a/testing/pipeline/templates/run-test.yml b/testing/pipeline/templates/run-test.yml deleted file mode 100644 index a9950f09063..00000000000 --- a/testing/pipeline/templates/run-test.yml +++ /dev/null @@ -1,104 +0,0 @@ -parameters: - jobName: '' - path: '' - -jobs: -- job: ${{ parameters.jobName}} - pool: - vmImage: 'ubuntu-latest' - variables: - ${{ if eq(variables['IS_PRIVATE_BRANCH'], 'False') }}: - EXTENSION_NAME: "k8s-extension" - EXTENSION_FILE_NAME: "k8s_extension" - ${{ if ne(variables['IS_PRIVATE_BRANCH'], 'False') }}: - EXTENSION_NAME: "k8s-extension-private" - EXTENSION_FILE_NAME: "k8s_extension_private" - steps: - - bash: | - echo "Installing helm3" - curl -fsSL -o get_helm.sh https://raw.githubusercontent.com/helm/helm/master/scripts/get-helm-3 - chmod 700 get_helm.sh - ./get_helm.sh --version v3.6.3 - - echo "Installing kubectl" - curl -LO "https://storage.googleapis.com/kubernetes-release/release/$(curl -s https://storage.googleapis.com/kubernetes-release/release/stable.txt)/bin/linux/amd64/kubectl" - chmod +x ./kubectl - sudo mv ./kubectl /usr/local/bin/kubectl - kubectl version --client - displayName: "Setup the VM with helm3 and kubectl" - - template: ./build-extension.yml - parameters: - CLI_REPO_PATH: $(CLI_REPO_PATH) - - bash: | - K8S_EXTENSION_VERSION=$(ls ${EXTENSION_FILE_NAME}* | cut -d "-" -f2) - echo "##vso[task.setvariable variable=K8S_EXTENSION_VERSION]$K8S_EXTENSION_VERSION" - cp * $(TEST_PATH)/bin - workingDirectory: $(CLI_REPO_PATH)/dist - displayName: "Copy the Built .whl to Extension Test Path" - - - bash: | - RAND_STR=$RANDOM - AKS_CLUSTER_NAME="${BASE_CLUSTER_NAME}-${RAND_STR}-aks" - ARC_CLUSTER_NAME="${BASE_CLUSTER_NAME}-${RAND_STR}-arc" - - JSON_STRING=$(jq -n \ - --arg SUB_ID "$SUBSCRIPTION_ID" \ - --arg RG "$RESOURCE_GROUP" \ - --arg AKS_CLUSTER_NAME "$AKS_CLUSTER_NAME" \ - --arg ARC_CLUSTER_NAME "$ARC_CLUSTER_NAME" \ - --arg K8S_EXTENSION_VERSION "$K8S_EXTENSION_VERSION" \ - '{subscriptionId: $SUB_ID, resourceGroup: $RG, aksClusterName: $AKS_CLUSTER_NAME, arcClusterName: $ARC_CLUSTER_NAME, extensionVersion: {"k8s-extension": $K8S_EXTENSION_VERSION, "k8s-extension-private": $K8S_EXTENSION_VERSION, connectedk8s: "1.0.0"}}') - echo $JSON_STRING > settings.json - cat settings.json - workingDirectory: $(TEST_PATH) - displayName: "Generate a settings.json file" - - - bash : | - echo "Downloading the kind script" - curl -Lo ./kind https://kind.sigs.k8s.io/dl/v0.11.1/kind-linux-amd64 - chmod +x ./kind - ./kind create cluster - displayName: "Create and Start the Kind cluster" - - - bash: | - curl -sL https://aka.ms/InstallAzureCLIDeb | sudo bash - displayName: "Upgrade az to latest version" - - - task: AzureCLI@2 - displayName: Bootstrap - inputs: - azureSubscription: AzureResourceConnection - scriptType: pscore - scriptLocation: inlineScript - inlineScript: | - .\Bootstrap.ps1 -CI - workingDirectory: $(TEST_PATH) - - - task: AzureCLI@2 - displayName: Run the Test Suite for ${{ parameters.path }} - inputs: - azureSubscription: AzureResourceConnection - scriptType: pscore - scriptLocation: inlineScript - inlineScript: | - .\Test.ps1 -CI -Path ${{ parameters.path }} -Type $(EXTENSION_NAME) - workingDirectory: $(TEST_PATH) - continueOnError: true - - - task: PublishTestResults@2 - inputs: - testResultsFormat: 'JUnit' - testResultsFiles: '**/testing/results/*.xml' - failTaskOnFailedTests: true - condition: succeededOrFailed() - - - task: AzureCLI@2 - displayName: Cleanup - inputs: - azureSubscription: AzureResourceConnection - scriptType: pscore - scriptLocation: inlineScript - inlineScript: | - .\Cleanup.ps1 -CI - workingDirectory: $(TEST_PATH) - condition: succeededOrFailed() \ No newline at end of file diff --git a/testing/settings.template.json b/testing/settings.template.json deleted file mode 100644 index 5129dbd0a20..00000000000 --- a/testing/settings.template.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "subscriptionId": "", - "resourceGroup": "", - "aksClusterName": "", - "arcClusterName": "", - - "extensionVersion": { - "k8s-extension": "0.3.0", - "k8s-extension-private": "0.1.0" - } -} \ No newline at end of file diff --git a/testing/test/configurations/Configuration.HTTPS.Tests.ps1 b/testing/test/configurations/Configuration.HTTPS.Tests.ps1 deleted file mode 100644 index a2dee2b348f..00000000000 --- a/testing/test/configurations/Configuration.HTTPS.Tests.ps1 +++ /dev/null @@ -1,54 +0,0 @@ -Describe 'Source Control Configuration (HTTPS) Testing' { - BeforeAll { - $configurationName = "https-config" - . $PSScriptRoot/Constants.ps1 - . $PSScriptRoot/Helper.ps1 - - $dummyValue = "dummyValue" - $secretName = "git-auth-$configurationName" - } - - It 'Creates a configuration with https user and https key on the cluster' { - $output = az k8s-configuration create -c $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type "connectedClusters" -u "https://github.com/Azure/arc-k8s-demo" -n $configurationName --scope cluster --https-user $dummyValue --https-key $dummyValue --operator-namespace $configurationName - $? | Should -BeTrue - - # Loop and retry until the configuration installs and helm pod comes up - $n = 0 - do - { - if (Get-ConfigStatus $configurationName -eq $SUCCESS_MESSAGE) { - if (Get-PodStatus $configurationName -Namespace $configurationName -eq $POD_RUNNING ) { - break - } - } - Start-Sleep -Seconds 10 - $n += 1 - } while ($n -le $MAX_RETRY_ATTEMPTS) - $n | Should -BeLessOrEqual $MAX_RETRY_ATTEMPTS - - Secret-Exists $secretName -Namespace $configurationName - } - - It "Lists the configurations on the cluster" { - $output = az k8s-configuration list -c $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type connectedClusters - $? | Should -BeTrue - - $configExists = $output | ConvertFrom-Json | Where-Object { $_.id -Match $configurationName } - $configExists | Should -Not -BeNullOrEmpty - } - - It "Deletes the configuration from the cluster" { - az k8s-configuration delete -c $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type connectedClusters -n $configurationName - $? | Should -BeTrue - - # Configuration should be removed from the resource model - az k8s-configuration show -c $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type connectedClusters -n $configurationName - $? | Should -BeFalse - } - - It "Performs another list after the delete" { - $output = az k8s-configuration list -c $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type connectedClusters - $configExists = $output | ConvertFrom-Json | Where-Object { $_.id -Match $configurationName } - $configExists | Should -BeNullOrEmpty - } -} \ No newline at end of file diff --git a/testing/test/configurations/Configuration.HelmOperator.Tests.ps1 b/testing/test/configurations/Configuration.HelmOperator.Tests.ps1 deleted file mode 100644 index 8b89ba24c58..00000000000 --- a/testing/test/configurations/Configuration.HelmOperator.Tests.ps1 +++ /dev/null @@ -1,137 +0,0 @@ -Describe 'Source Control Configuration (Helm Operator Properties) Testing' { - BeforeAll { - $configurationName = "helm-enabled-config" - . $PSScriptRoot/Constants.ps1 - . $PSScriptRoot/Helper.ps1 - - $customOperatorParams = "--set helm.versions=v3 --set mycustomhelmvalue=yay" - $customChartVersion = "0.6.0" - } - - It 'Creates a configuration with helm enabled on the cluster' { - $output = az k8s-configuration create -c $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type "connectedClusters" -u "https://github.com/Azure/arc-k8s-demo" -n $configurationName --scope cluster --enable-helm-operator --operator-namespace $configurationName --helm-operator-params "--set helm.versions=v3" - $? | Should -BeTrue - - # Loop and retry until the configuration installs and helm pod comes up - $n = 0 - do - { - if (Get-ConfigStatus $configurationName -eq $SUCCESS_MESSAGE) { - if (Get-PodStatus "$configurationName-helm" -Namespace $configurationName -eq $POD_RUNNING ) { - break - } - } - Start-Sleep -Seconds 10 - $n += 1 - } while ($n -le $MAX_RETRY_ATTEMPTS) - $n | Should -BeLessOrEqual $MAX_RETRY_ATTEMPTS - } - - It "Updates the helm operator params and performs a show" { - Set-ItResult -Skipped -Because "Update is not a valid scenario for now" - - az k8s-configuration update -c $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type "connectedClusters" -n $configurationName --helm-operator-params $customOperatorParams - $? | Should -BeTrue - - $output = az k8s-configuration show --cluster-name $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type connectedClusters -n $configurationName - $? | Should -BeTrue - - $configData = $output | ConvertFrom-Json | Where-Object { $_.id -Match $configurationName } - ($configData.helmOperatorProperties.chartValues -eq $customOperatorParams) | Should -BeTrue - - # Loop and retry until the configuration updates - $n = 0 - do - { - $helmOperatorChartValues = (Get-ConfigData $configurationName).spec.helmOperatorProperties.chartValues - if ($helmOperatorChartValues -ne $null -And $helmOperatorChartValues.ToString() -eq $customOperatorParams) { - if (Get-ConfigStatus $configurationName -Match $SUCCESS_MESSAGE) { - break - } - } - Start-Sleep -Seconds 10 - $n += 1 - } while ($n -le $MAX_RETRY_ATTEMPTS) - $n | Should -BeLessOrEqual $MAX_RETRY_ATTEMPTS - } - - It "Updates the helm operator chart version and performs a show" { - Set-ItResult -Skipped -Because "Update is not a valid scenario for now" - - az k8s-configuration update -c $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type "connectedClusters" -n $configurationName --helm-operator-chart-version $customChartVersion - $? | Should -BeTrue - - $output = az k8s-configuration show --cluster-name $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type connectedClusters -n $configurationName - $? | Should -BeTrue - - # Check that the helmOperatorProperties chartValues didn't change - $configData = $output | ConvertFrom-Json | Where-Object { $_.id -Match $configurationName } - ($configData.helmOperatorProperties.chartValues -eq $customOperatorParams) | Should -BeTrue - ($configData.helmOperatorProperties.chartVersion -eq $customChartVersion) | Should -BeTrue - - # Loop and retry until the configuration updates - $n = 0 - do - { - $helmOperatorChartVersion = (Get-ConfigData $configurationName).spec.helmOperatorProperties.chartVersion - if ($helmOperatorChartVersion -ne $null -And $helmOperatorChartVersion.ToString() -eq $customChartVersion) { - if (Get-ConfigStatus $configurationName -Match $SUCCESS_MESSAGE) { - break - } - } - Start-Sleep -Seconds 10 - $n += 1 - } while ($n -le $MAX_RETRY_ATTEMPTS) - $n | Should -BeLessOrEqual $MAX_RETRY_ATTEMPTS - } - - It "Disables the helm operator on the cluster" { - Set-ItResult -Skipped -Because "Update is not a valid scenario for now" - - az k8s-configuration update -c $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type "connectedClusters" -n $configurationName --enable-helm-operator=false - $? | Should -BeTrue - - $output = az k8s-configuration show --cluster-name $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type connectedClusters -n $configurationName - $? | Should -BeTrue - - $helmOperatorEnabled = ($output | ConvertFrom-Json).enableHelmOperator - $helmOperatorEnabled.ToString() -eq "False" | Should -BeTrue - - # Loop and retry until the configuration updates - $n = 0 - do { - $helmOperatorEnabled = (Get-ConfigData $configurationName).spec.enableHelmOperator - if ($helmOperatorEnabled -ne $null -And $helmOperatorEnabled.ToString() -eq "False") { - if (Get-ConfigStatus $configurationName -Match $SUCCESS_MESSAGE) { - break - } - } - Start-Sleep -Seconds 10 - $n += 1 - } while ($n -le $MAX_RETRY_ATTEMPTS) - $n | Should -BeLessOrEqual $MAX_RETRY_ATTEMPTS - } - - It "Lists the configurations on the cluster" { - $output = az k8s-configuration list -c $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type connectedClusters - $? | Should -BeTrue - - $configExists = $output | ConvertFrom-Json | Where-Object { $_.id -Match $configurationName } - $configExists | Should -Not -BeNullOrEmpty - } - - It "Deletes the configuration from the cluster" { - az k8s-configuration delete -c $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type connectedClusters -n $configurationName - $? | Should -BeTrue - - # Configuration should be removed from the resource model - az k8s-configuration show -c $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type connectedClusters -n $configurationName - $? | Should -BeFalse - } - - It "Performs another list after the delete" { - $output = az k8s-configuration list -c $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type connectedClusters - $configExists = $output | ConvertFrom-Json | Where-Object { $_.id -Match $configurationName } - $configExists | Should -BeNullOrEmpty - } -} \ No newline at end of file diff --git a/testing/test/configurations/Configuration.KnownHost.Tests.ps1 b/testing/test/configurations/Configuration.KnownHost.Tests.ps1 deleted file mode 100644 index 2cb2946bc3e..00000000000 --- a/testing/test/configurations/Configuration.KnownHost.Tests.ps1 +++ /dev/null @@ -1,6 +0,0 @@ -Describe 'Source Control Configuration (SSH Configs) Testing' { - BeforeAll { - . $PSScriptRoot/Constants.ps1 - . $PSScriptRoot/Helper.ps1 - } -} \ No newline at end of file diff --git a/testing/test/configurations/Configuration.PrivateKey.Tests.ps1 b/testing/test/configurations/Configuration.PrivateKey.Tests.ps1 deleted file mode 100644 index 4bf86d52012..00000000000 --- a/testing/test/configurations/Configuration.PrivateKey.Tests.ps1 +++ /dev/null @@ -1,86 +0,0 @@ -Describe 'Source Control Configuration (SSH Configs) Testing' { - BeforeAll { - . $PSScriptRoot/Constants.ps1 - . $PSScriptRoot/Helper.ps1 - - $RSA_KEYPATH = "$TMP_DIRECTORY\rsa.private" - $DSA_KEYPATH = "$TMP_DIRECTORY\dsa.private" - $ECDSA_KEYPATH = "$TMP_DIRECTORY\ecdsa.private" - $ED25519_KEYPATH = "$TMP_DIRECTORY\ed25519.private" - - $KEY_ARR = [System.Tuple]::Create("rsa", $RSA_KEYPATH), [System.Tuple]::Create("dsa", $DSA_KEYPATH), [System.Tuple]::Create("ecdsa", $ECDSA_KEYPATH), [System.Tuple]::Create("ed25519", $ED25519_KEYPATH) - foreach ($keyTuple in $KEY_ARR) { - # Automattically say yes to overwrite with ssh-keygen - Write-Output "y" | ssh-keygen -t $keyTuple.Item1 -f $keyTuple.Item2 -P """" - } - - $SSH_GIT_URL = "git://github.com/anubhav929/flux-get-started.git" - $HTTP_GIT_URL = "https://github.com/Azure/arc-k8s-demo" - - $configDataRSA = [System.Tuple]::Create("rsa-config", $RSA_KEYPATH) - $configDataDSA = [System.Tuple]::Create("dsa-config", $DSA_KEYPATH) - $configDataECDSA = [System.Tuple]::Create("ecdsa-config", $ECDSA_KEYPATH) - $configDataED25519 = [System.Tuple]::Create("ed25519-config", $ED25519_KEYPATH) - - $CONFIG_ARR = $configDataRSA, $configDataDSA, $configDataECDSA, $configDataED25519 - } - - It 'Creates a configuration with each type of ssh private key' { - foreach($configData in $CONFIG_ARR) { - az k8s-configuration create -c $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type "connectedClusters" -u $SSH_GIT_URL -n $configData.Item1 --scope cluster --operator-namespace $configData.Item1 --ssh-private-key-file $configData.Item2 - $? | Should -BeTrue - } - - # Loop and retry until the configuration installs and helm pod comes up - $n = 0 - do - { - $readyConfigs = 0 - foreach($configData in $CONFIG_ARR) { - # TODO: Change this to checking the success message after we merge in the bugfix into the agent - if (Get-PodStatus $configData.Item1 -Namespace $configData.Item1 -eq $POD_RUNNING) { - $readyConfigs += 1 - } - } - Start-Sleep -Seconds 10 - $n += 1 - } while ($n -le 30 -And $readyConfigs -ne 4) - $n | Should -BeLessOrEqual 30 - } - - It 'Fails when trying to create a configuration with ssh url and https auth values' { - az k8s-configuration create -c $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type "connectedClusters" -u $HTTP_GIT_URL -n "config-should-fail" --scope cluster --operator-namespace "config-should-fail" --ssh-private-key-file $RSA_KEYPATH - $? | Should -BeFalse - } - - It "Lists the configurations on the cluster" { - $output = az k8s-configuration list -c $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type connectedClusters - $? | Should -BeTrue - - foreach ($configData in $CONFIG_ARR) { - $configExists = $output | ConvertFrom-Json | Where-Object { $_.id -Match $configData.Item1 } - $configExists | Should -Not -BeNullOrEmpty - } - } - - It "Deletes the configuration from the cluster" { - foreach ($configData in $CONFIG_ARR) { - az k8s-configuration delete -c $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type connectedClusters -n $configData.Item1 - $? | Should -BeTrue - - # Configuration should be removed from the resource model - az k8s-configuration show -c $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type connectedClusters -n $configData.Item1 - $? | Should -BeFalse - } - } - - It "Performs another list after the delete" { - $output = az k8s-configuration list -c $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type connectedClusters - $? | Should -BeTrue - - foreach ($configData in $CONFIG_ARR) { - $configExists = $output | ConvertFrom-Json | Where-Object { $_.id -Match $configData.Item1 } - $configExists | Should -BeNullOrEmpty - } - } -} \ No newline at end of file diff --git a/testing/test/configurations/Configuration.Tests.ps1 b/testing/test/configurations/Configuration.Tests.ps1 deleted file mode 100644 index a85df42ed2e..00000000000 --- a/testing/test/configurations/Configuration.Tests.ps1 +++ /dev/null @@ -1,80 +0,0 @@ -Describe 'Basic Source Control Configuration Testing' { - BeforeAll { - $configurationName = "basic-config" - . $PSScriptRoot/Constants.ps1 - . $PSScriptRoot/Helper.ps1 - } - - It 'Creates a configuration and checks that it onboards correctly' { - az k8s-configuration create -c $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type "connectedClusters" -u "https://github.com/Azure/arc-k8s-demo" -n $configurationName --scope cluster --enable-helm-operator=false --operator-namespace $configurationName - $? | Should -BeTrue - - # Loop and retry until the configuration installs - $n = 0 - do - { - if (Get-ConfigStatus $configurationName -Match $SUCCESS_MESSAGE) { - break - } - Start-Sleep -Seconds 10 - $n += 1 - } while ($n -le $MAX_RETRY_ATTEMPTS) - $n | Should -BeLessOrEqual $MAX_RETRY_ATTEMPTS - } - - It "Performs a show on the configuration" { - $output = az k8s-configuration show -c $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type "connectedClusters" -n $configurationName - $? | Should -BeTrue - $output | Should -Not -BeNullOrEmpty - } - - It "Runs an update on the configuration on the cluster" { - Set-ItResult -Skipped -Because "Update is not a valid scenario for now" - - az k8s-configuration update -c $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type connectedClusters -n $configurationName --enable-helm-operator - $? | Should -BeTrue - - $output = az k8s-configuration show --cluster-name $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type connectedClusters -n $configurationName - $? | Should -BeTrue - - $helmOperatorEnabled = ($output | ConvertFrom-Json).enableHelmOperator - $helmOperatorEnabled.ToString() -eq "True" | Should -BeTrue - - # Loop and retry until the configuration updates - $n = 0 - do { - $helmOperatorEnabled = (Get-ConfigData $configurationName).spec.enableHelmOperator - if ($helmOperatorEnabled -And $helmOperatorEnabled.ToString() -eq "True") { - if (Get-ConfigStatus $configurationName -Match $SUCCESS_MESSAGE) { - break - } - } - Start-Sleep -Seconds 10 - $n += 1 - } while ($n -le $MAX_RETRY_ATTEMPTS) - $n | Should -BeLessOrEqual $MAX_RETRY_ATTEMPTS - } - - It "Lists the configurations on the cluster" { - $output = az k8s-configuration list -c $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type connectedClusters - $? | Should -BeTrue - - $configExists = $output | ConvertFrom-Json | Where-Object { $_.id -Match $configurationName } - $configExists | Should -Not -BeNullOrEmpty - } - - It "Deletes the configuration from the cluster" { - az k8s-configuration delete -c $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type connectedClusters -n $configurationName - $? | Should -BeTrue - - # Configuration should be removed from the resource model - az k8s-configuration show -c $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type connectedClusters -n $configurationName - $? | Should -BeFalse - } - - It "Performs another list after the delete" { - $output = az k8s-configuration list -c $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type connectedClusters - $configExists = $output | ConvertFrom-Json | Where-Object { $_.id -Match $configurationName } - $configExists | Should -BeNullOrEmpty - } -} \ No newline at end of file diff --git a/testing/test/configurations/Constants.ps1 b/testing/test/configurations/Constants.ps1 deleted file mode 100644 index f1e8c6ffdc3..00000000000 --- a/testing/test/configurations/Constants.ps1 +++ /dev/null @@ -1,8 +0,0 @@ -$ENVCONFIG = Get-Content -Path $PSScriptRoot\..\..\settings.json | ConvertFrom-Json -$SUCCESS_MESSAGE = "Successfully installed the operator" -$FAILED_MESSAGE = "Failed the install of the operator" -$TMP_DIRECTORY = "$PSScriptRoot\..\..\tmp" - -$POD_RUNNING = "Running" - -$MAX_RETRY_ATTEMPTS = 18 \ No newline at end of file diff --git a/testing/test/configurations/Helper.ps1 b/testing/test/configurations/Helper.ps1 deleted file mode 100644 index 842e2da84aa..00000000000 --- a/testing/test/configurations/Helper.ps1 +++ /dev/null @@ -1,45 +0,0 @@ -function Get-ConfigData { - param( - [string]$configName - ) - - $output = kubectl get gitconfigs -A -o json | ConvertFrom-Json - return $output.items | Where-Object { $_.metadata.name -eq $configurationName } -} - -function Get-ConfigStatus { - param( - [string]$configName - ) - - $configData = Get-ConfigData $configName - if ($configData -ne $null) { - return $configData.status.status - } - return $null -} - -function Get-PodStatus { - param( - [string]$podName, - [string]$Namespace - ) - - $allPodData = kubectl get pods -n $Namespace -o json | ConvertFrom-Json - $podData = $allPodData.items | Where-Object { $_.metadata.name -Match $podName } - return $podData.status.phase -} - -function Secret-Exists { - param( - [string]$secretName, - [string]$Namespace - ) - - $allSecretData = kubectl get secrets -n $Namespace -o json | ConvertFrom-Json - $secretData = $allSecretData.items | Where-Object { $_.metadata.name -Match $secretName } - if ($secretData.Length -ge 1) { - return $true - } - return $false -} \ No newline at end of file diff --git a/testing/test/extensions/data/azure_ml/test_cert.pem b/testing/test/extensions/data/azure_ml/test_cert.pem deleted file mode 100644 index a8cfe1294b4..00000000000 --- a/testing/test/extensions/data/azure_ml/test_cert.pem +++ /dev/null @@ -1,32 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIFlzCCA3+gAwIBAgIULOv9pTNMMZNMXFF3ctTJZEcPfQgwDQYJKoZIhvcNAQEL -BQAwWzELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM -GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDEUMBIGA1UEAwwLdGVzdGluZi5jb20w -HhcNMjIwOTA4MDMxNDA0WhcNMjMwOTA4MDMxNDA0WjBbMQswCQYDVQQGEwJBVTET -MBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50ZXJuZXQgV2lkZ2l0cyBQ -dHkgTHRkMRQwEgYDVQQDDAt0ZXN0aW5mLmNvbTCCAiIwDQYJKoZIhvcNAQEBBQAD -ggIPADCCAgoCggIBAM1dfkb2+o/VzazOSSXlv03rRaV4/+wKALuOGrZGJbGzm4DJ -WyN6+zogIKIbDMPUm4eB1Jh9vSQ0RciwhCUogPel7HYQhlFtyfq4I2PReXm7MInm -ZGpkFZdHliYFMQv5O1FDN4SjHbpKpEomUIngj+j5IRuQ+5wksJfEE4GKBdTv0zSJ -kvIW3o4qf3kYUuoT53g93HqUQKg6LKZ0ra7som1F1dVuOgYXmyirrdFm2SJ6y4Z7 -Fuu7/bNHJAeUIcaw/3FZrKAdYst5MO6BquDPQ3LUmIW9MPO7ynfR0MKMXFvHmQmt -ve9IMRF52FkKqO6mut3kMEhQqE9iiEcLZDUghRwzxIgYU1cHw1jBCTxois4f6HdY -zK9fK9TZoI1ftWoFcofhV4uiB8NwsRQqFeAsMrd2qCbMjLoHysMbKl3ORwtG+sl0 -ti3DhiLQbedfBHzy0xtaZvkauP6+qoGYYjGiDQjP+acvCrjjSwVpWWhu44EWFWnc -Iszjp2AaG4LagaV+ZjTHDa4mkyz/dI3EDZ6wDmHCYdbI57yJhXqjSooVYNE4FYwo -KDXVT+42mtBGIA+e/pRFYE1bUsuIubhuKVLNd081W+rsS3Juv6KWMNnahUm8Mv1P -unwQKrtJtc31rEXGjsVp4E95ncbu/0aAoczCnYlXp1pLNevnNfRdeoOWnNLzAgMB -AAGjUzBRMB0GA1UdDgQWBBQNlOeOEqZh7hojtGoKETkk+Cdl/zAfBgNVHSMEGDAW -gBQNlOeOEqZh7hojtGoKETkk+Cdl/zAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3 -DQEBCwUAA4ICAQB3Dmv2Ftxp0vTDBZMUXjOKqpyuznpT5Y7LzoHxWKBXnwCGVuOo -1P1sse/QD87No+jAi3lPtQo1+GVbnN8kkIPAu8Cu3ogrlMr9938ogWz1/x33D2Nh -DLk2ZDg7UxsGxIKvMCV9MopZGQ2HCN7M43iXJ5dpZ82F6kMHZpGdigAnbTzh8iGu -vro1SiBCwPwlv+VW7VvU0OpGgBQlVj8CMON01/lWYZd4vXrv5iox59l/b1HcNrYN -Pe2CvZAmqx4Wnar8HLV6TAFPqvFf/6kAeWXt89mKaGx/LTy632sXeBHVnL62o6OD -WUjLECjTF1LSI/tzgoQUKIJ70FysIayUoSDcyb+jpb0dKBoi1vUQOS3R5gH35Z0c -fiGXvsDCtK8UvU5V4W8msBtEF1TB1vk5rrhAIGecHyY87iQnF/Lue6CvLQ6jhga6 -A7RRaKf05Stz+pyG3AF3P6iAyFK72k9LIpb+iGvlDZ2yWAMcqEuTRGeo3G3xQabN -VGG0thneWrQ7KicRXNavMgytnHs73mHGTMgffg4njcNPuio8ewcwxhDycYJpr4gz -NqtdgFPBzx9nQjrK6ZBDSzbkkLFNLnr+OSB9pvlpVnC957qqZ1L0GrJIfN/kbCF3 -TG7u3bkbTjlrvuxY1FAqrXesZrlBpx5cxLrO8XOuSlFB0q2pJSwfIv6Jbw== ------END CERTIFICATE----- \ No newline at end of file diff --git a/testing/test/extensions/data/azure_ml/test_key.pem b/testing/test/extensions/data/azure_ml/test_key.pem deleted file mode 100644 index e238b5c2225..00000000000 --- a/testing/test/extensions/data/azure_ml/test_key.pem +++ /dev/null @@ -1,52 +0,0 @@ ------BEGIN PRIVATE KEY----- -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 -hsfzUDNwvb4vnu9Yq3m//0pk/pMs6g== ------END PRIVATE KEY----- \ No newline at end of file diff --git a/testing/test/extensions/public/AzureDefender.Tests.ps1 b/testing/test/extensions/public/AzureDefender.Tests.ps1 deleted file mode 100644 index 419d226cb2e..00000000000 --- a/testing/test/extensions/public/AzureDefender.Tests.ps1 +++ /dev/null @@ -1,71 +0,0 @@ -Describe 'Azure Defender Testing' { - BeforeAll { - $extensionType = "microsoft.azuredefender.kubernetes" - $extensionName = "microsoft.azuredefender.kubernetes" - $extensionAgentNamespace = "azuredefender" - - . $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 connectedClusters --extension-type $extensionType -n $extensionName --no-wait - $? | Should -BeTrue - - $output = az $Env:K8sExtensionName show -c $($ENVCONFIG.arcClusterName) -g $($ENVCONFIG.resourceGroup) --cluster-type connectedClusters -n $extensionName - $? | Should -BeTrue - - $isAutoUpgradeMinorVersion = ($output | ConvertFrom-Json).autoUpgradeMinorVersion - $isAutoUpgradeMinorVersion.ToString() -eq "True" | Should -BeTrue - - # Loop and retry until the extension installs - $n = 0 - do - { - # Only check the extension config, not the pod since this doesn't bring up pods - $output = az $Env:K8sExtensionName show -c $($ENVCONFIG.arcClusterName) -g $($ENVCONFIG.resourceGroup) --cluster-type connectedClusters -n $extensionName - $provisioningState = ($output | ConvertFrom-Json).provisioningState - Write-Host "Got ProvisioningState: $provisioningState for the extension" - if (Has-ExtensionData $extensionName) { - break - } - Start-Sleep -Seconds 10 - $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 connectedClusters -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 connectedClusters - $? | 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 connectedClusters -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 connectedClusters -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 connectedClusters - $? | Should -BeTrue - $output | Should -Not -BeNullOrEmpty - - $extensionExists = $output | ConvertFrom-Json | Where-Object { $_.extensionType -eq $extensionName } - $extensionExists | Should -BeNullOrEmpty - } -} diff --git a/testing/test/extensions/public/AzureMLKubernetes.Tests.ps1 b/testing/test/extensions/public/AzureMLKubernetes.Tests.ps1 deleted file mode 100644 index d38627219c1..00000000000 --- a/testing/test/extensions/public/AzureMLKubernetes.Tests.ps1 +++ /dev/null @@ -1,221 +0,0 @@ -Describe 'AzureML Kubernetes Testing' { - BeforeAll { - $extensionType = "Microsoft.AzureML.Kubernetes" - $extensionName = "azureml-kubernetes-connector" - $extensionAgentNamespace = "azureml" - $relayResourceIDKey = "relayserver.hybridConnectionResourceID" - $serviceBusResourceIDKey = "servicebus.resourceID" - $mockUpdateKey = "mockTest" - $mockProtectedUpdateKey = "mockProtectedTest" - - . $PSScriptRoot/../../helper/Constants.ps1 - . $PSScriptRoot/../../helper/Helper.ps1 - } - - It 'Creates the extension and checks that it onboards correctly with inference and SSL enabled' { - $sslKeyPemFile = Join-Path (Join-Path (Join-Path (Split-Path $PSScriptRoot -Parent) "data") "azure_ml") "test_key.pem" - $sslCertPemFile = Join-Path (Join-Path (Join-Path (Split-Path $PSScriptRoot -Parent) "data") "azure_ml") "test_cert.pem" - az $Env:K8sExtensionName create -c $($ENVCONFIG.arcClusterName) -g $($ENVCONFIG.resourceGroup) --cluster-type connectedClusters --extension-type $extensionType -n $extensionName --release-train stable --config enableInference=true identity.proxy.remoteEnabled=True identity.proxy.remoteHost=https://master.experiments.azureml-test.net inferenceRouterServiceType=nodePort sslCname=testinf.com --config-protected sslKeyPemFile=$sslKeyPemFile sslCertPemFile=$sslCertPemFile --no-wait - $? | Should -BeTrue - - $output = az $Env:K8sExtensionName show -c $($ENVCONFIG.arcClusterName) -g $($ENVCONFIG.resourceGroup) --cluster-type connectedClusters -n $extensionName - $? | Should -BeTrue - - $isAutoUpgradeMinorVersion = ($output | ConvertFrom-Json).autoUpgradeMinorVersion - $isAutoUpgradeMinorVersion.ToString() -eq "True" | Should -BeTrue - - # Loop and retry until the extension installs - $n = 0 - do - { - if (Has-ExtensionData $extensionName) { - break - } - Start-Sleep -Seconds 20 - $n += 1 - } while ($n -le $MAX_RETRY_ATTEMPTS) - $n | Should -BeLessOrEqual $MAX_RETRY_ATTEMPTS - - # check if relay is populated - $relayResourceID = Get-ExtensionConfigurationSettings $extensionName $relayResourceIDKey - $relayResourceID | Should -Not -BeNullOrEmpty - } - - It "Performs a show on the extension" { - $output = az $Env:K8sExtensionName show -c $($ENVCONFIG.arcClusterName) -g $($ENVCONFIG.resourceGroup) --cluster-type connectedClusters -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 connectedClusters - $? | Should -BeTrue - - $output | Should -Not -BeNullOrEmpty - $extensionExists = $output | ConvertFrom-Json | Where-Object { $_.extensionType -eq $extensionType } - $extensionExists | Should -Not -BeNullOrEmpty - } - - It "Wait for the extension to be ready" { - # Loop and retry until the extension installed - $n = 0 - do - { - - $output = az $Env:K8sExtensionName show -c $($ENVCONFIG.arcClusterName) -g $($ENVCONFIG.resourceGroup) --cluster-type connectedClusters -n $extensionName - $? | Should -BeTrue - - $provisioningState = ($output | ConvertFrom-Json).provisioningState - Write-Host "Provisioning state: $provisioningState" - if ($provisioningState -eq "Succeeded") { - break - } - Start-Sleep -Seconds 20 - $n += 1 - } while ($n -le $MAX_RETRY_ATTEMPTS) - $n | Should -BeLessOrEqual $MAX_RETRY_ATTEMPTS - } - - It "Perform Update extension" { - $sslKeyPemFile = Join-Path (Join-Path (Join-Path (Split-Path $PSScriptRoot -Parent) "data") "azure_ml") "test_key.pem" - $sslCertPemFile = Join-Path (Join-Path (Join-Path (Split-Path $PSScriptRoot -Parent) "data") "azure_ml") "test_cert.pem" - az $Env:K8sExtensionName update -c $($ENVCONFIG.arcClusterName) -g $($ENVCONFIG.resourceGroup) --cluster-type connectedClusters -n $extensionName --config "$($mockUpdateKey)=true" --config-protected "$($mockProtectedUpdateKey)=true" sslKeyPemFile=$sslKeyPemFile sslCertPemFile=$sslCertPemFile --no-wait - $? | Should -BeTrue - - # Loop and retry until the extension updated - $n = 0 - do - { - - $output = az $Env:K8sExtensionName show -c $($ENVCONFIG.arcClusterName) -g $($ENVCONFIG.resourceGroup) --cluster-type connectedClusters -n $extensionName - $? | Should -BeTrue - - $provisioningState = ($output | ConvertFrom-Json).provisioningState - Write-Host "Provisioning state: $provisioningState" - if ($provisioningState -eq "Succeeded") { - break - } - Start-Sleep -Seconds 20 - $n += 1 - } while ($n -le $MAX_RETRY_ATTEMPTS) - $n | Should -BeLessOrEqual $MAX_RETRY_ATTEMPTS - - $mockedUpdateData = Get-ExtensionConfigurationSettings $extensionName $mockUpdateKey - $mockedUpdateData | Should -Not -BeNullOrEmpty - } - - It "Deletes the extension from the cluster with inference enabled" { - # cleanup the relay and servicebus - $relayResourceID = Get-ExtensionConfigurationSettings $extensionName $relayResourceIDKey - $relayNamespaceName = $relayResourceID.split("/")[8] - az relay namespace delete --resource-group $ENVCONFIG.resourceGroup --name $relayNamespaceName - - $output = az $Env:K8sExtensionName delete -c $($ENVCONFIG.arcClusterName) -g $($ENVCONFIG.resourceGroup) --cluster-type connectedClusters -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 connectedClusters -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 connectedClusters - $? | Should -BeTrue - $output | Should -Not -BeNullOrEmpty - - $extensionExists = $output | ConvertFrom-Json | Where-Object { $_.extensionType -eq $extensionName } - $extensionExists | Should -BeNullOrEmpty - } - - # It 'Creates the extension and checks that it onboards correctly with training enabled' { - # az $Env:K8sExtensionName create -c $($ENVCONFIG.arcClusterName) -g $($ENVCONFIG.resourceGroup) --cluster-type connectedClusters --extension-type $extensionType -n $extensionName --release-train staging --config enableTraining=true - # $? | Should -BeTrue - - # $output = az $Env:K8sExtensionName show -c $($ENVCONFIG.arcClusterName) -g $($ENVCONFIG.resourceGroup) --cluster-type connectedClusters -n $extensionName - # $? | Should -BeTrue - - # $isAutoUpgradeMinorVersion = ($output | ConvertFrom-Json).autoUpgradeMinorVersion - # $isAutoUpgradeMinorVersion.ToString() -eq "True" | Should -BeTrue - - # # Loop and retry until the extension installs - # $n = 0 - # do - # { - # if (Has-ExtensionData $extensionName) { - # break - # } - # Start-Sleep -Seconds 20 - # $n += 1 - # } while ($n -le $MAX_RETRY_ATTEMPTS) - # $n | Should -BeLessOrEqual $MAX_RETRY_ATTEMPTS - - # # check if relay is populated - # $relayResourceID = Get-ExtensionConfigurationSettings $extensionName $relayResourceIDKey - # $relayResourceID | Should -Not -BeNullOrEmpty - # } - - # It "Deletes the extension from the cluster" { - # # cleanup the relay and servicebus - # $relayResourceID = Get-ExtensionConfigurationSettings $extensionName $relayResourceIDKey - # $serviceBusResourceID = Get-ExtensionConfigurationSettings $extensionName $serviceBusResourceIDKey - # $relayNamespaceName = $relayResourceID.split("/")[8] - # $serviceBusNamespaceName = $serviceBusResourceID.split("/")[8] - # az relay namespace delete --resource-group $ENVCONFIG.resourceGroup --name $relayNamespaceName - # az servicebus namespace delete --resource-group $ENVCONFIG.resourceGroup --name $serviceBusNamespaceName - - # $output = az $Env:K8sExtensionName delete -c $($ENVCONFIG.arcClusterName) -g $($ENVCONFIG.resourceGroup) --cluster-type connectedClusters -n $extensionName - # $? | Should -BeTrue - - # # Extension should not be found on the cluster - # $output = az $Env:K8sExtensionName show -c $($ENVCONFIG.arcClusterName) -g $($ENVCONFIG.resourceGroup) --cluster-type connectedClusters -n $extensionName - # $? | Should -BeFalse - # $output | Should -BeNullOrEmpty - # } - - # It 'Creates the extension and checks that it onboards correctly with inference enabled' { - # az $Env:K8sExtensionName create -c $($ENVCONFIG.arcClusterName) -g $($ENVCONFIG.resourceGroup) --cluster-type connectedClusters --extension-type $extensionType -n $extensionName --release-train staging --config enableInference=true identity.proxy.remoteEnabled=True identity.proxy.remoteHost=https://master.experiments.azureml-test.net allowInsecureConnections=True inferenceLoadBalancerHA=false - # $? | Should -BeTrue - - # $output = az $Env:K8sExtensionName show -c $($ENVCONFIG.arcClusterName) -g $($ENVCONFIG.resourceGroup) --cluster-type connectedClusters -n $extensionName - # $? | Should -BeTrue - - # $isAutoUpgradeMinorVersion = ($output | ConvertFrom-Json).autoUpgradeMinorVersion - # $isAutoUpgradeMinorVersion.ToString() -eq "True" | Should -BeTrue - - # # Loop and retry until the extension installs - # $n = 0 - # do - # { - # if (Has-ExtensionData $extensionName) { - # break - # } - # Start-Sleep -Seconds 20 - # $n += 1 - # } while ($n -le $MAX_RETRY_ATTEMPTS) - # $n | Should -BeLessOrEqual $MAX_RETRY_ATTEMPTS - - # # check if relay is populated - # $relayResourceID = Get-ExtensionConfigurationSettings $extensionName $relayResourceIDKey - # $relayResourceID | Should -Not -BeNullOrEmpty - # } - - # It "Deletes the extension from the cluster with inference enabled" { - # # cleanup the relay and servicebus - # $relayResourceID = Get-ExtensionConfigurationSettings $extensionName $relayResourceIDKey - # $serviceBusResourceID = Get-ExtensionConfigurationSettings $extensionName $serviceBusResourceIDKey - # $relayNamespaceName = $relayResourceID.split("/")[8] - # $serviceBusNamespaceName = $serviceBusResourceID.split("/")[8] - # az relay namespace delete --resource-group $ENVCONFIG.resourceGroup --name $relayNamespaceName - # az servicebus namespace delete --resource-group $ENVCONFIG.resourceGroup --name $serviceBusNamespaceName - - # $output = az $Env:K8sExtensionName delete -c $($ENVCONFIG.arcClusterName) -g $($ENVCONFIG.resourceGroup) --cluster-type connectedClusters -n $extensionName - # $? | Should -BeTrue - - # # Extension should not be found on the cluster - # $output = az $Env:K8sExtensionName show -c $($ENVCONFIG.arcClusterName) -g $($ENVCONFIG.resourceGroup) --cluster-type connectedClusters -n $extensionName - # $? | Should -BeFalse - # $output | Should -BeNullOrEmpty - # } -} diff --git a/testing/test/extensions/public/AzureMonitor.Tests.ps1 b/testing/test/extensions/public/AzureMonitor.Tests.ps1 deleted file mode 100644 index 9748755eea0..00000000000 --- a/testing/test/extensions/public/AzureMonitor.Tests.ps1 +++ /dev/null @@ -1,68 +0,0 @@ -Describe 'Azure Monitor Testing' { - BeforeAll { - $extensionType = "microsoft.azuremonitor.containers" - $extensionName = "azuremonitor-containers" - $extensionAgentName = "omsagent" - $extensionAgentNamespace = "kube-system" - - . $PSScriptRoot/../../helper/Constants.ps1 - . $PSScriptRoot/../../helper/Helper.ps1 - } - - It 'Creates the extension and checks that it onboards correctly' { - az $Env:K8sExtensionName create -c $($ENVCONFIG.arcClusterName) -g $($ENVCONFIG.resourceGroup) --cluster-type connectedClusters --extension-type $extensionType -n $extensionName --no-wait - $? | Should -BeTrue - - $output = az $Env:K8sExtensionName show -c $($ENVCONFIG.arcClusterName) -g $($ENVCONFIG.resourceGroup) --cluster-type connectedClusters -n $extensionName - $? | Should -BeTrue - - $isAutoUpgradeMinorVersion = ($output | ConvertFrom-Json).autoUpgradeMinorVersion - $isAutoUpgradeMinorVersion.ToString() -eq "True" | Should -BeTrue - - # Loop and retry until the extension installs - $n = 0 - do - { - if (Has-ExtensionData $extensionName) { - break - } - Start-Sleep -Seconds 10 - $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 connectedClusters -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 connectedClusters - $? | 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 connectedClusters -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 connectedClusters -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 connectedClusters - $? | Should -BeTrue - $output | Should -Not -BeNullOrEmpty - - $extensionExists = $output | ConvertFrom-Json | Where-Object { $_.extensionType -eq $extensionName } - $extensionExists | Should -BeNullOrEmpty - } -} diff --git a/testing/test/extensions/public/AzurePolicy.Tests.ps1 b/testing/test/extensions/public/AzurePolicy.Tests.ps1 deleted file mode 100644 index 8f68426f4ed..00000000000 --- a/testing/test/extensions/public/AzurePolicy.Tests.ps1 +++ /dev/null @@ -1,74 +0,0 @@ -Describe 'Azure Policy Testing' { - BeforeAll { - $extensionType = "microsoft.policyinsights" - $extensionName = "policy" - $extensionAgentName = "azure-policy" - $extensionAgentNamespace = "kube-system" - - . $PSScriptRoot/../../helper/Constants.ps1 - . $PSScriptRoot/../../helper/Helper.ps1 - } - - It 'Creates the extension and checks that it onboards correctly' { - az $Env:K8sExtensionName create -c $($ENVCONFIG.arcClusterName) -g $($ENVCONFIG.resourceGroup) --cluster-type connectedClusters --extension-type $extensionType -n $extensionName --no-wait - $? | Should -BeTrue - - $output = az $Env:K8sExtensionName show -c $($ENVCONFIG.arcClusterName) -g $($ENVCONFIG.resourceGroup) --cluster-type connectedClusters -n $extensionName - $? | Should -BeTrue - - $isAutoUpgradeMinorVersion = ($output | ConvertFrom-Json).autoUpgradeMinorVersion - $isAutoUpgradeMinorVersion.ToString() -eq "True" | Should -BeTrue - - # Check that we get the principal id back for the created identity - $principalId = ($output | ConvertFrom-Json).identity.principalId - $principalId | Should -Not -BeNullOrEmpty - - # Loop and retry until the extension installs - $n = 0 - do - { - # Only check the extension config, not the pod since this doesn't bring up pods - $output = Invoke-Expression "az $Env:K8sExtensionName show -c $($ENVCONFIG.arcClusterName) -g $($ENVCONFIG.resourceGroup) --cluster-type connectedClusters -n $extensionName" -ErrorVariable badOut - if (Has-ExtensionData $extensionName){ - break - } - Start-Sleep -Seconds 10 - $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 connectedClusters -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 connectedClusters - $? | 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 connectedClusters -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 connectedClusters -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 connectedClusters - $? | Should -BeTrue - $output | Should -Not -BeNullOrEmpty - - $extensionExists = $output | ConvertFrom-Json | Where-Object { $_.extensionType -eq $extensionName } - $extensionExists | Should -BeNullOrEmpty - } -} diff --git a/testing/test/extensions/public/Cassandra.Tests.ps1 b/testing/test/extensions/public/Cassandra.Tests.ps1 deleted file mode 100644 index e579c43ee30..00000000000 --- a/testing/test/extensions/public/Cassandra.Tests.ps1 +++ /dev/null @@ -1,98 +0,0 @@ -Describe 'Cassandra Testing' { - BeforeAll { - $extensionType = "cassandradatacentersoperator" - $extensionName = "cassandra" - - . $PSScriptRoot/../../helper/Constants.ps1 - . $PSScriptRoot/../../helper/Helper.ps1 - } - - It 'Creates the extension and checks that it onboards correctly' { - az $Env:K8sExtensionName create -c $($ENVCONFIG.arcClusterName) -g $($ENVCONFIG.resourceGroup) --cluster-type connectedClusters --extension-type $extensionType -n $extensionName --no-wait - $? | Should -BeTrue - - $output = az $Env:K8sExtensionName show -c $($ENVCONFIG.arcClusterName) -g $($ENVCONFIG.resourceGroup) --cluster-type connectedClusters -n $extensionName - $? | Should -BeTrue - - $isAutoUpgradeMinorVersion = ($output | ConvertFrom-Json).autoUpgradeMinorVersion - $isAutoUpgradeMinorVersion.ToString() -eq "True" | Should -BeTrue - - # Check that we get the principal id back for the created identity - $principalId = ($output | ConvertFrom-Json).identity.principalId - $principalId | Should -Not -BeNullOrEmpty - - # Loop and retry until the extension installs - $n = 0 - do - { - # Only check the extension config, not the pod since this doesn't bring up pods - $output = az $Env:K8sExtensionName show -c $($ENVCONFIG.arcClusterName) -g $($ENVCONFIG.resourceGroup) --cluster-type connectedClusters -n $extensionName - $provisioningState = ($output | ConvertFrom-Json).provisioningState - Write-Host "Got ProvisioningState: $provisioningState for the extension" - if ((Has-ExtensionData $extensionName) -And ($provisioningState -eq "Succeeded")) { - break - } - Start-Sleep -Seconds 10 - $n += 1 - } while ($n -le 20) - $n | Should -BeLessOrEqual 20 - } - - It "Performs a show on the extension" { - $output = az $Env:K8sExtensionName show -c $($ENVCONFIG.arcClusterName) -g $($ENVCONFIG.resourceGroup) --cluster-type connectedClusters -n $extensionName - $? | Should -BeTrue - $output | Should -Not -BeNullOrEmpty - } - - It "Runs an update on the extension on the cluster" { - $output = az $Env:K8sExtensionName update -c $($ENVCONFIG.arcClusterName) -g $($ENVCONFIG.resourceGroup) --cluster-type connectedClusters -n $extensionName --auto-upgrade false --no-wait - $? | Should -BeTrue - - $output = az $Env:K8sExtensionName show -c $($ENVCONFIG.arcClusterName) -g $($ENVCONFIG.resourceGroup) --cluster-type connectedClusters -n $extensionName - $? | Should -BeTrue - - $isAutoUpgradeMinorVersion = ($output | ConvertFrom-Json).autoUpgradeMinorVersion - $isAutoUpgradeMinorVersion.ToString() -eq "False" | Should -BeTrue - - # Loop and retry until the extension config updates - $n = 0 - do - { - $isAutoUpgradeMinorVersion = (Get-ExtensionData $extensionName).spec.autoUpgradeMinorVersion - if (!$isAutoUpgradeMinorVersion) { #autoUpgradeMinorVersion doesn't exist in ExtensionConfig CRD if false - break - } - Start-Sleep -Seconds 10 - $n += 1 - } while ($n -le $MAX_RETRY_ATTEMPTS) - $n | Should -BeLessOrEqual $MAX_RETRY_ATTEMPTS - } - - It "Lists the extensions on the cluster" { - $output = az $Env:K8sExtensionName list -c $($ENVCONFIG.arcClusterName) -g $($ENVCONFIG.resourceGroup) --cluster-type connectedClusters - $? | 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 connectedClusters -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 connectedClusters -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 connectedClusters - $? | Should -BeTrue - $output | Should -Not -BeNullOrEmpty - - $extensionExists = $output | ConvertFrom-Json | Where-Object { $_.extensionType -eq $extensionName } - $extensionExists | Should -BeNullOrEmpty - } -} diff --git a/testing/test/extensions/public/Dapr.Tests.ps1 b/testing/test/extensions/public/Dapr.Tests.ps1 deleted file mode 100644 index 5af40546c19..00000000000 --- a/testing/test/extensions/public/Dapr.Tests.ps1 +++ /dev/null @@ -1,63 +0,0 @@ -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 diff --git a/testing/test/extensions/public/ExtensionTypes.Tests.ps1 b/testing/test/extensions/public/ExtensionTypes.Tests.ps1 deleted file mode 100644 index d9634024432..00000000000 --- a/testing/test/extensions/public/ExtensionTypes.Tests.ps1 +++ /dev/null @@ -1,33 +0,0 @@ -Describe 'Extension Types Testing' { - BeforeAll { - $extensionType = "cassandradatacentersoperator" - $location = "eastus2euap" - - . $PSScriptRoot/../../helper/Constants.ps1 - . $PSScriptRoot/../../helper/Helper.ps1 - } - - It 'Performs a show extension types call' { - $output = az $Env:K8sExtensionName extension-types show -c $($ENVCONFIG.arcClusterName) -g $($ENVCONFIG.resourceGroup) --cluster-type connectedClusters --extension-type $extensionType - $? | Should -BeTrue - $output | Should -Not -BeNullOrEmpty - } - - It "Performs a cluster-scoped list extension types call" { - $output = az $Env:K8sExtensionName extension-types list -c $($ENVCONFIG.arcClusterName) -g $($ENVCONFIG.resourceGroup) --cluster-type connectedClusters - $? | Should -BeTrue - $output | Should -Not -BeNullOrEmpty - } - - It "Performs a location-scoped list extension types call" { - $output = az $Env:K8sExtensionName extension-types list-by-location --location $location - $? | Should -BeTrue - $output | Should -Not -BeNullOrEmpty - } - - It "Performs a location-scoped list extension type versions call" { - $output = az $Env:K8sExtensionName extension-types list-versions --location $location --extension-type $extensionType - $? | Should -BeTrue - $output | Should -Not -BeNullOrEmpty - } -} diff --git a/testing/test/extensions/public/Flux.Tests.ps1 b/testing/test/extensions/public/Flux.Tests.ps1 deleted file mode 100644 index 7faa6aa9c70..00000000000 --- a/testing/test/extensions/public/Flux.Tests.ps1 +++ /dev/null @@ -1,63 +0,0 @@ -Describe 'Azure Flux Testing' { - BeforeAll { - $extensionType = "microsoft.flux" - $extensionName = "flux" - $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 --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 diff --git a/testing/test/extensions/public/OpenServiceMesh.Tests.ps1 b/testing/test/extensions/public/OpenServiceMesh.Tests.ps1 deleted file mode 100644 index 75756a8ad1f..00000000000 --- a/testing/test/extensions/public/OpenServiceMesh.Tests.ps1 +++ /dev/null @@ -1,72 +0,0 @@ -Describe 'Azure OpenServiceMesh Testing' { - BeforeAll { - $extensionType = "microsoft.openservicemesh" - $extensionName = "openservicemesh" - $extensionVersion = "1.0.0" - $extensionAgentName = "osm-controller" - $extensionAgentNamespace = "arc-osm-system" - $releaseTrain = "pilot" - - . $PSScriptRoot/../../helper/Constants.ps1 - . $PSScriptRoot/../../helper/Helper.ps1 - } - - # Should Not BeNullOrEmpty checks if the command returns JSON output - - It 'Creates the extension and checks that it onboards correctly' { - az $Env:K8sExtensionName create -c $($ENVCONFIG.arcClusterName) -g $($ENVCONFIG.resourceGroup) --cluster-type connectedClusters --extension-type $extensionType -n $extensionName --release-train $releaseTrain --version $extensionVersion --no-wait - $? | Should -BeTrue - - $output = az $Env:K8sExtensionName show -c $($ENVCONFIG.arcClusterName) -g $($ENVCONFIG.resourceGroup) --cluster-type connectedClusters -n $extensionName - $? | Should -BeTrue - - $isAutoUpgradeMinorVersion = ($output | ConvertFrom-Json).autoUpgradeMinorVersion - $isAutoUpgradeMinorVersion.ToString() -eq "False" | Should -BeTrue - - # Loop and retry until the extension installs - $n = 0 - do - { - if (Has-ExtensionData $extensionName) { - break - } - Start-Sleep -Seconds 10 - $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 connectedClusters -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 connectedClusters - $? | 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 connectedClusters -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 connectedClusters -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 connectedClusters - $? | Should -BeTrue - $output | Should -Not -BeNullOrEmpty - - $extensionExists = $output | ConvertFrom-Json | Where-Object { $_.extensionType -eq $extensionName } - $extensionExists | Should -BeNullOrEmpty - } -} diff --git a/testing/test/helper/Constants.ps1 b/testing/test/helper/Constants.ps1 deleted file mode 100644 index 3ecd3621bc5..00000000000 --- a/testing/test/helper/Constants.ps1 +++ /dev/null @@ -1,7 +0,0 @@ -$ENVCONFIG = Get-Content -Path $PSScriptRoot/../../settings.json | ConvertFrom-Json -$SUCCESS_MESSAGE = "Successfully installed the extension" -$FAILED_MESSAGE = "Failed to install the extension" - -$POD_RUNNING = "Running" - -$MAX_RETRY_ATTEMPTS = 10 \ No newline at end of file diff --git a/testing/test/helper/Helper.ps1 b/testing/test/helper/Helper.ps1 deleted file mode 100644 index 4ff949e7ab4..00000000000 --- a/testing/test/helper/Helper.ps1 +++ /dev/null @@ -1,72 +0,0 @@ -function Get-ExtensionData { - param( - [string]$extensionName - ) - - $output = kubectl get extensionconfigs -A -o json | ConvertFrom-Json - return $output.items | Where-Object { $_.metadata.name -eq $extensionName } -} - -function Has-ExtensionData { - param( - [string]$extensionName - ) - $extensionData = Get-ExtensionData $extensionName - if ($extensionData) { - return $true - } - return $false -} - - -function Has-Identity-Provisioned { - $output = kubectl get azureclusteridentityrequests -n azure-arc container-insights-clusteridentityrequest -o json | ConvertFrom-Json - return ($null -ne $output.status.expirationTime) -and ($null -ne $output.status.tokenReference.dataName) -and ($null -ne $output.status.tokenReference.secretName) -} - -function Get-ExtensionStatus { - param( - [string]$extensionName - ) - - $extensionData = Get-ExtensionData $extensionName - if ($extensionData) { - return $extensionData.status.status - } - return $null -} - -function Get-PodStatus { - param( - [string]$podName, - [string]$Namespace - ) - - $allPodData = kubectl get pods -n $Namespace -o json | ConvertFrom-Json - $podData = $allPodData.items | Where-Object { $_.metadata.name -Match $podName } - if ($podData.Length -gt 1) { - return $podData[0].status.phase - } - return $podData.status.phase -} - -function Get-ExtensionConfigurationSettings { - param( - [string]$extensionName, - [string]$configKey - ) - - $extensionData = Get-ExtensionData $extensionName - if ($extensionData) { - return $extensionData.spec.parameter."$configKey" - } - return $null -} - -function Check-Error { - param( - [string]$output - ) - $hasError = $output -CMatch "ERROR" - return $hasError -} \ No newline at end of file