From e92fe03a3e778d45d62ee6649c9066910becb8f6 Mon Sep 17 00:00:00 2001 From: Bavneet Singh Date: Fri, 22 Nov 2024 09:02:11 -0800 Subject: [PATCH 01/42] add null check for empty kube-config in connectedk8s proxy scenario --- src/connectedk8s/HISTORY.rst | 4 + src/connectedk8s/azext_connectedk8s/custom.py | 13 +- src/connectedk8s/setup.py | 2 +- testing/.gitignore | 9 + testing/Bootstrap.ps1 | 30 ++ testing/README.md | 116 ++++++ testing/Test.ps1 | 95 +++++ .../bin/connectedk8s-1.0.0-py3-none-any.whl | Bin 0 -> 62802 bytes testing/bin/connectedk8s-values.yaml | 3 + .../k8s_configuration-1.0.0-py3-none-any.whl | Bin 0 -> 42351 bytes .../bin/k8s_extension-0.3.0-py3-none-any.whl | Bin 0 -> 52893 bytes testing/owners.txt | 2 + testing/pipeline/k8s-custom-pipelines.yml | 334 ++++++++++++++++++ testing/pipeline/templates/run-test.yml | 112 ++++++ testing/settings.template.json | 12 + .../test/configurations/AutoUpdate.Tests.ps1 | 62 ++++ .../configurations/BasicOnboarding.Tests.ps1 | 62 ++++ .../configurations/ConnectProxy.Tests.ps1 | 62 ++++ testing/test/configurations/Gateway.Tests.ps1 | 116 ++++++ testing/test/configurations/Proxy.Tests.ps1 | 65 ++++ .../configurations/Troubleshoot.Tests.ps1 | 40 +++ .../configurations/WorkloadIdentity.Tests.ps1 | 239 +++++++++++++ testing/test/helper/Constants.ps1 | 5 + 23 files changed, 1377 insertions(+), 6 deletions(-) create mode 100644 testing/.gitignore create mode 100644 testing/Bootstrap.ps1 create mode 100644 testing/README.md create mode 100644 testing/Test.ps1 create mode 100644 testing/bin/connectedk8s-1.0.0-py3-none-any.whl create mode 100644 testing/bin/connectedk8s-values.yaml create mode 100644 testing/bin/k8s_configuration-1.0.0-py3-none-any.whl create mode 100644 testing/bin/k8s_extension-0.3.0-py3-none-any.whl create mode 100644 testing/owners.txt create mode 100644 testing/pipeline/k8s-custom-pipelines.yml create mode 100644 testing/pipeline/templates/run-test.yml create mode 100644 testing/settings.template.json create mode 100644 testing/test/configurations/AutoUpdate.Tests.ps1 create mode 100644 testing/test/configurations/BasicOnboarding.Tests.ps1 create mode 100644 testing/test/configurations/ConnectProxy.Tests.ps1 create mode 100644 testing/test/configurations/Gateway.Tests.ps1 create mode 100644 testing/test/configurations/Proxy.Tests.ps1 create mode 100644 testing/test/configurations/Troubleshoot.Tests.ps1 create mode 100644 testing/test/configurations/WorkloadIdentity.Tests.ps1 create mode 100644 testing/test/helper/Constants.ps1 diff --git a/src/connectedk8s/HISTORY.rst b/src/connectedk8s/HISTORY.rst index 7401698f1dc..42a48c4dc42 100644 --- a/src/connectedk8s/HISTORY.rst +++ b/src/connectedk8s/HISTORY.rst @@ -3,6 +3,10 @@ Release History =============== +1.10.4 +++++++ +* Fixed the issue where the 'connectedk8s proxy' command would fail if the kubeconfig file was empty. + 1.10.3 ++++++ * Fixed linting and styling issues, and added type annotations. diff --git a/src/connectedk8s/azext_connectedk8s/custom.py b/src/connectedk8s/azext_connectedk8s/custom.py index 466c57cd5a2..20af4086a31 100644 --- a/src/connectedk8s/azext_connectedk8s/custom.py +++ b/src/connectedk8s/azext_connectedk8s/custom.py @@ -3372,11 +3372,14 @@ def merge_kubernetes_configurations( break except (KeyError, TypeError): continue - - handle_merge(existing, addition, "clusters", replace) - handle_merge(existing, addition, "users", replace) - handle_merge(existing, addition, "contexts", replace) - existing["current-context"] = addition["current-context"] + + if existing is None: + existing = addition + else: + handle_merge(existing, addition, "clusters", replace) + handle_merge(existing, addition, "users", replace) + handle_merge(existing, addition, "contexts", replace) + existing["current-context"] = addition["current-context"] # check that ~/.kube/config is only read- and writable by its owner if platform.system() != "Windows": diff --git a/src/connectedk8s/setup.py b/src/connectedk8s/setup.py index a190ea577a4..e28f498f682 100644 --- a/src/connectedk8s/setup.py +++ b/src/connectedk8s/setup.py @@ -13,7 +13,7 @@ # TODO: Confirm this is the right version number you want and it matches your # HISTORY.rst entry. -VERSION = "1.10.3" +VERSION = "1.10.4" # The full list of classifiers is available at # https://pypi.python.org/pypi?%3Aaction=list_classifiers diff --git a/testing/.gitignore b/testing/.gitignore new file mode 100644 index 00000000000..29f33294b8b --- /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.3.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..ad21cfddad2 --- /dev/null +++ b/testing/Bootstrap.ps1 @@ -0,0 +1,30 @@ +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 +} + +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 + } +} + + +Copy-Item $HOME/.kube/config -Destination $PSScriptRoot/tmp/KUBECONFIG \ No newline at end of file diff --git a/testing/README.md b/testing/README.md new file mode 100644 index 00000000000..33f12b5b1a3 --- /dev/null +++ b/testing/README.md @@ -0,0 +1,116 @@ +# 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: 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 4: 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 5: 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 6: 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..1f9fc5481f0 --- /dev/null +++ b/testing/Test.ps1 @@ -0,0 +1,95 @@ +param ( + [string] $Path, + [switch] $SkipInstall, + [switch] $CI, + [switch] $ParallelCI, + [switch] $OnlyPublicTests, + + [Parameter(Mandatory=$True)] + [ValidateSet('connectedk8s')] + [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 'connectedk8s') { + $connectedk8sVersion = $ENVCONFIG.extensionVersion.'connectedk8s' + if (!$SkipInstall) { + Write-Host "Removing the old connectedk8s extension..." + az extension remove -n connectedk8s + Write-Host "Installing connectedk8s version $connectedk8sVersion..." + az extension add --source ./bin/connectedk8s-$connectedk8sVersion-py2.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 + } +} \ No newline at end of file diff --git a/testing/bin/connectedk8s-1.0.0-py3-none-any.whl b/testing/bin/connectedk8s-1.0.0-py3-none-any.whl new file mode 100644 index 0000000000000000000000000000000000000000..08f34250036f455aad7e3e820c65d08d790e1201 GIT binary patch literal 62802 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

YVX(toUj*0P8con1X*tXmm22nWyt~gf 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

4OLf9N*^x0^yG1@+&DtXglO();gI7nFsJg6 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 b*Hx{YBsky%^Yh?A4x|a#NYixu{O$h(4U{Xp literal 0 HcmV?d00001 diff --git a/testing/owners.txt b/testing/owners.txt new file mode 100644 index 00000000000..c1bbe9a9e5c --- /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..6d647bd87b3 --- /dev/null +++ b/testing/pipeline/k8s-custom-pipelines.yml @@ -0,0 +1,334 @@ +resources: +- repo: self + +trigger: + batch: true + branches: + include: + - 'main' + +pr: + branches: + include: + - '*' + +stages: +- stage: BuildTestPublishExtension + displayName: "Build, Test, and Publish Extension" + variables: + TEST_PATH: $(Agent.BuildDirectory)/s/testing + CLI_REPO_PATH: $(Agent.BuildDirectory)/s + EXTENSION_NAME: "connectedk8s" + EXTENSION_FILE_NAME: "connectedk8s" + SUBSCRIPTION_ID: "15c06b1b-01d6-407b-bb21-740b8617dea3" + RESOURCE_GROUP: "K8sPartnerExtensionTest" + BASE_CLUSTER_NAME: "connectedk8s-cluster" + jobs: + - template: ./templates/run-test.yml + parameters: + jobName: BasicOnboardingTest + path: ./test/configurations/BasicOnboarding.Tests.ps1 + - template: ./templates/run-test.yml + parameters: + jobName: AutoUpdateTest + path: ./test/configurations/AutoUpdate.Tests.ps1 + - template: ./templates/run-test.yml + parameters: + jobName: ProxyTest + path: ./test/configurations/Proxy.Tests.ps1 + - template: ./templates/run-test.yml + parameters: + jobName: GatewayTest + path: ./test/configurations/Gateway.Tests.ps1 + - template: ./templates/run-test.yml + parameters: + jobName: WorkloadIdentityTest + path: ./test/configurations/WorkloadIdentity.Tests.ps1 + - template: ./templates/run-test.yml + parameters: + jobName: TroubleshootTest + path: ./test/configurations/Troubleshoot.Tests.ps1 + - template: ./templates/run-test.yml + parameters: + jobName: Connectedk8sProxyTest + path: ./test/configurations/ConnectProxy.Tests.ps1 + - job: BuildPublishExtension + pool: + vmImage: 'ubuntu-20.04' + displayName: "Build and Publish the Extension Artifact" + variables: + CLI_REPO_PATH: $(Agent.BuildDirectory)/s + EXTENSION_NAME: "connectedk8s" + steps: + - 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 + pip install --upgrade pip + pip install azdev + + ls $(CLI_REPO_PATH) + + azdev --version + azdev setup -r $(CLI_REPO_PATH) -e $(EXTENSION_NAME) + azdev extension build $(EXTENSION_NAME) + workingDirectory: $(CLI_REPO_PATH) + displayName: "Setup and Build Extension with azdev" + - 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: 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: + Python39: + python.version: '3.9' + Python310: + python.version: '3.10' + Python311: + python.version: '3.11' + Python312: + python.version: '3.12' + 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 connectedk8s + azdev test connectedk8s + displayName: 'Run integration test and build test' + + - job: AzdevLinterModifiedExtensions + displayName: "azdev linter on Modified Extensions" + pool: + vmImage: 'ubuntu-latest' + steps: + - task: UsePythonVersion@0 + displayName: 'Use Python 3.12' + inputs: + versionSpec: 3.12 + - 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 connectedk8s + # Installing setuptools with a version higher than 70.0.0 will not generate metadata.json + pip install setuptools==70.0.0 + pip list -v + + # 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 connectedk8s + displayName: "CLI Linter on Modified Extension" + env: + ADO_PULL_REQUEST_LATEST_COMMIT: $(System.PullRequest.SourceCommitId) + ADO_PULL_REQUEST_TARGET_BRANCH: $(System.PullRequest.TargetBranch) + + - job: AzdevStyleModifiedExtensions + displayName: "azdev style on Modified Extensions" + continueOnError: true + pool: + vmImage: 'ubuntu-latest' + steps: + - task: UsePythonVersion@0 + displayName: 'Use Python 3.12' + inputs: + versionSpec: 3.12 + - 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 azdev + + azdev --version + + azdev setup -c ../azure-cli -r ./ -e connectedk8s + # Installing setuptools with a version higher than 70.0.0 will not generate metadata.json + pip install setuptools==70.0.0 + pip list -v + az --version + + # overwrite the default AZURE_EXTENSION_DIR set by ADO + AZURE_EXTENSION_DIR=~/.azure/cliextensions az --version + + AZURE_EXTENSION_DIR=~/.azure/cliextensions azdev style connectedk8s + displayName: "azdev style on Modified Extensions" + env: + ADO_PULL_REQUEST_LATEST_COMMIT: $(System.PullRequest.SourceCommitId) + ADO_PULL_REQUEST_TARGET_BRANCH: $(System.PullRequest.TargetBranch) + + - job: RuffCheck + displayName: "Lint connectedk8s with ruff check" + pool: + vmImage: 'ubuntu-latest' + steps: + - task: UsePythonVersion@0 + displayName: 'Use Python 3.12' + inputs: + versionSpec: 3.12 + - bash: | + set -ev + + # prepare and activate virtualenv + cd src/connectedk8s + python -m venv env + source ./env/bin/activate + + pip install --upgrade pip + pip install azure-cli --editable .[linting] + + ruff check + + displayName: "ruff check" + + - job: RuffFormat + displayName: "Check connected8ks formatting with ruff" + pool: + vmImage: 'ubuntu-latest' + steps: + - task: UsePythonVersion@0 + displayName: 'Use Python 3.12' + inputs: + versionSpec: 3.12 + - bash: | + set -ev + + # prepare and activate virtualenv + cd src/connectedk8s + python -m venv env + source ./env/bin/activate + + pip install --upgrade pip + pip install azure-cli --editable .[linting] + + ruff format --check + + displayName: "ruff format check" + + - job: TypeChecking + displayName: "Typecheck connected8ks with mypy" + pool: + vmImage: 'ubuntu-latest' + steps: + - task: UsePythonVersion@0 + displayName: 'Use Python 3.12' + inputs: + versionSpec: 3.12 + - bash: | + set -ev + + # prepare and activate virtualenv + cd src/connectedk8s + python -m venv env + source ./env/bin/activate + + pip install --upgrade pip + pip install azure-cli --editable .[linting] + + mypy + + displayName: "mypy" diff --git a/testing/pipeline/templates/run-test.yml b/testing/pipeline/templates/run-test.yml new file mode 100644 index 00000000000..f1d42ae9714 --- /dev/null +++ b/testing/pipeline/templates/run-test.yml @@ -0,0 +1,112 @@ +parameters: + jobName: '' + path: '' + +jobs: +- job: ${{ parameters.jobName}} + pool: + vmImage: 'ubuntu-20.04' + 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" + + - 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 $(CLI_REPO_PATH) + azdev --version + azdev setup -c ../azure-cli -r $(CLI_REPO_PATH) -e $(EXTENSION_NAME) + azdev extension build $(EXTENSION_NAME) + workingDirectory: $(CLI_REPO_PATH) + displayName: "Setup and Build Extension with azdev" + + - bash: | + K8S_CONFIG_VERSION=$(ls ${EXTENSION_FILE_NAME}* | cut -d "-" -f2) + echo "##vso[task.setvariable variable=K8S_CONFIG_VERSION]$K8S_CONFIG_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_CONFIG_VERSION "$K8S_CONFIG_VERSION" \ + '{subscriptionId: $SUB_ID, resourceGroup: $RG, aksClusterName: $AKS_CLUSTER_NAME, arcClusterName: $ARC_CLUSTER_NAME, extensionVersion: {"connectedk8s": $K8S_CONFIG_VERSION}}') + 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.24.0/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" + + - bash: | + 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 + displayName: "Install Helm" + + - 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 connectedk8s + workingDirectory: $(TEST_PATH) + continueOnError: true + + - task: PublishTestResults@2 + inputs: + testResultsFormat: 'JUnit' + testResultsFiles: '**/testing/results/*.xml' + failTaskOnFailedTests: true + condition: succeededOrFailed() diff --git a/testing/settings.template.json b/testing/settings.template.json new file mode 100644 index 00000000000..657126c20aa --- /dev/null +++ b/testing/settings.template.json @@ -0,0 +1,12 @@ +{ + "subscriptionId": "", + "resourceGroup": "", + "aksClusterName": "", + "arcClusterName": "", + + "extensionVersion": { + "k8s-extension": "0.3.0", + "k8s-extension-private": "0.1.0", + "connectedk8s": "1.0.0" + } +} \ No newline at end of file diff --git a/testing/test/configurations/AutoUpdate.Tests.ps1 b/testing/test/configurations/AutoUpdate.Tests.ps1 new file mode 100644 index 00000000000..d55029ceeb8 --- /dev/null +++ b/testing/test/configurations/AutoUpdate.Tests.ps1 @@ -0,0 +1,62 @@ +Describe 'Auto Upgrade Scenario' { + BeforeAll { + . $PSScriptRoot/../helper/Constants.ps1 + } + + It 'Check if basic onboarding works with auto-upgrade disabled' { + az connectedk8s connect -n $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup -l $ARC_LOCATION --disable-auto-upgrade --no-wait + $? | Should -BeTrue + Start-Sleep -Seconds 10 + + # Loop and retry until the configuration installs + $n = 0 + do + { + $output = az connectedk8s show -n $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup + $jsonOutput = [System.Text.Json.JsonDocument]::Parse($output) + $provisioningState = ($output | ConvertFrom-Json).provisioningState + $autoUpdate = $jsonOutput.RootElement.GetProperty("arcAgentProfile").GetProperty("agentAutoUpgrade").GetString() + Write-Host "Provisioning State: $provisioningState" + Write-Host "Auto Update: $autoUpdate" + if ($provisioningState -eq $SUCCEEDED -and $autoUpdate -eq "Disabled") { + break + } + Start-Sleep -Seconds 10 + $n += 1 + } while ($n -le $MAX_RETRY_ATTEMPTS) + $n | Should -BeLessOrEqual $MAX_RETRY_ATTEMPTS + } + + It 'Enable auto-upgrade using update cmd' { + az connectedk8s update -n $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --auto-upgrade true + $? | Should -BeTrue + Start-Sleep -Seconds 10 + + # Loop and retry until the configuration installs + $n = 0 + do + { + $output = az connectedk8s show -n $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup + $jsonOutput = [System.Text.Json.JsonDocument]::Parse($output) + $provisioningState = ($output | ConvertFrom-Json).provisioningState + $autoUpdate = $jsonOutput.RootElement.GetProperty("arcAgentProfile").GetProperty("agentAutoUpgrade").GetString() + Write-Host "Provisioning State: $provisioningState" + Write-Host "Auto Update: $autoUpdate" + if ($provisioningState -eq $SUCCEEDED -and $autoUpdate -eq "Enabled") { + break + } + Start-Sleep -Seconds 10 + $n += 1 + } while ($n -le $MAX_RETRY_ATTEMPTS) + $n | Should -BeLessOrEqual $MAX_RETRY_ATTEMPTS + } + + It "Delete the connected instance" { + az connectedk8s delete -n $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --force -y + $? | Should -BeTrue + + # Configuration should be removed from the resource model + az connectedk8s show -n $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup + $? | Should -BeFalse + } +} \ No newline at end of file diff --git a/testing/test/configurations/BasicOnboarding.Tests.ps1 b/testing/test/configurations/BasicOnboarding.Tests.ps1 new file mode 100644 index 00000000000..541327682c0 --- /dev/null +++ b/testing/test/configurations/BasicOnboarding.Tests.ps1 @@ -0,0 +1,62 @@ +Describe 'Basic Onboarding Scenario' { + BeforeAll { + . $PSScriptRoot/../helper/Constants.ps1 + } + + It 'Check if basic onboarding works correctly' { + az connectedk8s connect -n $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup -l $ARC_LOCATION --no-wait + $? | Should -BeTrue + Start-Sleep -Seconds 10 + + # Loop and retry until the configuration installs + $n = 0 + do + { + $output = az connectedk8s show -n $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup + $jsonOutput = [System.Text.Json.JsonDocument]::Parse($output) + $provisioningState = ($output | ConvertFrom-Json).provisioningState + $autoUpdate = $jsonOutput.RootElement.GetProperty("arcAgentProfile").GetProperty("agentAutoUpgrade").GetString() + Write-Host "Provisioning State: $provisioningState" + Write-Host "Auto Update: $autoUpdate" + if ($provisioningState -eq $SUCCEEDED -and $autoUpdate -eq "Enabled") { + break + } + Start-Sleep -Seconds 10 + $n += 1 + } while ($n -le $MAX_RETRY_ATTEMPTS) + $n | Should -BeLessOrEqual $MAX_RETRY_ATTEMPTS + } + + It 'Disable auto-upgrade' { + az connectedk8s update -n $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --auto-upgrade false + $? | Should -BeTrue + Start-Sleep -Seconds 10 + + # Loop and retry until the configuration installs + $n = 0 + do + { + $output = az connectedk8s show -n $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup + $jsonOutput = [System.Text.Json.JsonDocument]::Parse($output) + $provisioningState = ($output | ConvertFrom-Json).provisioningState + $autoUpdate = $jsonOutput.RootElement.GetProperty("arcAgentProfile").GetProperty("agentAutoUpgrade").GetString() + Write-Host "Provisioning State: $provisioningState" + Write-Host "Auto Update: $autoUpdate" + if ($provisioningState -eq $SUCCEEDED -and $autoUpdate -eq "Disabled") { + break + } + Start-Sleep -Seconds 10 + $n += 1 + } while ($n -le $MAX_RETRY_ATTEMPTS) + $n | Should -BeLessOrEqual $MAX_RETRY_ATTEMPTS + } + + It "Delete the connected instance" { + az connectedk8s delete -n $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --force -y + $? | Should -BeTrue + + # Configuration should be removed from the resource model + az connectedk8s show -n $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup + $? | Should -BeFalse + } +} \ No newline at end of file diff --git a/testing/test/configurations/ConnectProxy.Tests.ps1 b/testing/test/configurations/ConnectProxy.Tests.ps1 new file mode 100644 index 00000000000..3e6f8e06899 --- /dev/null +++ b/testing/test/configurations/ConnectProxy.Tests.ps1 @@ -0,0 +1,62 @@ +Describe 'Connectedk8s Proxy Scenario' { + BeforeAll { + . $PSScriptRoot/../helper/Constants.ps1 + } + + It 'Check if basic onboarding works correctly' { + az connectedk8s connect -n $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup -l $ARC_LOCATION --no-wait + $? | Should -BeTrue + Start-Sleep -Seconds 10 + + # Loop and retry until the configuration installs + $n = 0 + do + { + $output = az connectedk8s show -n $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup + $provisioningState = ($output | ConvertFrom-Json).provisioningState + Write-Host "Provisioning State: $provisioningState" + if ($provisioningState -eq $SUCCEEDED) { + break + } + Start-Sleep -Seconds 10 + $n += 1 + } while ($n -le $MAX_RETRY_ATTEMPTS) + $n | Should -BeLessOrEqual $MAX_RETRY_ATTEMPTS + } + + It 'Connectedk8s proxy test with non-empty kubeconfig' { + # Start the proxy command as a background job + $proxyJob = Start-Job -ScriptBlock { + param($ClusterName, $ResourceGroup) + + # Capture output and errors + try { + $output = az connectedk8s proxy -n $ClusterName -g $ResourceGroup 2>&1 + return @{ Success = $LASTEXITCODE -eq 0; Output = $output } + } catch { + return @{ Success = $false; Output = $_.Exception.Message } + } + } -ArgumentList $ENVCONFIG.arcClusterName, $ENVCONFIG.resourceGroup + + # Wait for a certain amount of time (e.g., 30 seconds) + Start-Sleep -Seconds 30 + + # Display the output + Write-Host "Proxy Job State: $($proxyJob.State)" + + # Check if the job ran successfully + $proxyJob.State | Should -Be 'Running' + + Stop-Job -Job $proxyJob + Remove-Job -Job $proxyJob + } + + It "Delete the connected instance" { + az connectedk8s delete -n $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --force -y + $? | Should -BeTrue + + # Configuration should be removed from the resource model + az connectedk8s show -n $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup + $? | Should -BeFalse + } +} \ No newline at end of file diff --git a/testing/test/configurations/Gateway.Tests.ps1 b/testing/test/configurations/Gateway.Tests.ps1 new file mode 100644 index 00000000000..37dab0eccc9 --- /dev/null +++ b/testing/test/configurations/Gateway.Tests.ps1 @@ -0,0 +1,116 @@ +Describe 'Onboarding with Gateway Scenario' { + BeforeAll { + . $PSScriptRoot/../helper/Constants.ps1 + + $gatewayResourceId = "/subscriptions/15c06b1b-01d6-407b-bb21-740b8617dea3/resourceGroups/connectedk8sCLITestResources/providers/Microsoft.HybridCompute/gateways/gateway-test-cli" + } + + It 'Check if onboarding works with gateway enabled' { + az connectedk8s connect -n $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup -l $ARC_LOCATION --gateway-resource-id $gatewayResourceId --no-wait + $? | Should -BeTrue + Start-Sleep -Seconds 10 + + # Loop and retry until the configuration installs + $n = 0 + do + { + $output = az connectedk8s show -n $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup + $jsonOutput = [System.Text.Json.JsonDocument]::Parse($output) + $provisioningState = ($output | ConvertFrom-Json).provisioningState + $gatewayStatus = $jsonOutput.RootElement.GetProperty("gateway").GetProperty("enabled").GetBoolean() + $gatewayId = $jsonOutput.RootElement.GetProperty("gateway").GetProperty("resourceId").GetString() + Write-Host "Provisioning State: $provisioningState" + Write-Host "Gateway Status: $gatewayStatus" + Write-Host "Gateway Id: $gatewayId" + if ($provisioningState -eq $SUCCEEDED -and $gatewayStatus -eq $true -and $gatewayId -eq $gatewayResourceId) { + break + } + Start-Sleep -Seconds 10 + $n += 1 + } while ($n -le $MAX_RETRY_ATTEMPTS) + $n | Should -BeLessOrEqual $MAX_RETRY_ATTEMPTS + } + + It 'Disable the gateway' { + az connectedk8s update -n $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --disable-gateway + $? | Should -BeTrue + Start-Sleep -Seconds 10 + + # Loop and retry until the configuration installs + $n = 0 + do + { + $output = az connectedk8s show -n $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup + $jsonOutput = [System.Text.Json.JsonDocument]::Parse($output) + $provisioningState = ($output | ConvertFrom-Json).provisioningState + $gatewayStatus = $jsonOutput.RootElement.GetProperty("gateway").GetProperty("enabled").GetBoolean() + Write-Host "Provisioning State: $provisioningState" + Write-Host "Gateway Status: $gatewayStatus" + if ($provisioningState -eq $SUCCEEDED -and $gatewayStatus -eq $false) { + break + } + Start-Sleep -Seconds 10 + $n += 1 + } while ($n -le $MAX_RETRY_ATTEMPTS) + $n | Should -BeLessOrEqual $MAX_RETRY_ATTEMPTS + } + + It 'Update the cluster to use gateway again using update cmd' { + az connectedk8s update -n $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --gateway-resource-id $gatewayResourceId + $? | Should -BeTrue + Start-Sleep -Seconds 10 + + # Loop and retry until the configuration installs + $n = 0 + do + { + $output = az connectedk8s show -n $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup + $jsonOutput = [System.Text.Json.JsonDocument]::Parse($output) + $provisioningState = ($output | ConvertFrom-Json).provisioningState + $gatewayStatus = $jsonOutput.RootElement.GetProperty("gateway").GetProperty("enabled").GetBoolean() + $gatewayId = $jsonOutput.RootElement.GetProperty("gateway").GetProperty("resourceId").GetString() + Write-Host "Provisioning State: $provisioningState" + Write-Host "Gateway Status: $gatewayStatus" + Write-Host "Gateway Id: $gatewayId" + if ($provisioningState -eq $SUCCEEDED -and $gatewayStatus -eq $true -and $gatewayId -eq $gatewayResourceId) { + break + } + Start-Sleep -Seconds 10 + $n += 1 + } while ($n -le $MAX_RETRY_ATTEMPTS) + $n | Should -BeLessOrEqual $MAX_RETRY_ATTEMPTS + } + + It 'Disable the gateway' { + az connectedk8s update -n $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --disable-gateway + $? | Should -BeTrue + Start-Sleep -Seconds 10 + + # Loop and retry until the configuration installs + $n = 0 + do + { + $output = az connectedk8s show -n $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup + $jsonOutput = [System.Text.Json.JsonDocument]::Parse($output) + $provisioningState = ($output | ConvertFrom-Json).provisioningState + $gatewayStatus = $jsonOutput.RootElement.GetProperty("gateway").GetProperty("enabled").GetBoolean() + Write-Host "Provisioning State: $provisioningState" + Write-Host "Gateway Status: $gatewayStatus" + if ($provisioningState -eq $SUCCEEDED -and $gatewayStatus -eq $false) { + break + } + Start-Sleep -Seconds 10 + $n += 1 + } while ($n -le $MAX_RETRY_ATTEMPTS) + $n | Should -BeLessOrEqual $MAX_RETRY_ATTEMPTS + } + + It "Delete the connected instance" { + az connectedk8s delete -n $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --force -y + $? | Should -BeTrue + + # Configuration should be removed from the resource model + az connectedk8s show -n $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup + $? | Should -BeFalse + } +} \ No newline at end of file diff --git a/testing/test/configurations/Proxy.Tests.ps1 b/testing/test/configurations/Proxy.Tests.ps1 new file mode 100644 index 00000000000..bda7b06e4bc --- /dev/null +++ b/testing/test/configurations/Proxy.Tests.ps1 @@ -0,0 +1,65 @@ +Describe 'Proxy Scenario' { + BeforeAll { + . $PSScriptRoot/../helper/Constants.ps1 + } + + It 'Check if basic onboarding works correctly with proxy enabled' { + az connectedk8s connect -n $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup -l $ARC_LOCATION --proxy-skip-range logcollector --no-wait + $? | Should -BeTrue + Start-Sleep -Seconds 10 + + # Loop and retry until the configuration installs + $n = 0 + do + { + $output = az connectedk8s show -n $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup + $provisioningState = ($output | ConvertFrom-Json).provisioningState + Write-Host "Provisioning State: $provisioningState" + if ($provisioningState -eq $SUCCEEDED) { + $isProxyEnabled = helm get values -n azure-arc-release azure-arc -o yaml | grep isProxyEnabled + Write-Host "$isProxyEnabled" + if ($isProxyEnabled -match "isProxyEnabled: true") { + break + } + } + Start-Sleep -Seconds 10 + $n += 1 + } while ($n -le $MAX_RETRY_ATTEMPTS) + $n | Should -BeLessOrEqual $MAX_RETRY_ATTEMPTS + } + + It 'Disable proxy' { + az connectedk8s update -n $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --disable-proxy + $? | Should -BeTrue + Start-Sleep -Seconds 10 + + # Loop and retry until the configuration installs + $n = 0 + do + { + $output = az connectedk8s show -n $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup + $provisioningState = ($output | ConvertFrom-Json).provisioningState + Write-Host "Provisioning State: $provisioningState" + if ($provisioningState -eq $SUCCEEDED) { + $isProxyEnabled = helm get values -n azure-arc-release azure-arc -o yaml | grep isProxyEnabled + Write-Host "$isProxyEnabled" + if ($isProxyEnabled -match "isProxyEnabled: false") { + break + } + break + } + Start-Sleep -Seconds 10 + $n += 1 + } while ($n -le $MAX_RETRY_ATTEMPTS) + $n | Should -BeLessOrEqual $MAX_RETRY_ATTEMPTS + } + + It "Delete the connected instance" { + az connectedk8s delete -n $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --force -y + $? | Should -BeTrue + + # Configuration should be removed from the resource model + az connectedk8s show -n $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup + $? | Should -BeFalse + } +} \ No newline at end of file diff --git a/testing/test/configurations/Troubleshoot.Tests.ps1 b/testing/test/configurations/Troubleshoot.Tests.ps1 new file mode 100644 index 00000000000..c9cb4e26010 --- /dev/null +++ b/testing/test/configurations/Troubleshoot.Tests.ps1 @@ -0,0 +1,40 @@ +Describe 'Troubleshoot Scenario' { + BeforeAll { + . $PSScriptRoot/../helper/Constants.ps1 + } + + It 'Verify cluster onboarding process' { + az connectedk8s connect -n $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup -l $ARC_LOCATION --no-wait + $? | Should -BeTrue + Start-Sleep -Seconds 10 + + # Loop and retry until the configuration installs + $n = 0 + do + { + $output = az connectedk8s show -n $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup + $provisioningState = ($output | ConvertFrom-Json).provisioningState + Write-Host "Provisioning State: $provisioningState" + if ($provisioningState -eq $SUCCEEDED) { + break + } + Start-Sleep -Seconds 10 + $n += 1 + } while ($n -le $MAX_RETRY_ATTEMPTS) + $n | Should -BeLessOrEqual $MAX_RETRY_ATTEMPTS + } + + It 'Verify troubleshoot command functionality' { + az connectedk8s troubleshoot -n $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup + $? | Should -BeTrue + } + + It "Delete the connected instance" { + az connectedk8s delete -n $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --force -y + $? | Should -BeTrue + + # Configuration should be removed from the resource model + az connectedk8s show -n $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup + $? | Should -BeFalse + } +} \ No newline at end of file diff --git a/testing/test/configurations/WorkloadIdentity.Tests.ps1 b/testing/test/configurations/WorkloadIdentity.Tests.ps1 new file mode 100644 index 00000000000..c728b6a5236 --- /dev/null +++ b/testing/test/configurations/WorkloadIdentity.Tests.ps1 @@ -0,0 +1,239 @@ +Describe 'Onboarding with Workload Identity Scenario' { + BeforeAll { + . $PSScriptRoot/../helper/Constants.ps1 + } + + It 'Check if onboarding works with oidc and workload identity enabled' { + az connectedk8s connect -n $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup -l $ARC_LOCATION --enable-oidc-issuer --enable-workload-identity --no-wait + $? | Should -BeTrue + Start-Sleep -Seconds 10 + + # Loop and retry until the configuration installs + $n = 0 + do + { + $output = az connectedk8s show -n $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup + $jsonOutput = [System.Text.Json.JsonDocument]::Parse($output) + $provisioningState = ($output | ConvertFrom-Json).provisioningState + $securityProfile = $jsonOutput.RootElement.GetProperty("securityProfile").GetProperty("workloadIdentity").GetProperty("enabled").GetBoolean() + $oidcIssuerProfile = $jsonOutput.RootElement.GetProperty("oidcIssuerProfile").GetProperty("enabled").GetBoolean() + $issuerUrl = $jsonOutput.RootElement.GetProperty("oidcIssuerProfile").GetProperty("issuerUrl").GetString() + $selfHostedIssuerUrl = $jsonOutput.RootElement.GetProperty("oidcIssuerProfile").GetProperty("selfHostedIssuerUrl").GetString() + $agentState = $jsonOutput.RootElement.GetProperty("arcAgentProfile").GetProperty("agentState").GetString() + Write-Host "Provisioning State: $provisioningState" + Write-Host "Security Profile Status: $securityProfile" + Write-Host "OIDC Issuer Profile Status: $oidcIssuerProfile" + Write-Host "Issuer Url: $issuerUrl" + Write-Host "Self Hosted Issuer Url: $selfHostedIssuerUrl" + Write-Host "Agent State: $agentState" + if ( + $provisioningState -eq $SUCCEEDED -and + $securityProfile -eq $true -and + $oidcIssuerProfile -eq $true -and + ![string]::IsNullOrEmpty($issuerUrl) -and + $issuerUrl -like "*unitedkingdom*" -and + [string]::IsNullOrEmpty($selfHostedIssuerUrl) -and + $agentState -eq $SUCCEEDED + ) { + break + } + Start-Sleep -Seconds 10 + $n += 1 + } while ($n -le $MAX_RETRY_ATTEMPTS) + $n | Should -BeLessOrEqual $MAX_RETRY_ATTEMPTS + } + + It 'Disable workload identity' { + az connectedk8s update -n $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --disable-workload-identity + $? | Should -BeTrue + Start-Sleep -Seconds 10 + + # Loop and retry until the configuration installs + $n = 0 + do + { + $output = az connectedk8s show -n $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup + $jsonOutput = [System.Text.Json.JsonDocument]::Parse($output) + $provisioningState = ($output | ConvertFrom-Json).provisioningState + $securityProfile = $jsonOutput.RootElement.GetProperty("securityProfile").GetProperty("workloadIdentity").GetProperty("enabled").GetBoolean() + $agentState = $jsonOutput.RootElement.GetProperty("arcAgentProfile").GetProperty("agentState").GetString() + Write-Host "Provisioning State: $provisioningState" + Write-Host "Security Profile Status: $securityProfile" + Write-Host "Agent State: $agentState" + if ($provisioningState -eq $SUCCEEDED -and $securityProfile -eq $false -and $agentState -eq $SUCCEEDED) { + break + } + Start-Sleep -Seconds 10 + $n += 1 + } while ($n -le $MAX_RETRY_ATTEMPTS) + $n | Should -BeLessOrEqual $MAX_RETRY_ATTEMPTS + } + + It 'Update the cluster to use workload identity again using update cmd' { + az connectedk8s update -n $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --enable-workload-identity + $? | Should -BeTrue + Start-Sleep -Seconds 10 + + # Loop and retry until the configuration installs + $n = 0 + do + { + $output = az connectedk8s show -n $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup + $jsonOutput = [System.Text.Json.JsonDocument]::Parse($output) + $provisioningState = ($output | ConvertFrom-Json).provisioningState + $securityProfile = $jsonOutput.RootElement.GetProperty("securityProfile").GetProperty("workloadIdentity").GetProperty("enabled").GetBoolean() + $agentState = $jsonOutput.RootElement.GetProperty("arcAgentProfile").GetProperty("agentState").GetString() + Write-Host "Provisioning State: $provisioningState" + Write-Host "Security Profile Status: $securityProfile" + Write-Host "Agent State: $agentState" + if ( + $provisioningState -eq $SUCCEEDED -and + $securityProfile -eq $true -and + $agentState -eq $SUCCEEDED + ) { + break + } + Start-Sleep -Seconds 10 + $n += 1 + } while ($n -le $MAX_RETRY_ATTEMPTS) + $n | Should -BeLessOrEqual $MAX_RETRY_ATTEMPTS + } + + It "Delete the connected instance" { + az connectedk8s delete -n $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --force -y + $? | Should -BeTrue + + # Configuration should be removed from the resource model + az connectedk8s show -n $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup + $? | Should -BeFalse + Start-Sleep -Seconds 10 + } +} + +Describe 'Updating with Workload Identity Scenario' { + BeforeAll { + . $PSScriptRoot/../helper/Constants.ps1 + } + + It 'Onboard a cluster to arc' { + az connectedk8s connect -n $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup -l $ARC_LOCATION --no-wait + $? | Should -BeTrue + Start-Sleep -Seconds 10 + + # Loop and retry until the configuration installs + $n = 0 + do + { + $output = az connectedk8s show -n $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup + $provisioningState = ($output | ConvertFrom-Json).provisioningState + Write-Host "Provisioning State: $provisioningState" + if ($provisioningState -eq $SUCCEEDED) { + break + } + Start-Sleep -Seconds 10 + $n += 1 + } while ($n -le $MAX_RETRY_ATTEMPTS) + $n | Should -BeLessOrEqual $MAX_RETRY_ATTEMPTS + } + + It 'Update the cluster with oidc and workload identity enabled' { + az connectedk8s update -n $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --enable-oidc-issuer --enable-workload-identity + $? | Should -BeTrue + Start-Sleep -Seconds 10 + + # Loop and retry until the configuration installs + $n = 0 + do + { + $output = az connectedk8s show -n $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup + $jsonOutput = [System.Text.Json.JsonDocument]::Parse($output) + $provisioningState = ($output | ConvertFrom-Json).provisioningState + $securityProfile = $jsonOutput.RootElement.GetProperty("securityProfile").GetProperty("workloadIdentity").GetProperty("enabled").GetBoolean() + $oidcIssuerProfile = $jsonOutput.RootElement.GetProperty("oidcIssuerProfile").GetProperty("enabled").GetBoolean() + $issuerUrl = $jsonOutput.RootElement.GetProperty("oidcIssuerProfile").GetProperty("issuerUrl").GetString() + $selfHostedIssuerUrl = $jsonOutput.RootElement.GetProperty("oidcIssuerProfile").GetProperty("selfHostedIssuerUrl").GetString() + $agentState = $jsonOutput.RootElement.GetProperty("arcAgentProfile").GetProperty("agentState").GetString() + Write-Host "Provisioning State: $provisioningState" + Write-Host "Security Profile Status: $securityProfile" + Write-Host "OIDC Issuer Profile Status: $oidcIssuerProfile" + Write-Host "Issuer Url: $issuerUrl" + Write-Host "Self Hosted Issuer Url: $selfHostedIssuerUrl" + Write-Host "Agent State: $agentState" + if ( + $provisioningState -eq $SUCCEEDED -and + $securityProfile -eq $true -and + $oidcIssuerProfile -eq $true -and + ![string]::IsNullOrEmpty($issuerUrl) -and + $issuerUrl -like "*unitedkingdom*" -and + [string]::IsNullOrEmpty($selfHostedIssuerUrl) -and + $agentState -eq $SUCCEEDED + ) { + break + } + Start-Sleep -Seconds 10 + $n += 1 + } while ($n -le $MAX_RETRY_ATTEMPTS) + $n | Should -BeLessOrEqual $MAX_RETRY_ATTEMPTS + } + + It "Delete the connected instance" { + az connectedk8s delete -n $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --force -y + $? | Should -BeTrue + + # Configuration should be removed from the resource model + az connectedk8s show -n $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup + $? | Should -BeFalse + Start-Sleep -Seconds 10 + } +} + +Describe 'Creating with Workload Identity Scenario and Self Hosted Issuer' { + BeforeAll { + . $PSScriptRoot/../helper/Constants.ps1 + + $SelfHostedIssuer = "https://eastus.oic.prod-aks.azure.com/fc50e82b-3761-4218-8691-d98bcgb146da/e6c4bf03-84d9-480c-a269-37a41c28c5cb/" + } + + It 'Check if onboarding works with oidc enabled and self-hosted issuer url passed in' { + az connectedk8s connect -n $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup -l $ARC_LOCATION --enable-oidc-issuer --self-hosted-issuer $SelfHostedIssuer --no-wait + $? | Should -BeTrue + Start-Sleep -Seconds 10 + + # Loop and retry until the configuration installs + $n = 0 + do + { + $output = az connectedk8s show -n $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup + $jsonOutput = [System.Text.Json.JsonDocument]::Parse($output) + $provisioningState = ($output | ConvertFrom-Json).provisioningState + $oidcIssuerProfile = $jsonOutput.RootElement.GetProperty("oidcIssuerProfile").GetProperty("enabled").GetBoolean() + $issuerUrl = $jsonOutput.RootElement.GetProperty("oidcIssuerProfile").GetProperty("issuerUrl").GetString() + $selfHostedIssuerUrl = $jsonOutput.RootElement.GetProperty("oidcIssuerProfile").GetProperty("selfHostedIssuerUrl").GetString() + Write-Host "Provisioning State: $provisioningState" + Write-Host "OIDC Issuer Profile Status: $oidcIssuerProfile" + Write-Host "Issuer Url: $issuerUrl" + Write-Host "Self Hosted Issuer Url: $selfHostedIssuerUrl" + if ( + $provisioningState -eq $SUCCEEDED -and + $oidcIssuerProfile -eq $true -and + [string]::IsNullOrEmpty($issuerUrl) -and + ![string]::IsNullOrEmpty($selfHostedIssuerUrl) -and + $selfHostedIssuerUrl -eq $SelfHostedIssuer + ) { + break + } + Start-Sleep -Seconds 10 + $n += 1 + } while ($n -le $MAX_RETRY_ATTEMPTS) + $n | Should -BeLessOrEqual $MAX_RETRY_ATTEMPTS + } + + It "Delete the connected instance" { + az connectedk8s delete -n $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --force -y + $? | Should -BeTrue + + # Configuration should be removed from the resource model + az connectedk8s show -n $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup + $? | Should -BeFalse + } +} \ No newline at end of file diff --git a/testing/test/helper/Constants.ps1 b/testing/test/helper/Constants.ps1 new file mode 100644 index 00000000000..43006f78a69 --- /dev/null +++ b/testing/test/helper/Constants.ps1 @@ -0,0 +1,5 @@ +$ENVCONFIG = Get-Content -Path $PSScriptRoot/../../settings.json | ConvertFrom-Json + +$MAX_RETRY_ATTEMPTS = 30 +$ARC_LOCATION = "uksouth" +$SUCCEEDED = "Succeeded" \ No newline at end of file From 77ac7cd194353bda835aca1e5338f97cb23bd3fb Mon Sep 17 00:00:00 2001 From: David Hotham Date: Fri, 22 Nov 2024 17:51:59 +0000 Subject: [PATCH 02/42] Tidying - ruff and type annotations fix (#25) * ruff fix * remove duplicate code block * cleaner fix for no existing kubeconfig makes the type annotations true again --- src/connectedk8s/azext_connectedk8s/__init__.py | 4 ++-- src/connectedk8s/azext_connectedk8s/custom.py | 10 +++------- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/src/connectedk8s/azext_connectedk8s/__init__.py b/src/connectedk8s/azext_connectedk8s/__init__.py index d979443c192..4ea68896e31 100644 --- a/src/connectedk8s/azext_connectedk8s/__init__.py +++ b/src/connectedk8s/azext_connectedk8s/__init__.py @@ -43,7 +43,7 @@ def load_arguments(self, command: CLICommand) -> None: COMMAND_LOADER_CLS = Connectedk8sCommandsLoader __all__ = [ - "helps", - "Connectedk8sCommandsLoader", "COMMAND_LOADER_CLS", + "Connectedk8sCommandsLoader", + "helps", ] diff --git a/src/connectedk8s/azext_connectedk8s/custom.py b/src/connectedk8s/azext_connectedk8s/custom.py index 20af4086a31..1ad75b2546f 100644 --- a/src/connectedk8s/azext_connectedk8s/custom.py +++ b/src/connectedk8s/azext_connectedk8s/custom.py @@ -2289,10 +2289,6 @@ def update_connected_cluster( if disable_proxy: helm_content_values["global.isProxyEnabled"] = "False" - # Disable proxy if disable_proxy flag is set - if disable_proxy: - helm_content_values["global.isProxyEnabled"] = "False" - # Set agent version in registry path if connected_cluster.agent_version is not None: agent_version = connected_cluster.agent_version # type: ignore[unreachable] @@ -3271,7 +3267,7 @@ def disable_cluster_connect( def load_kubernetes_configuration(filename: str) -> dict[str, Any]: try: with open(filename) as stream: - k8s_config: dict[str, Any] = yaml.safe_load(stream) + k8s_config: dict[str, Any] = yaml.safe_load(stream) or {} return k8s_config except OSError as ex: if getattr(ex, "errno", 0) == errno.ENOENT: @@ -3372,8 +3368,8 @@ def merge_kubernetes_configurations( break except (KeyError, TypeError): continue - - if existing is None: + + if not existing: existing = addition else: handle_merge(existing, addition, "clusters", replace) From 43e253f9e8304ce0b8bf77a465e1f5d34581f41a Mon Sep 17 00:00:00 2001 From: Atchut Kumar Barli Date: Tue, 3 Dec 2024 12:49:08 -0800 Subject: [PATCH 03/42] add telemetry for error --- src/connectedk8s/azext_connectedk8s/_utils.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/connectedk8s/azext_connectedk8s/_utils.py b/src/connectedk8s/azext_connectedk8s/_utils.py index d55fa4e2023..118418c9988 100644 --- a/src/connectedk8s/azext_connectedk8s/_utils.py +++ b/src/connectedk8s/azext_connectedk8s/_utils.py @@ -1307,6 +1307,11 @@ def helm_install_release( _, error_helm_install = response_helm_install.communicate() if response_helm_install.returncode != 0: helm_install_error_message = error_helm_install.decode("ascii") + helm_error_detail ={ + "Context.Default.AzureCLI.onboardingErrorType":consts.Install_HelmRelease_Fault_Type, + "Context.Default.AzureCLI.onboardingErrorMessage":helm_install_error_message + } + telemetry.add_extension_event("connectedk8s", helm_error_detail) if any( message in helm_install_error_message for message in consts.Helm_Install_Release_Userfault_Messages From 0185e40860f0a81f1d8c3833418c37714ee64d9e Mon Sep 17 00:00:00 2001 From: Jorge Daboub Date: Tue, 3 Dec 2024 16:10:07 -0800 Subject: [PATCH 04/42] initial commit move utils --- .../azext_connectedk8s/clientproxyhelper/__init__.py | 4 ++++ .../{_clientproxyutils.py => clientproxyhelper/_utils.py} | 0 src/connectedk8s/azext_connectedk8s/custom.py | 2 +- 3 files changed, 5 insertions(+), 1 deletion(-) create mode 100644 src/connectedk8s/azext_connectedk8s/clientproxyhelper/__init__.py rename src/connectedk8s/azext_connectedk8s/{_clientproxyutils.py => clientproxyhelper/_utils.py} (100%) diff --git a/src/connectedk8s/azext_connectedk8s/clientproxyhelper/__init__.py b/src/connectedk8s/azext_connectedk8s/clientproxyhelper/__init__.py new file mode 100644 index 00000000000..34913fb394d --- /dev/null +++ b/src/connectedk8s/azext_connectedk8s/clientproxyhelper/__init__.py @@ -0,0 +1,4 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- diff --git a/src/connectedk8s/azext_connectedk8s/_clientproxyutils.py b/src/connectedk8s/azext_connectedk8s/clientproxyhelper/_utils.py similarity index 100% rename from src/connectedk8s/azext_connectedk8s/_clientproxyutils.py rename to src/connectedk8s/azext_connectedk8s/clientproxyhelper/_utils.py diff --git a/src/connectedk8s/azext_connectedk8s/custom.py b/src/connectedk8s/azext_connectedk8s/custom.py index 1ad75b2546f..24feab04b34 100644 --- a/src/connectedk8s/azext_connectedk8s/custom.py +++ b/src/connectedk8s/azext_connectedk8s/custom.py @@ -53,7 +53,7 @@ from kubernetes.config.kube_config import KubeConfigMerger from packaging import version -import azext_connectedk8s._clientproxyutils as clientproxyutils +import azext_connectedk8s.clientproxyhelper._utils as clientproxyutils import azext_connectedk8s._constants as consts import azext_connectedk8s._precheckutils as precheckutils import azext_connectedk8s._troubleshootutils as troubleshootutils From 10e746b15a0fc3a945e604ce3e82055110dde110 Mon Sep 17 00:00:00 2001 From: Jorge Daboub Date: Tue, 3 Dec 2024 16:31:12 -0800 Subject: [PATCH 05/42] enable at token refresh --- src/connectedk8s/azext_connectedk8s/_utils.py | 12 - .../clientproxyhelper/_enums.py | 16 ++ .../clientproxyhelper/_proxylogic.py | 95 ++++++++ .../clientproxyhelper/_utils.py | 25 ++- src/connectedk8s/azext_connectedk8s/custom.py | 205 +++++------------- 5 files changed, 194 insertions(+), 159 deletions(-) create mode 100644 src/connectedk8s/azext_connectedk8s/clientproxyhelper/_enums.py create mode 100644 src/connectedk8s/azext_connectedk8s/clientproxyhelper/_proxylogic.py diff --git a/src/connectedk8s/azext_connectedk8s/_utils.py b/src/connectedk8s/azext_connectedk8s/_utils.py index d55fa4e2023..44916c79ac1 100644 --- a/src/connectedk8s/azext_connectedk8s/_utils.py +++ b/src/connectedk8s/azext_connectedk8s/_utils.py @@ -1542,18 +1542,6 @@ def az_cli(args_str: str) -> Any: return True -# def is_cli_using_msal_auth(): -# response_cli_version = az_cli("version --output json") -# try: -# cli_version = response_cli_version['azure-cli'] -# except Exception as ex: -# raise CLIInternalError(f"Unable to decode the az cli version installed: {ex}") -# if version.parse(cli_version) >= version.parse(consts.AZ_CLI_ADAL_TO_MSAL_MIGRATE_VERSION): -# return True -# else: -# return False - - def is_cli_using_msal_auth() -> bool: response_cli_version = az_cli("version --output json") try: diff --git a/src/connectedk8s/azext_connectedk8s/clientproxyhelper/_enums.py b/src/connectedk8s/azext_connectedk8s/clientproxyhelper/_enums.py new file mode 100644 index 00000000000..3762b1124ba --- /dev/null +++ b/src/connectedk8s/azext_connectedk8s/clientproxyhelper/_enums.py @@ -0,0 +1,16 @@ +from enum import Enum + + +class ProxyStatus(Enum): + FirstRun = 0 + HCTokenRefresh = 1 + AccessTokenRefresh = 2 + AllRefresh = 3 + + @classmethod + def should_hc_token_refresh(cls, status): + return status in {cls.FirstRun, cls.HCTokenRefresh, cls.AllRefresh} + + @classmethod + def should_access_token_refresh(cls, status): + return status in {cls.FirstRun, cls.AccessTokenRefresh, cls.AllRefresh} diff --git a/src/connectedk8s/azext_connectedk8s/clientproxyhelper/_proxylogic.py b/src/connectedk8s/azext_connectedk8s/clientproxyhelper/_proxylogic.py new file mode 100644 index 00000000000..b3759755256 --- /dev/null +++ b/src/connectedk8s/azext_connectedk8s/clientproxyhelper/_proxylogic.py @@ -0,0 +1,95 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + +import azext_connectedk8s._constants as consts +import azext_connectedk8s.clientproxyhelper._utils as clientproxyutils +from azure.cli.core import telemetry +from ..vendored_sdks.models import ( + ListClusterUserCredentialProperties, +) + + +def handle_post_at_to_csp( + cmd, api_server_port, tenant_id, clientproxy_process +): + kid = clientproxyutils.fetch_pop_publickey_kid( + api_server_port, clientproxy_process + ) + post_at_response, expiry = clientproxyutils.fetch_and_post_at_to_csp( + cmd, api_server_port, tenant_id, kid, clientproxy_process + ) + + if post_at_response.status_code != 200: + if ( + post_at_response.status_code == 500 + and "public key expired" in post_at_response.text + ): + # Handle public key rotation + telemetry.set_exception( + exception=post_at_response.text, + fault_type=consts.PoP_Public_Key_Expried_Fault_Type, + summary="PoP public key has expired", + ) + kid = clientproxyutils.fetch_pop_publickey_kid( + api_server_port, clientproxy_process + ) # Fetch rotated public key + # Retry posting AT with the new public key + post_at_response, expiry = clientproxyutils.fetch_and_post_at_to_csp( + cmd, api_server_port, tenant_id, kid, clientproxy_process + ) + # If after second try we still dont get a 200, raise error + if post_at_response.status_code != 200: + telemetry.set_exception( + exception=post_at_response.text, + fault_type=consts.Post_AT_To_ClientProxy_Failed_Fault_Type, + summary="Failed to post access token to client proxy", + ) + clientproxyutils.close_subprocess_and_raise_cli_error( + clientproxy_process, + "Failed to post access token to client proxy" + + post_at_response.text, + ) + + return expiry + + +def get_cluster_user_credentials(client, resource_group_name, cluster_name, auth_method): + list_prop = ListClusterUserCredentialProperties( + authentication_method=auth_method, client_proxy=True + ) + return client.list_cluster_user_credential(resource_group_name, cluster_name, list_prop) + + +def post_register_to_proxy( + data, + token, + client_proxy_port, + subscription_id, + resource_group_name, + cluster_name, + clientproxy_process +): + if token is not None: + data["kubeconfigs"][0]["value"] = clientproxyutils.insert_token_in_kubeconfig( + data, token + ) + + uri = ( + f"http://localhost:{client_proxy_port}/subscriptions/{subscription_id}/resourceGroups/{resource_group_name}" + f"/providers/Microsoft.Kubernetes/connectedClusters/{cluster_name}/register?api-version=2020-10-01" + ) + + # Posting hybrid connection details to proxy in order to get kubeconfig + response = clientproxyutils.make_api_call_with_retries( + uri, + data, + "post", + False, + consts.Post_Hybridconn_Fault_Type, + "Unable to post hybrid connection details to clientproxy", + "Failed to pass hybrid connection details to proxy.", + clientproxy_process, + ) + return response diff --git a/src/connectedk8s/azext_connectedk8s/clientproxyhelper/_utils.py b/src/connectedk8s/azext_connectedk8s/clientproxyhelper/_utils.py index 8d6176245dc..62130e2e81d 100644 --- a/src/connectedk8s/azext_connectedk8s/clientproxyhelper/_utils.py +++ b/src/connectedk8s/azext_connectedk8s/clientproxyhelper/_utils.py @@ -33,6 +33,9 @@ from subprocess import Popen from knack.commands import CLICommand + from azext_connectedk8s.vendored_sdks.preview_2024_07_01.models import ( + CredentialResults, + ) logger = get_logger(__name__) @@ -182,7 +185,7 @@ def fetch_and_post_at_to_csp( ) sys.stderr = original_stderr - return post_at_response + return post_at_response, accessToken.expires_on def insert_token_in_kubeconfig(data: dict[str, Any], token: str) -> str: @@ -195,6 +198,26 @@ def insert_token_in_kubeconfig(data: dict[str, Any], token: str) -> str: return b64kubeconfig +# Prepare data as needed by client proxy executable +def prepare_clientproxy_data(response: CredentialResults) -> dict[str, Any]: + data: dict[str, Any] = {} + data["kubeconfigs"] = [] + kubeconfig = {} + kubeconfig["name"] = "Kubeconfig" + kubeconfig["value"] = b64encode(response.kubeconfigs[0].value).decode("utf-8") # type: ignore[index] + data["kubeconfigs"].append(kubeconfig) + data["hybridConnectionConfig"] = {} + data["hybridConnectionConfig"]["relay"] = response.hybrid_connection_config.relay # type: ignore[attr-defined] + data["hybridConnectionConfig"]["hybridConnectionName"] = ( + response.hybrid_connection_config.hybrid_connection_name # type: ignore[attr-defined] + ) + data["hybridConnectionConfig"]["token"] = response.hybrid_connection_config.token # type: ignore[attr-defined] + data["hybridConnectionConfig"]["expirationTime"] = ( + response.hybrid_connection_config.expiration_time # type: ignore[attr-defined] + ) + return data + + def check_process(processName: str) -> bool: """ Check if there is any running process that contains the given name processName. diff --git a/src/connectedk8s/azext_connectedk8s/custom.py b/src/connectedk8s/azext_connectedk8s/custom.py index 24feab04b34..6a51a30e67c 100644 --- a/src/connectedk8s/azext_connectedk8s/custom.py +++ b/src/connectedk8s/azext_connectedk8s/custom.py @@ -52,8 +52,12 @@ from kubernetes import config from kubernetes.config.kube_config import KubeConfigMerger from packaging import version +from concurrent.futures import ThreadPoolExecutor import azext_connectedk8s.clientproxyhelper._utils as clientproxyutils +import azext_connectedk8s.clientproxyhelper._proxylogic as proxylogic +from azext_connectedk8s.clientproxyhelper._enums import ProxyStatus + import azext_connectedk8s._constants as consts import azext_connectedk8s._precheckutils as precheckutils import azext_connectedk8s._troubleshootutils as troubleshootutils @@ -3737,13 +3741,9 @@ def client_side_proxy_wrapper( client, resource_group_name, cluster_name, - 0, args, client_proxy_port, api_server_port, - operating_system, - creds, - user_type, debug_mode, token=token, path=path, @@ -3751,89 +3751,69 @@ def client_side_proxy_wrapper( ) -# Prepare data as needed by client proxy executable -def prepare_clientproxy_data(response: CredentialResults) -> dict[str, Any]: - data: dict[str, Any] = {} - data["kubeconfigs"] = [] - kubeconfig = {} - kubeconfig["name"] = "Kubeconfig" - kubeconfig["value"] = b64encode(response.kubeconfigs[0].value).decode("utf-8") # type: ignore[index] - data["kubeconfigs"].append(kubeconfig) - data["hybridConnectionConfig"] = {} - data["hybridConnectionConfig"]["relay"] = response.hybrid_connection_config.relay # type: ignore[attr-defined] - data["hybridConnectionConfig"]["hybridConnectionName"] = ( - response.hybrid_connection_config.hybrid_connection_name # type: ignore[attr-defined] - ) - data["hybridConnectionConfig"]["token"] = response.hybrid_connection_config.token # type: ignore[attr-defined] - data["hybridConnectionConfig"]["expirationTime"] = ( - response.hybrid_connection_config.expiration_time # type: ignore[attr-defined] - ) - return data - - def client_side_proxy_main( cmd: CLICommmand, tenant_id: str, client: ConnectedClusterOperations, resource_group_name: str, cluster_name: str, - flag: int, args: list[str], client_proxy_port: int, api_server_port: int, - operating_system: str, - creds: str, - user_type: str, debug_mode: bool, token: str | None = None, path: str = os.path.join(os.path.expanduser("~"), ".kube", "config"), context_name: str | None = None, ) -> None: - expiry, clientproxy_process = client_side_proxy( + hc_expiry, at_expiry, clientproxy_process = client_side_proxy( cmd, tenant_id, client, resource_group_name, cluster_name, - 0, + ProxyStatus.FirstRun, args, client_proxy_port, api_server_port, - operating_system, - creds, - user_type, debug_mode, token=token, path=path, context_name=context_name, clientproxy_process=None, ) - next_refresh_time = expiry - consts.CSP_REFRESH_TIME while True: time.sleep(60) if clientproxyutils.check_if_csp_is_running(clientproxy_process): - if time.time() >= next_refresh_time: - expiry, clientproxy_process = client_side_proxy( + flag = None + if time.time() >= (hc_expiry - consts.CSP_REFRESH_TIME): + flag = ProxyStatus.HCTokenRefresh + elif time.time() >= (at_expiry - consts.CSP_REFRESH_TIME): + flag = ProxyStatus.AccessTokenRefresh + + if flag is not None: + print("Refreshing tokens with flag ", flag) + new_hc_expiry, new_at_expiry, clientproxy_process = client_side_proxy( cmd, tenant_id, client, resource_group_name, cluster_name, - 1, + flag, args, client_proxy_port, api_server_port, - operating_system, - creds, - user_type, debug_mode, token=token, path=path, context_name=context_name, clientproxy_process=clientproxy_process, ) - next_refresh_time = expiry - consts.CSP_REFRESH_TIME + if flag == ProxyStatus.HCTokenRefresh: + hc_expiry = new_hc_expiry + elif flag == ProxyStatus.AccessTokenRefresh: + at_expiry = new_at_expiry + else: telemetry.set_exception( exception="Process closed externally.", @@ -3853,9 +3833,6 @@ def client_side_proxy( args: list[str], client_proxy_port: int, api_server_port: int, - operating_system: str, - creds: str, - user_type: str, debug_mode: bool, token: str | None = None, path: str = os.path.join(os.path.expanduser("~"), ".kube", "config"), @@ -3865,27 +3842,18 @@ def client_side_proxy( subscription_id = get_subscription_id(cmd.cli_ctx) auth_method = "Token" if token is not None else "AAD" + hc_expiry, at_expiry = 0, 0 + # Fetching hybrid connection details from Userrp - try: - list_prop = ListClusterUserCredentialProperties( - authentication_method=auth_method, client_proxy=True - ) - cluster_user_credentials = client.list_cluster_user_credential( - resource_group_name, cluster_name, list_prop - ) - except Exception as e: - if flag == 1: - assert clientproxy_process is not None - clientproxy_process.terminate() - utils.arm_exception_handler( - e, - consts.Get_Credentials_Failed_Fault_Type, - "Unable to list cluster user credentials", - ) - raise CLIInternalError(f"Failed to get credentials: {e}") + # We do this in a separate process to avoid blocking the main thread + # Since we still need to bring up the proxy and make API calls to it. + if ProxyStatus.should_hc_token_refresh(flag): + with ThreadPoolExecutor() as executor: + future_get_cluster_user_credentials = executor.submit( + proxylogic.get_cluster_user_credentials, client, resource_group_name, cluster_name, auth_method) # Starting the client proxy process, if this is the first time that this function is invoked - if flag == 0: + if flag == ProxyStatus.FirstRun: try: if debug_mode: clientproxy_process = Popen(args) @@ -3901,96 +3869,41 @@ def client_side_proxy( ) raise CLIInternalError(f"Failed to start proxy process: {e}") - # refresh token approach if cli is using ADAL auth (for cli < 2.30.0) - if (not utils.is_cli_using_msal_auth()) and user_type == "user": - identity_data = {} - identity_data["refreshToken"] = creds - identity_uri = f"https://localhost:{api_server_port}/identity/rt" - - # Needed to prevent skip tls warning from printing to the console - original_stderr = sys.stderr - with open(os.devnull, "w") as f: - sys.stderr = f - - clientproxyutils.make_api_call_with_retries( - identity_uri, - identity_data, - "post", - False, - consts.Post_RefreshToken_Fault_Type, - "Unable to post refresh token details to clientproxy", - "Failed to pass refresh token details to proxy.", - clientproxy_process, - ) - sys.stderr = original_stderr - assert clientproxy_process is not None - if token is None and ( - utils.is_cli_using_msal_auth() - ): # jwt token approach if cli is using MSAL. This is for cli >= 2.30.0 - kid = clientproxyutils.fetch_pop_publickey_kid( - api_server_port, clientproxy_process - ) - post_at_response = clientproxyutils.fetch_and_post_at_to_csp( - cmd, api_server_port, tenant_id, kid, clientproxy_process - ) - if post_at_response.status_code != 200: - if ( - post_at_response.status_code == 500 - and "public key expired" in post_at_response.text - ): - # pop public key must have been rotated - telemetry.set_exception( - exception=post_at_response.text, - fault_type=consts.PoP_Public_Key_Expried_Fault_Type, - summary="PoP public key has expired", - ) - kid = clientproxyutils.fetch_pop_publickey_kid( - api_server_port, clientproxy_process - ) # fetch the rotated PoP public key - # fetch and post the at corresponding to the new public key - clientproxyutils.fetch_and_post_at_to_csp( - cmd, api_server_port, tenant_id, kid, clientproxy_process - ) - else: - telemetry.set_exception( - exception=post_at_response.text, - fault_type=consts.Post_AT_To_ClientProxy_Failed_Fault_Type, - summary="Failed to post access token to client proxy", - ) - clientproxyutils.close_subprocess_and_raise_cli_error( - clientproxy_process, - "Failed to post access token to client proxy" - + post_at_response.text, - ) + if token is None and ProxyStatus.should_access_token_refresh(flag): + # jwt token approach if cli is using MSAL. This is for cli >= 2.30.0 + at_expiry = proxylogic.handle_post_at_to_csp(cmd, api_server_port, tenant_id, clientproxy_process) - data = prepare_clientproxy_data(cluster_user_credentials) - expiry = data["hybridConnectionConfig"]["expirationTime"] + # Check hybrid connection details from Userrp + response = None - if token is not None: - data["kubeconfigs"][0]["value"] = clientproxyutils.insert_token_in_kubeconfig( - data, token - ) + if ProxyStatus.should_hc_token_refresh(flag): + try: + response = future_get_cluster_user_credentials.result() + except Exception as e: + clientproxy_process.terminate() + utils.arm_exception_handler( + e, + consts.Get_Credentials_Failed_Fault_Type, + "Unable to list cluster user credentials", + ) + raise CLIInternalError(f"Failed to get credentials: {e}") - uri = ( - f"http://localhost:{client_proxy_port}/subscriptions/{subscription_id}/resourceGroups/{resource_group_name}" - f"/providers/Microsoft.Kubernetes/connectedClusters/{cluster_name}/register?api-version=2020-10-01" - ) + data = clientproxyutils.prepare_clientproxy_data(response) + hc_expiry = data["hybridConnectionConfig"]["expirationTime"] - # Posting hybrid connection details to proxy in order to get kubeconfig - response = clientproxyutils.make_api_call_with_retries( - uri, - data, - "post", - False, - consts.Post_Hybridconn_Fault_Type, - "Unable to post hybrid connection details to clientproxy", - "Failed to pass hybrid connection details to proxy.", - clientproxy_process, - ) + response = proxylogic.post_register_to_proxy( + data, + token, + client_proxy_port, + subscription_id, + resource_group_name, + cluster_name, + clientproxy_process + ) - if flag == 0: + if flag == ProxyStatus.FirstRun: # Decoding kubeconfig into a string try: kubeconfig = json.loads(response.text) @@ -4033,7 +3946,7 @@ def client_side_proxy( clientproxy_process, "Failed to merge kubeconfig." + str(e) ) - return expiry, clientproxy_process + return hc_expiry, at_expiry, clientproxy_process def check_cl_registration_and_get_oid( From cdb52e2196bfc2179345b5866cb5a9f0e4d9ac77 Mon Sep 17 00:00:00 2001 From: Atchut Kumar Barli Date: Tue, 3 Dec 2024 16:49:27 -0800 Subject: [PATCH 06/42] spaces --- src/connectedk8s/azext_connectedk8s/_utils.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/connectedk8s/azext_connectedk8s/_utils.py b/src/connectedk8s/azext_connectedk8s/_utils.py index 118418c9988..18c55883752 100644 --- a/src/connectedk8s/azext_connectedk8s/_utils.py +++ b/src/connectedk8s/azext_connectedk8s/_utils.py @@ -1307,9 +1307,9 @@ def helm_install_release( _, error_helm_install = response_helm_install.communicate() if response_helm_install.returncode != 0: helm_install_error_message = error_helm_install.decode("ascii") - helm_error_detail ={ - "Context.Default.AzureCLI.onboardingErrorType":consts.Install_HelmRelease_Fault_Type, - "Context.Default.AzureCLI.onboardingErrorMessage":helm_install_error_message + helm_error_detail = { + "Context.Default.AzureCLI.onboardingErrorType": consts.Install_HelmRelease_Fault_Type, + "Context.Default.AzureCLI.onboardingErrorMessage": helm_install_error_message } telemetry.add_extension_event("connectedk8s", helm_error_detail) if any( From 52b6d33fdcaf9873568d11101db5027a6e8bb181 Mon Sep 17 00:00:00 2001 From: Jorge Daboub Date: Tue, 3 Dec 2024 17:09:16 -0800 Subject: [PATCH 07/42] clean up --- src/connectedk8s/azext_connectedk8s/custom.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/src/connectedk8s/azext_connectedk8s/custom.py b/src/connectedk8s/azext_connectedk8s/custom.py index 6a51a30e67c..228a53a5750 100644 --- a/src/connectedk8s/azext_connectedk8s/custom.py +++ b/src/connectedk8s/azext_connectedk8s/custom.py @@ -14,7 +14,6 @@ import re import shutil import stat -import sys import tempfile import time import urllib.request @@ -22,6 +21,7 @@ from glob import glob from subprocess import DEVNULL, PIPE, Popen from typing import TYPE_CHECKING, Any, Iterable +from concurrent.futures import ThreadPoolExecutor import yaml from azure.cli.command_modules.role import graph_client_factory @@ -52,7 +52,6 @@ from kubernetes import config from kubernetes.config.kube_config import KubeConfigMerger from packaging import version -from concurrent.futures import ThreadPoolExecutor import azext_connectedk8s.clientproxyhelper._utils as clientproxyutils import azext_connectedk8s.clientproxyhelper._proxylogic as proxylogic @@ -75,7 +74,6 @@ ConnectedClusterIdentity, ConnectedClusterPatch, Gateway, - ListClusterUserCredentialProperties, OidcIssuerProfile, SecurityProfile, SecurityProfileWorkloadIdentity, @@ -89,9 +87,6 @@ from kubernetes.client import V1NodeList from kubernetes.config.kube_config import ConfigNode - from azext_connectedk8s.vendored_sdks.preview_2024_07_01.models import ( - CredentialResults, - ) from azext_connectedk8s.vendored_sdks.preview_2024_07_01.operations import ( ConnectedClusterOperations, ) @@ -3792,7 +3787,6 @@ def client_side_proxy_main( flag = ProxyStatus.AccessTokenRefresh if flag is not None: - print("Refreshing tokens with flag ", flag) new_hc_expiry, new_at_expiry, clientproxy_process = client_side_proxy( cmd, tenant_id, From 9cc1d72509340abff0f744541c7e851f9b719751 Mon Sep 17 00:00:00 2001 From: Atchut Kumar Barli Date: Wed, 4 Dec 2024 11:20:33 -0800 Subject: [PATCH 08/42] Update _utils.py --- src/connectedk8s/azext_connectedk8s/_utils.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/connectedk8s/azext_connectedk8s/_utils.py b/src/connectedk8s/azext_connectedk8s/_utils.py index 118418c9988..18c55883752 100644 --- a/src/connectedk8s/azext_connectedk8s/_utils.py +++ b/src/connectedk8s/azext_connectedk8s/_utils.py @@ -1307,9 +1307,9 @@ def helm_install_release( _, error_helm_install = response_helm_install.communicate() if response_helm_install.returncode != 0: helm_install_error_message = error_helm_install.decode("ascii") - helm_error_detail ={ - "Context.Default.AzureCLI.onboardingErrorType":consts.Install_HelmRelease_Fault_Type, - "Context.Default.AzureCLI.onboardingErrorMessage":helm_install_error_message + helm_error_detail = { + "Context.Default.AzureCLI.onboardingErrorType": consts.Install_HelmRelease_Fault_Type, + "Context.Default.AzureCLI.onboardingErrorMessage": helm_install_error_message } telemetry.add_extension_event("connectedk8s", helm_error_detail) if any( From 173399537da8e8ba966237a132f0405125eb0bc8 Mon Sep 17 00:00:00 2001 From: Jorge Daboub Date: Wed, 4 Dec 2024 14:28:33 -0800 Subject: [PATCH 09/42] fix lint errors --- .../clientproxyhelper/_enums.py | 4 +++ .../clientproxyhelper/_proxylogic.py | 25 ++++++++++--------- .../clientproxyhelper/_utils.py | 1 + src/connectedk8s/azext_connectedk8s/custom.py | 22 ++++++++++------ .../latest/test_connectedk8s_scenario.py | 18 ++++++++----- 5 files changed, 44 insertions(+), 26 deletions(-) diff --git a/src/connectedk8s/azext_connectedk8s/clientproxyhelper/_enums.py b/src/connectedk8s/azext_connectedk8s/clientproxyhelper/_enums.py index 3762b1124ba..86a9e091421 100644 --- a/src/connectedk8s/azext_connectedk8s/clientproxyhelper/_enums.py +++ b/src/connectedk8s/azext_connectedk8s/clientproxyhelper/_enums.py @@ -1,3 +1,7 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- from enum import Enum diff --git a/src/connectedk8s/azext_connectedk8s/clientproxyhelper/_proxylogic.py b/src/connectedk8s/azext_connectedk8s/clientproxyhelper/_proxylogic.py index b3759755256..848ad09f84a 100644 --- a/src/connectedk8s/azext_connectedk8s/clientproxyhelper/_proxylogic.py +++ b/src/connectedk8s/azext_connectedk8s/clientproxyhelper/_proxylogic.py @@ -3,20 +3,18 @@ # Licensed under the MIT License. See License.txt in the project root for license information. # -------------------------------------------------------------------------------------------- +from azure.cli.core import telemetry + import azext_connectedk8s._constants as consts import azext_connectedk8s.clientproxyhelper._utils as clientproxyutils -from azure.cli.core import telemetry + from ..vendored_sdks.models import ( ListClusterUserCredentialProperties, ) -def handle_post_at_to_csp( - cmd, api_server_port, tenant_id, clientproxy_process -): - kid = clientproxyutils.fetch_pop_publickey_kid( - api_server_port, clientproxy_process - ) +def handle_post_at_to_csp(cmd, api_server_port, tenant_id, clientproxy_process): + kid = clientproxyutils.fetch_pop_publickey_kid(api_server_port, clientproxy_process) post_at_response, expiry = clientproxyutils.fetch_and_post_at_to_csp( cmd, api_server_port, tenant_id, kid, clientproxy_process ) @@ -48,18 +46,21 @@ def handle_post_at_to_csp( ) clientproxyutils.close_subprocess_and_raise_cli_error( clientproxy_process, - "Failed to post access token to client proxy" - + post_at_response.text, + "Failed to post access token to client proxy" + post_at_response.text, ) return expiry -def get_cluster_user_credentials(client, resource_group_name, cluster_name, auth_method): +def get_cluster_user_credentials( + client, resource_group_name, cluster_name, auth_method +): list_prop = ListClusterUserCredentialProperties( authentication_method=auth_method, client_proxy=True ) - return client.list_cluster_user_credential(resource_group_name, cluster_name, list_prop) + return client.list_cluster_user_credential( + resource_group_name, cluster_name, list_prop + ) def post_register_to_proxy( @@ -69,7 +70,7 @@ def post_register_to_proxy( subscription_id, resource_group_name, cluster_name, - clientproxy_process + clientproxy_process, ): if token is not None: data["kubeconfigs"][0]["value"] = clientproxyutils.insert_token_in_kubeconfig( diff --git a/src/connectedk8s/azext_connectedk8s/clientproxyhelper/_utils.py b/src/connectedk8s/azext_connectedk8s/clientproxyhelper/_utils.py index 62130e2e81d..9c2413c7d63 100644 --- a/src/connectedk8s/azext_connectedk8s/clientproxyhelper/_utils.py +++ b/src/connectedk8s/azext_connectedk8s/clientproxyhelper/_utils.py @@ -33,6 +33,7 @@ from subprocess import Popen from knack.commands import CLICommand + from azext_connectedk8s.vendored_sdks.preview_2024_07_01.models import ( CredentialResults, ) diff --git a/src/connectedk8s/azext_connectedk8s/custom.py b/src/connectedk8s/azext_connectedk8s/custom.py index 228a53a5750..c36edbbf9b4 100644 --- a/src/connectedk8s/azext_connectedk8s/custom.py +++ b/src/connectedk8s/azext_connectedk8s/custom.py @@ -18,10 +18,10 @@ import time import urllib.request from base64 import b64decode, b64encode +from concurrent.futures import ThreadPoolExecutor from glob import glob from subprocess import DEVNULL, PIPE, Popen from typing import TYPE_CHECKING, Any, Iterable -from concurrent.futures import ThreadPoolExecutor import yaml from azure.cli.command_modules.role import graph_client_factory @@ -53,19 +53,18 @@ from kubernetes.config.kube_config import KubeConfigMerger from packaging import version -import azext_connectedk8s.clientproxyhelper._utils as clientproxyutils -import azext_connectedk8s.clientproxyhelper._proxylogic as proxylogic -from azext_connectedk8s.clientproxyhelper._enums import ProxyStatus - import azext_connectedk8s._constants as consts import azext_connectedk8s._precheckutils as precheckutils import azext_connectedk8s._troubleshootutils as troubleshootutils import azext_connectedk8s._utils as utils +import azext_connectedk8s.clientproxyhelper._proxylogic as proxylogic +import azext_connectedk8s.clientproxyhelper._utils as clientproxyutils from azext_connectedk8s._client_factory import ( cf_connectedmachine, cf_resource_groups, resource_providers_client, ) +from azext_connectedk8s.clientproxyhelper._enums import ProxyStatus from .vendored_sdks.preview_2024_07_01.models import ( ArcAgentProfile, @@ -3844,7 +3843,12 @@ def client_side_proxy( if ProxyStatus.should_hc_token_refresh(flag): with ThreadPoolExecutor() as executor: future_get_cluster_user_credentials = executor.submit( - proxylogic.get_cluster_user_credentials, client, resource_group_name, cluster_name, auth_method) + proxylogic.get_cluster_user_credentials, + client, + resource_group_name, + cluster_name, + auth_method, + ) # Starting the client proxy process, if this is the first time that this function is invoked if flag == ProxyStatus.FirstRun: @@ -3867,7 +3871,9 @@ def client_side_proxy( if token is None and ProxyStatus.should_access_token_refresh(flag): # jwt token approach if cli is using MSAL. This is for cli >= 2.30.0 - at_expiry = proxylogic.handle_post_at_to_csp(cmd, api_server_port, tenant_id, clientproxy_process) + at_expiry = proxylogic.handle_post_at_to_csp( + cmd, api_server_port, tenant_id, clientproxy_process + ) # Check hybrid connection details from Userrp response = None @@ -3894,7 +3900,7 @@ def client_side_proxy( subscription_id, resource_group_name, cluster_name, - clientproxy_process + clientproxy_process, ) if flag == ProxyStatus.FirstRun: diff --git a/src/connectedk8s/azext_connectedk8s/tests/latest/test_connectedk8s_scenario.py b/src/connectedk8s/azext_connectedk8s/tests/latest/test_connectedk8s_scenario.py index cca97a6ef15..0d6c2377041 100644 --- a/src/connectedk8s/azext_connectedk8s/tests/latest/test_connectedk8s_scenario.py +++ b/src/connectedk8s/azext_connectedk8s/tests/latest/test_connectedk8s_scenario.py @@ -43,8 +43,10 @@ CONFIG = json.load(f) for key in CONFIG: if not CONFIG[key]: - raise RequiredArgumentMissingError(f"Missing required configuration in {config_path} file. Make sure all \ -properties are populated.") + raise RequiredArgumentMissingError( + f"Missing required configuration in {config_path} file. Make sure all \ +properties are populated." + ) def _get_test_data_file(filename): @@ -148,8 +150,10 @@ def install_kubectl_client(): elif operating_system == "linux" or operating_system == "darwin": kubectl_path = os.path.join(kubectl_filepath, "kubectl") else: - logger.warning(f"The {operating_system} platform is not currently supported for installing kubectl \ -client.") + logger.warning( + f"The {operating_system} platform is not currently supported for installing kubectl \ +client." + ) return if os.path.isfile(kubectl_path): @@ -712,8 +716,10 @@ def test_upgrade(self, resource_group): "connectedk8s upgrade -g {rg} -n {name} --kube-config {kubeconfig} --kube-context \ {managed_cluster_name}-admin" ) - response = requests.post(f'https://{CONFIG["location"]}.dp.kubernetesconfiguration.azure.com/azure-\ - arc-k8sagents/GetLatestHelmPackagePath?api-version=2019-11-01-preview&releaseTrain=stable') + response = requests.post( + f'https://{CONFIG["location"]}.dp.kubernetesconfiguration.azure.com/azure-\ + arc-k8sagents/GetLatestHelmPackagePath?api-version=2019-11-01-preview&releaseTrain=stable' + ) jsonData = json.loads(response.text) repo_path = jsonData["repositoryPath"] index_value = 0 From 401c18f8da6c49caff19aac0c2baf4b0db29a54a Mon Sep 17 00:00:00 2001 From: Jorge Daboub Date: Wed, 4 Dec 2024 16:24:18 -0800 Subject: [PATCH 10/42] fix lint errors --- .../clientproxyhelper/_enums.py | 6 ++- .../clientproxyhelper/_proxylogic.py | 54 ++++++++++++++----- .../clientproxyhelper/_utils.py | 2 +- src/connectedk8s/azext_connectedk8s/custom.py | 11 ++-- 4 files changed, 52 insertions(+), 21 deletions(-) diff --git a/src/connectedk8s/azext_connectedk8s/clientproxyhelper/_enums.py b/src/connectedk8s/azext_connectedk8s/clientproxyhelper/_enums.py index 86a9e091421..a94c5a92acd 100644 --- a/src/connectedk8s/azext_connectedk8s/clientproxyhelper/_enums.py +++ b/src/connectedk8s/azext_connectedk8s/clientproxyhelper/_enums.py @@ -2,6 +2,8 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. See License.txt in the project root for license information. # -------------------------------------------------------------------------------------------- +from __future__ import annotations + from enum import Enum @@ -12,9 +14,9 @@ class ProxyStatus(Enum): AllRefresh = 3 @classmethod - def should_hc_token_refresh(cls, status): + def should_hc_token_refresh(cls, status: ProxyStatus) -> bool: return status in {cls.FirstRun, cls.HCTokenRefresh, cls.AllRefresh} @classmethod - def should_access_token_refresh(cls, status): + def should_access_token_refresh(cls, status: ProxyStatus) -> bool: return status in {cls.FirstRun, cls.AccessTokenRefresh, cls.AllRefresh} diff --git a/src/connectedk8s/azext_connectedk8s/clientproxyhelper/_proxylogic.py b/src/connectedk8s/azext_connectedk8s/clientproxyhelper/_proxylogic.py index 848ad09f84a..71345064af6 100644 --- a/src/connectedk8s/azext_connectedk8s/clientproxyhelper/_proxylogic.py +++ b/src/connectedk8s/azext_connectedk8s/clientproxyhelper/_proxylogic.py @@ -2,6 +2,9 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. See License.txt in the project root for license information. # -------------------------------------------------------------------------------------------- +from __future__ import annotations + +from typing import TYPE_CHECKING, Any from azure.cli.core import telemetry @@ -12,8 +15,26 @@ ListClusterUserCredentialProperties, ) +if TYPE_CHECKING: + from subprocess import Popen + + from knack.commands import CLICommmand + from requests.models import Response -def handle_post_at_to_csp(cmd, api_server_port, tenant_id, clientproxy_process): + from azext_connectedk8s.vendored_sdks.preview_2024_07_01.models import ( + CredentialResults, + ) + from azext_connectedk8s.vendored_sdks.preview_2024_07_01.operations import ( + ConnectedClusterOperations, + ) + + +def handle_post_at_to_csp( + cmd: CLICommmand, + api_server_port: int, + tenant_id: str, + clientproxy_process: Popen[bytes], +) -> int: kid = clientproxyutils.fetch_pop_publickey_kid(api_server_port, clientproxy_process) post_at_response, expiry = clientproxyutils.fetch_and_post_at_to_csp( cmd, api_server_port, tenant_id, kid, clientproxy_process @@ -53,25 +74,32 @@ def handle_post_at_to_csp(cmd, api_server_port, tenant_id, clientproxy_process): def get_cluster_user_credentials( - client, resource_group_name, cluster_name, auth_method -): + client: ConnectedClusterOperations, + resource_group_name: str, + cluster_name: str, + auth_method: str, +) -> CredentialResults: list_prop = ListClusterUserCredentialProperties( authentication_method=auth_method, client_proxy=True ) - return client.list_cluster_user_credential( - resource_group_name, cluster_name, list_prop + + result: CredentialResults = client.list_cluster_user_credential( # type: ignore[call-overload] + resource_group_name, + cluster_name, + list_prop, ) + return result def post_register_to_proxy( - data, - token, - client_proxy_port, - subscription_id, - resource_group_name, - cluster_name, - clientproxy_process, -): + data: dict[str, Any], + token: str | None, + client_proxy_port: int, + subscription_id: str, + resource_group_name: str, + cluster_name: str, + clientproxy_process: Popen[bytes], +) -> Response: if token is not None: data["kubeconfigs"][0]["value"] = clientproxyutils.insert_token_in_kubeconfig( data, token diff --git a/src/connectedk8s/azext_connectedk8s/clientproxyhelper/_utils.py b/src/connectedk8s/azext_connectedk8s/clientproxyhelper/_utils.py index 9c2413c7d63..dcf9469d430 100644 --- a/src/connectedk8s/azext_connectedk8s/clientproxyhelper/_utils.py +++ b/src/connectedk8s/azext_connectedk8s/clientproxyhelper/_utils.py @@ -132,7 +132,7 @@ def fetch_and_post_at_to_csp( tenant_id: str, kid: str, clientproxy_process: Popen[bytes], -) -> requests.Response: +) -> tuple[requests.Response, int]: req_cnfJSON = {"kid": kid, "xms_ksl": "sw"} req_cnf = base64.urlsafe_b64encode(json.dumps(req_cnfJSON).encode("utf-8")).decode( "utf-8" diff --git a/src/connectedk8s/azext_connectedk8s/custom.py b/src/connectedk8s/azext_connectedk8s/custom.py index c36edbbf9b4..e195ce04261 100644 --- a/src/connectedk8s/azext_connectedk8s/custom.py +++ b/src/connectedk8s/azext_connectedk8s/custom.py @@ -85,6 +85,7 @@ from knack.commands import CLICommmand from kubernetes.client import V1NodeList from kubernetes.config.kube_config import ConfigNode + from requests.models import Response from azext_connectedk8s.vendored_sdks.preview_2024_07_01.operations import ( ConnectedClusterOperations, @@ -3822,7 +3823,7 @@ def client_side_proxy( client: ConnectedClusterOperations, resource_group_name: str, cluster_name: str, - flag: int, + flag: ProxyStatus, args: list[str], client_proxy_port: int, api_server_port: int, @@ -3831,7 +3832,7 @@ def client_side_proxy( path: str = os.path.join(os.path.expanduser("~"), ".kube", "config"), context_name: str | None = None, clientproxy_process: Popen[bytes] | None = None, -) -> tuple[int, Popen[bytes]]: +) -> tuple[int, int, Popen[bytes]]: subscription_id = get_subscription_id(cmd.cli_ctx) auth_method = "Token" if token is not None else "AAD" @@ -3876,11 +3877,11 @@ def client_side_proxy( ) # Check hybrid connection details from Userrp - response = None + response: Response if ProxyStatus.should_hc_token_refresh(flag): try: - response = future_get_cluster_user_credentials.result() + response_data = future_get_cluster_user_credentials.result() except Exception as e: clientproxy_process.terminate() utils.arm_exception_handler( @@ -3890,7 +3891,7 @@ def client_side_proxy( ) raise CLIInternalError(f"Failed to get credentials: {e}") - data = clientproxyutils.prepare_clientproxy_data(response) + data = clientproxyutils.prepare_clientproxy_data(response_data) hc_expiry = data["hybridConnectionConfig"]["expirationTime"] response = proxylogic.post_register_to_proxy( From 2358a49d601de41c413dfcc4380fdc7a534e5759 Mon Sep 17 00:00:00 2001 From: Jorge Daboub <52983326+JorgeDaboub@users.noreply.github.com> Date: Tue, 10 Dec 2024 11:44:33 -0600 Subject: [PATCH 11/42] Add Kubectl Check (#29) Add Kubectl Check to Proxy Test --- testing/Test.ps1 | 4 ++ .../configurations/ConnectProxy.Tests.ps1 | 38 ++++++++++++++++++- 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/testing/Test.ps1 b/testing/Test.ps1 index 1f9fc5481f0..7c6f522d082 100644 --- a/testing/Test.ps1 +++ b/testing/Test.ps1 @@ -17,6 +17,10 @@ az config set core.only_show_errors=true $ENVCONFIG = Get-Content -Path $PSScriptRoot/settings.json | ConvertFrom-Json +# Install the powershell-yaml module +# Needed to parse the kubeconfig file +Install-Module -Name powershell-yaml -Force -Scope CurrentUser + az account set --subscription $ENVCONFIG.subscriptionId $Env:KUBECONFIG="$PSScriptRoot/tmp/KUBECONFIG" diff --git a/testing/test/configurations/ConnectProxy.Tests.ps1 b/testing/test/configurations/ConnectProxy.Tests.ps1 index 3e6f8e06899..4de00bbeba0 100644 --- a/testing/test/configurations/ConnectProxy.Tests.ps1 +++ b/testing/test/configurations/ConnectProxy.Tests.ps1 @@ -39,13 +39,49 @@ Describe 'Connectedk8s Proxy Scenario' { } -ArgumentList $ENVCONFIG.arcClusterName, $ENVCONFIG.resourceGroup # Wait for a certain amount of time (e.g., 30 seconds) - Start-Sleep -Seconds 30 + Start-Sleep -Seconds 60 # Display the output Write-Host "Proxy Job State: $($proxyJob.State)" # Check if the job ran successfully $proxyJob.State | Should -Be 'Running' + + # Check if the kubeconfig file has been updated to use the proxy + $kubeconfigPath = "~/.kube/config" + $kubeconfig = Get-Content $kubeconfigPath -Raw | ConvertFrom-Yaml + # Extract the current context + $currentContext = $kubeconfig.'current-context' + + # Validate that the current context is for the arc machine + $currentContext | Should -Be $ENVCONFIG.arcClusterName + + # Find the cluster associated with the current context + $context = $kubeconfig.contexts | Where-Object { $_.name -eq $currentContext } + $clusterName = $context.context.cluster + + # Retrieve the server URL for the cluster + $cluster = $kubeconfig.clusters | Where-Object { $_.name -eq $clusterName } + $server = $cluster.cluster.server + + # Validate the server URL + $server | Should -Match "^https://127.0.0.1:47011/proxies/" + + # Check if the proxy command ran successfully + $kubectlJob = Start-Job -ScriptBlock { + try { + $output = kubectl get pods -n azure-arc 2>&1 + return @{ Success = $LASTEXITCODE -eq 0; Output = $output } + } catch { + return @{ Success = $false; Output = $_.Exception.Message } + } + } + + $kubectlJob | Wait-Job + $kubectlResult = Receive-Job -Job $kubectlJob + + # Assert that the result is 0 + $kubectlResult.Success | Should -BeTrue Stop-Job -Job $proxyJob Remove-Job -Job $proxyJob From 80d29bcc51f98dd487b8362331c81c9b3294ad63 Mon Sep 17 00:00:00 2001 From: Jorge Daboub <52983326+JorgeDaboub@users.noreply.github.com> Date: Thu, 9 Jan 2025 15:20:40 -0800 Subject: [PATCH 12/42] Pull Arc-Proxy from MAR (#28) --- .../azext_connectedk8s/_constants.py | 10 +- .../azext_connectedk8s/_fileutils.py | 42 +++ .../azext_connectedk8s/_troubleshootutils.py | 3 +- .../clientproxyhelper/_binaryutils.py | 277 ++++++++++++++++++ src/connectedk8s/azext_connectedk8s/custom.py | 154 +--------- .../latest/test_connectedk8s_scenario.py | 26 +- src/connectedk8s/setup.py | 1 + 7 files changed, 350 insertions(+), 163 deletions(-) create mode 100644 src/connectedk8s/azext_connectedk8s/_fileutils.py create mode 100644 src/connectedk8s/azext_connectedk8s/clientproxyhelper/_binaryutils.py diff --git a/src/connectedk8s/azext_connectedk8s/_constants.py b/src/connectedk8s/azext_connectedk8s/_constants.py index 15eb12ba05f..b8eaa6f048c 100644 --- a/src/connectedk8s/azext_connectedk8s/_constants.py +++ b/src/connectedk8s/azext_connectedk8s/_constants.py @@ -163,6 +163,7 @@ Create_Directory_Fault_Type = ( "Error while creating directory for placing the executable" ) +Remove_File_Fault_Type = "Error while deleting the specified file" Run_Clientproxy_Fault_Type = "Error while starting client proxy process." Post_Hybridconn_Fault_Type = ( "Error while posting hybrid connection details to proxy process" @@ -460,23 +461,20 @@ ) DNS_Check_Result_String = "DNS Result:" AZ_CLI_ADAL_TO_MSAL_MIGRATE_VERSION = "2.30.0" -CLIENT_PROXY_VERSION = "1.3.022011" +CLIENT_PROXY_VERSION = "1.3.029301" +CLIENT_PROXY_FOLDER = ".clientproxy" API_SERVER_PORT = 47011 CLIENT_PROXY_PORT = 47010 CLIENTPROXY_CLIENT_ID = "04b07795-8ddb-461a-bbee-02f9e1bf7b46" API_CALL_RETRIES = 12 DEFAULT_REQUEST_TIMEOUT = 10 # seconds -RELEASE_DATE_WINDOWS = "release12-01-23" -RELEASE_DATE_LINUX = "release12-01-23" CSP_REFRESH_TIME = 300 # Default timeout in seconds for Onboarding Helm Install DEFAULT_MAX_ONBOARDING_TIMEOUT_HELMVALUE_SECONDS = "1200" # URL constants -CSP_Storage_Url = "https://k8sconnectcsp.azureedge.net" -CSP_Storage_Url_Mooncake = "https://k8sconnectcsp.blob.core.chinacloudapi.cn" -CSP_Storage_Url_Fairfax = "https://k8sconnectcsp.azureedge.us" +CLIENT_PROXY_MCR_TARGET = "mcr.microsoft.com/azureconnectivity/proxy" HELM_STORAGE_URL = "https://k8connecthelm.azureedge.net" HELM_VERSION = "v3.12.2" Download_And_Install_Kubectl_Fault_Type = "Failed to download and install kubectl" diff --git a/src/connectedk8s/azext_connectedk8s/_fileutils.py b/src/connectedk8s/azext_connectedk8s/_fileutils.py new file mode 100644 index 00000000000..a667354a4a6 --- /dev/null +++ b/src/connectedk8s/azext_connectedk8s/_fileutils.py @@ -0,0 +1,42 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + +import os + +from azure.cli.core import azclierror, telemetry +from knack import log + +import azext_connectedk8s._constants as consts + +logger = log.get_logger(__name__) + + +def delete_file(file_path: str, message: str, warning: bool = False) -> None: + # pylint: disable=broad-except + if os.path.isfile(file_path): + try: + os.remove(file_path) + except Exception as e: + telemetry.set_exception( + exception=e, + fault_type=consts.Remove_File_Fault_Type, + summary=f"Unable to delete file at {file_path}", + ) + if warning: + logger.warning(message) + else: + raise azclierror.FileOperationError(message + "Error: " + str(e)) from e + + +def create_directory(file_path: str, error_message: str) -> None: + try: + os.makedirs(file_path) + except Exception as e: + telemetry.set_exception( + exception=e, + fault_type=consts.Create_Directory_Fault_Type, + summary="Unable to create installation directory", + ) + raise azclierror.FileOperationError(error_message + "Error: " + str(e)) from e diff --git a/src/connectedk8s/azext_connectedk8s/_troubleshootutils.py b/src/connectedk8s/azext_connectedk8s/_troubleshootutils.py index ccbe101f772..831f36e0762 100644 --- a/src/connectedk8s/azext_connectedk8s/_troubleshootutils.py +++ b/src/connectedk8s/azext_connectedk8s/_troubleshootutils.py @@ -1769,8 +1769,7 @@ def check_msi_expiry(connected_cluster: ConnectedCluster) -> str: # To handle any exception that may occur during the execution except Exception as e: logger.exception( - "An exception has occured while performing msi expiry check on the " - "cluster." + "An exception has occured while performing msi expiry check on the cluster." ) telemetry.set_exception( exception=e, diff --git a/src/connectedk8s/azext_connectedk8s/clientproxyhelper/_binaryutils.py b/src/connectedk8s/azext_connectedk8s/clientproxyhelper/_binaryutils.py new file mode 100644 index 00000000000..56f7b218b7e --- /dev/null +++ b/src/connectedk8s/azext_connectedk8s/clientproxyhelper/_binaryutils.py @@ -0,0 +1,277 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- +import os +import stat +import tarfile +import time +from glob import glob +from typing import List, Optional + +import oras.client # type: ignore[import-untyped] +from azure.cli.core import azclierror, telemetry +from azure.cli.core.style import Style, print_styled_text +from knack import log + +import azext_connectedk8s._constants as consts +import azext_connectedk8s._fileutils as file_utils + +logger = log.get_logger(__name__) + + +# Downloads client side proxy to connect to Arc Connectivity Platform +def install_client_side_proxy( + arc_proxy_folder: Optional[str], debug: bool = False +) -> str: + client_operating_system = _get_client_operating_system() + client_architecture = _get_client_architeture() + install_dir = _get_proxy_install_dir(arc_proxy_folder) + proxy_name = _get_proxy_filename(client_operating_system, client_architecture) + install_location = os.path.join(install_dir, proxy_name) + + # Only download new proxy if it doesn't exist already + try: + if not os.path.isfile(install_location): + if not os.path.isdir(install_dir): + file_utils.create_directory( + install_dir, + f"Failed to create client proxy directory '{install_dir}'.", + ) + # if directory exists, delete any older versions of the proxy + else: + older_version_location = _get_older_version_proxy_path(install_dir) + older_version_files = glob(older_version_location) + for f in older_version_files: + file_utils.delete_file( + f, f"failed to delete older version file {f}", warning=True + ) + + _download_proxy_from_MCR( + install_dir, proxy_name, client_operating_system, client_architecture + ) + _check_proxy_installation(install_dir, proxy_name, debug) + + except Exception as e: + telemetry.set_exception( + exception=e, + fault_type=consts.Create_CSPExe_Fault_Type, + summary="Unable to create proxy executable", + ) + raise e + + return install_location + + +def _download_proxy_from_MCR( + dest_dir: str, proxy_name: str, operating_system: str, architecture: str +) -> None: + mar_target = f"{consts.CLIENT_PROXY_MCR_TARGET}/{operating_system.lower()}/{architecture}/arc-proxy" + logger.debug( + "Downloading Arc Connectivity Proxy from %s in Microsoft Artifact Regristy.", + mar_target, + ) + + client = oras.client.OrasClient() + t0 = time.time() + + try: + response = client.pull( + target=f"{mar_target}:{consts.CLIENT_PROXY_VERSION}", outdir=dest_dir + ) + except Exception as e: + telemetry.set_exception( + exception=e, + fault_type=consts.Download_Exe_Fault_Type, + summary="Unable to download clientproxy executable.", + ) + raise azclierror.CLIInternalError( + f"Failed to download Arc Connectivity proxy with error {e!s}. Please try again." + ) + + time_elapsed = time.time() - t0 + + proxy_data = { + "Context.Default.AzureCLI.ArcProxyDownloadTime": time_elapsed, + "Context.Default.AzureCLI.ArcProxyVersion": consts.CLIENT_PROXY_VERSION, + } + telemetry.add_extension_event("connectedk8s", proxy_data) + + proxy_package_path = _get_proxy_package_path_from_oras_response(response) + _extract_proxy_tar_files(proxy_package_path, dest_dir, proxy_name) + file_utils.delete_file( + proxy_package_path, + f"Failed to delete {proxy_package_path}. Please delete manually.", + True, + ) + + +def _get_proxy_package_path_from_oras_response(pull_response: List[str]) -> str: + if not isinstance(pull_response, list): + raise azclierror.CLIInternalError( + "Attempt to download Arc Connectivity Proxy returned unnexpected result. Please try again." + ) + + if len(pull_response) != 1: + for r in pull_response: + file_utils.delete_file( + r, f"Failed to delete {r}. Please delete it manually.", True + ) + raise azclierror.CLIInternalError( + "Attempt to download Arc Connectivity Proxy returned unnexpected result. Please try again." + ) + + proxy_package_path = pull_response[0] + + if not os.path.isfile(proxy_package_path): + raise azclierror.CLIInternalError( + "Unable to download Arc Connectivity Proxy. Please try again." + ) + + logger.debug("Proxy package downloaded to %s", proxy_package_path) + + return proxy_package_path + + +def _extract_proxy_tar_files( + proxy_package_path: str, install_dir: str, proxy_name: str +) -> None: + with tarfile.open(proxy_package_path, "r:gz") as tar: + members = [] + for member in tar.getmembers(): + if member.isfile(): + filenames = member.name.split("/") + + if len(filenames) != 2: + tar.close() + file_utils.delete_file( + proxy_package_path, + f"Failed to delete {proxy_package_path}. Please delete it manually.", + True, + ) + raise azclierror.CLIInternalError( + "Attempt to download Arc Connectivity Proxy returned unnexpected result. Please try again." + ) + + member.name = filenames[1] + + if member.name.startswith("arcproxy"): + member.name = proxy_name + elif member.name.lower() not in ["license.txt", "thirdpartynotice.txt"]: + tar.close() + file_utils.delete_file( + proxy_package_path, + f"Failed to delete {proxy_package_path}. Please delete it manually.", + True, + ) + raise azclierror.CLIInternalError( + "Attempt to download Arc Connectivity Proxy returned unnexpected result. Please try again." + ) + + members.append(member) + + tar.extractall(members=members, path=install_dir) + + +def _check_proxy_installation( + install_dir: str, proxy_name: str, debug: bool = False +) -> None: + proxy_filepath = os.path.join(install_dir, proxy_name) + os.chmod(proxy_filepath, os.stat(proxy_filepath).st_mode | stat.S_IXUSR) + if os.path.isfile(proxy_filepath): + if debug: + print_styled_text( + ( + Style.SUCCESS, + f"Successfully installed Arc Connectivity Proxy file {proxy_filepath}", + ) + ) + else: + raise azclierror.CLIInternalError( + "Failed to install required Arc Connectivity Proxy. " + f"Couldn't find expected file {proxy_filepath}. Please try again." + ) + + license_files = ["LICENSE.txt", "ThirdPartyNotice.txt"] + for file in license_files: + file_location = os.path.join(install_dir, file) + if os.path.isfile(file_location): + if debug: + print_styled_text( + ( + Style.SUCCESS, + f"Successfully installed Arc Connectivity Proxy License file {file_location}", + ) + ) + else: + logger.warning( + "Failed to download Arc Connectivity Proxy license file %s. Couldn't find expected file %s. " + "This won't affect your connection.", + file, + file_location, + ) + + +def _get_proxy_filename(operating_system: str, architecture: str) -> str: + if operating_system.lower() == "darwin" and architecture == "386": + raise azclierror.BadRequestError("Unsupported Darwin OS with 386 architecture.") + proxy_filename = f"arcProxy_{operating_system.lower()}_{architecture}_{consts.CLIENT_PROXY_VERSION.replace('.', '_')}" + if operating_system.lower() == "windows": + proxy_filename += ".exe" + return proxy_filename + + +def _get_older_version_proxy_path( + install_dir: str, +) -> str: + proxy_name = "arcProxy*" + return os.path.join(install_dir, proxy_name) + + +def _get_proxy_install_dir(arc_proxy_folder: Optional[str]) -> str: + if not arc_proxy_folder: + return os.path.expanduser(os.path.join("~", consts.CLIENT_PROXY_FOLDER)) + return arc_proxy_folder + + +def _get_client_architeture() -> str: + import platform + + machine = platform.machine() + architecture = None + + logger.debug("Platform architecture: %s", machine) + + if "arm64" in machine.lower() or "aarch64" in machine.lower(): + architecture = "arm64" + elif machine.endswith("64"): + architecture = "amd64" + elif machine.endswith("86"): + architecture = "386" + elif machine == "": + raise azclierror.ClientRequestError( + "Couldn't identify the platform architecture." + ) + else: + raise azclierror.ClientRequestError( + f"Unsuported architecture: {machine} is not currently supported" + ) + + return architecture + + +def _get_client_operating_system() -> str: + import platform + + operating_system = platform.system() + + if operating_system.lower() not in ("linux", "darwin", "windows"): + telemetry.set_exception( + exception="Unsupported OS", + fault_type=consts.Unsupported_Fault_Type, + summary=f"{operating_system} is not supported yet", + ) + raise azclierror.ClientRequestError( + f"The {operating_system} platform is not currently supported." + ) + return operating_system diff --git a/src/connectedk8s/azext_connectedk8s/custom.py b/src/connectedk8s/azext_connectedk8s/custom.py index e195ce04261..ab1c49cc3b5 100644 --- a/src/connectedk8s/azext_connectedk8s/custom.py +++ b/src/connectedk8s/azext_connectedk8s/custom.py @@ -19,7 +19,6 @@ import urllib.request from base64 import b64decode, b64encode from concurrent.futures import ThreadPoolExecutor -from glob import glob from subprocess import DEVNULL, PIPE, Popen from typing import TYPE_CHECKING, Any, Iterable @@ -36,7 +35,6 @@ ManualInterrupt, MutuallyExclusiveArgumentError, RequiredArgumentMissingError, - UnclassifiedUserFault, ValidationError, ) from azure.cli.core.commands import LongRunningOperation @@ -57,6 +55,7 @@ import azext_connectedk8s._precheckutils as precheckutils import azext_connectedk8s._troubleshootutils as troubleshootutils import azext_connectedk8s._utils as utils +import azext_connectedk8s.clientproxyhelper._binaryutils as proxybinaryutils import azext_connectedk8s.clientproxyhelper._proxylogic as proxylogic import azext_connectedk8s.clientproxyhelper._utils as clientproxyutils from azext_connectedk8s._client_factory import ( @@ -177,8 +176,10 @@ def create_connectedk8s( else get_subscription_id(cmd.cli_ctx) ) - resource_id = f"/subscriptions/{subscription_id}/resourcegroups/{resource_group_name}/providers/Microsoft.\ + resource_id = ( + f"/subscriptions/{subscription_id}/resourcegroups/{resource_group_name}/providers/Microsoft.\ Kubernetes/connectedClusters/{cluster_name}/location/{location}" + ) telemetry.add_extension_event( "connectedk8s", {"Context.Default.AzureCLI.resourceid": resource_id} ) @@ -3473,8 +3474,8 @@ def client_side_proxy_wrapper( ) args = [] - operating_system = platform.system() - proc_name = f"arcProxy{operating_system}" + operating_system = proxybinaryutils._get_client_operating_system() + proc_name = f"arcProxy_{operating_system.lower()}" telemetry.set_debug_info("CSP Version is ", consts.CLIENT_PROXY_VERSION) telemetry.set_debug_info("OS is ", operating_system) @@ -3507,103 +3508,14 @@ def client_side_proxy_wrapper( if port_error_string != "": raise ClientRequestError(port_error_string) - # Set csp url based on cloud - CSP_Url = consts.CSP_Storage_Url - if cloud == consts.Azure_ChinaCloudName: - CSP_Url = consts.CSP_Storage_Url_Mooncake - elif cloud == consts.Azure_USGovCloudName: - CSP_Url = consts.CSP_Storage_Url_Fairfax - - # Creating installation location, request uri and older version exe location depending on OS - if operating_system == "Windows": - install_location_string = ( - f".clientproxy\\arcProxy{operating_system}{consts.CLIENT_PROXY_VERSION}.exe" - ) - requestUri = f"{CSP_Url}/{consts.RELEASE_DATE_WINDOWS}/arcProxy{operating_system}{consts.CLIENT_PROXY_VERSION}.exe" - older_version_string = f".clientproxy\\arcProxy{operating_system}*.exe" - creds_string = r".azure\accessTokens.json" - - elif operating_system == "Linux" or operating_system == "Darwin": - install_location_string = ( - f".clientproxy/arcProxy{operating_system}{consts.CLIENT_PROXY_VERSION}" - ) - requestUri = f"{CSP_Url}/{consts.RELEASE_DATE_LINUX}/arcProxy{operating_system}{consts.CLIENT_PROXY_VERSION}" - older_version_string = f".clientproxy/arcProxy{operating_system}*" - creds_string = r".azure/accessTokens.json" - - else: - telemetry.set_exception( - exception="Unsupported OS", - fault_type=consts.Unsupported_Fault_Type, - summary=f"{operating_system} is not supported yet", - ) - raise ClientRequestError( - f"The {operating_system} platform is not currently supported." - ) + debug_mode = False + if "--debug" in cmd.cli_ctx.data["safe_params"]: + debug_mode = True - install_location = os.path.expanduser(os.path.join("~", install_location_string)) + install_location = proxybinaryutils.install_client_side_proxy(None, debug_mode) args.append(install_location) install_dir = os.path.dirname(install_location) - # If version specified by install location doesnt exist, then download the executable - if not os.path.isfile(install_location): - print("Setting up environment for first time use. This can take few minutes...") - # Downloading the executable - try: - response = urllib.request.urlopen(requestUri) - except Exception as e: - telemetry.set_exception( - exception=e, - fault_type=consts.Download_Exe_Fault_Type, - summary="Unable to download clientproxy executable.", - ) - raise CLIInternalError( - f"Failed to download executable with client: {e}", - recommendation="Please check your internet connection.", - ) - - responseContent = response.read() - response.close() - - # Creating the .clientproxy folder if it doesnt exist - if not os.path.exists(install_dir): - try: - os.makedirs(install_dir) - except Exception as e: - telemetry.set_exception( - exception=e, - fault_type=consts.Create_Directory_Fault_Type, - summary="Unable to create installation directory", - ) - raise ClientRequestError( - "Failed to create installation directory." + str(e) - ) - else: - older_version_string = os.path.expanduser( - os.path.join("~", older_version_string) - ) - older_version_files = glob(older_version_string) - - # Removing older executables from the directory - for file_ in older_version_files: - try: - os.remove(file_) - except OSError: - logger.warning("failed to delete older version files") - - try: - with open(install_location, "wb") as f: - f.write(responseContent) - except Exception as e: - telemetry.set_exception( - exception=e, - fault_type=consts.Create_CSPExe_Fault_Type, - summary="Unable to create proxy executable", - ) - raise ClientRequestError("Failed to create proxy executable." + str(e)) - - os.chmod(install_location, os.stat(install_location).st_mode | stat.S_IXUSR) - # Creating config file to pass config to clientproxy config_file_location = os.path.join(install_dir, "config.yml") @@ -3620,7 +3532,6 @@ def client_side_proxy_wrapper( # initializations user_type = "sat" - creds = "" dict_file: dict[str, Any] = { "server": { "httpPort": int(client_proxy_port), @@ -3641,47 +3552,6 @@ def client_side_proxy_wrapper( else: dict_file["identity"]["clientID"] = account["user"]["name"] - if not utils.is_cli_using_msal_auth(): - # Fetching creds - creds_location = os.path.expanduser(os.path.join("~", creds_string)) - try: - with open(creds_location) as f: - creds_list = json.load(f) - except Exception as e: - telemetry.set_exception( - exception=e, - fault_type=consts.Load_Creds_Fault_Type, - summary="Unable to load accessToken.json", - ) - raise FileOperationError("Failed to load credentials." + str(e)) - - user_name = account["user"]["name"] - - if user_type == "user": - key = "userId" - key2 = "refreshToken" - else: - key = "servicePrincipalId" - key2 = "accessToken" - - for i in range(len(creds_list)): - creds_obj = creds_list[i] - - if key in creds_obj and creds_obj[key] == user_name: - creds = creds_obj[key2] - break - - if creds == "": - telemetry.set_exception( - exception="Credentials of user not found.", - fault_type=consts.Creds_NotFound_Fault_Type, - summary="Unable to find creds of user", - ) - raise UnclassifiedUserFault("Credentials of user not found.") - - if user_type != "user": - dict_file["identity"]["clientSecret"] = creds - if cloud == "DOGFOOD": dict_file["cloud"] = "AzureDogFood" elif cloud == consts.Azure_ChinaCloudName: @@ -3725,10 +3595,8 @@ def client_side_proxy_wrapper( args.append("-c") args.append(config_file_location) - debug_mode = False - if "--debug" in cmd.cli_ctx.data["safe_params"]: + if debug_mode: args.append("-d") - debug_mode = True client_side_proxy_main( cmd, diff --git a/src/connectedk8s/azext_connectedk8s/tests/latest/test_connectedk8s_scenario.py b/src/connectedk8s/azext_connectedk8s/tests/latest/test_connectedk8s_scenario.py index 0d6c2377041..aa55f2800ee 100644 --- a/src/connectedk8s/azext_connectedk8s/tests/latest/test_connectedk8s_scenario.py +++ b/src/connectedk8s/azext_connectedk8s/tests/latest/test_connectedk8s_scenario.py @@ -28,6 +28,7 @@ from knack.util import CLIError import azext_connectedk8s._constants as consts +import azext_connectedk8s.clientproxyhelper._binaryutils as proxybinaryutils TEST_DIR = os.path.abspath(os.path.join(os.path.abspath(__file__), "..")) logger = get_logger(__name__) @@ -70,15 +71,19 @@ def install_helm_client(): # Set helm binary download & install locations if operating_system == "windows": - download_location_string = f".azure\\helm\\{consts.HELM_VERSION}\\helm-{consts.HELM_VERSION}-\ + download_location_string = ( + f".azure\\helm\\{consts.HELM_VERSION}\\helm-{consts.HELM_VERSION}-\ {operating_system}-amd64.zip" + ) install_location_string = ( f".azure\\helm\\{consts.HELM_VERSION}\\{operating_system}-amd64\\helm.exe" ) requestUri = f"{consts.HELM_STORAGE_URL}/helm/helm-{consts.HELM_VERSION}-{operating_system}-amd64.zip" elif operating_system == "linux" or operating_system == "darwin": - download_location_string = f".azure/helm/{consts.HELM_VERSION}/helm-{consts.HELM_VERSION}-\ + download_location_string = ( + f".azure/helm/{consts.HELM_VERSION}/helm-{consts.HELM_VERSION}-\ {operating_system}-amd64.tar.gz" + ) install_location_string = ( f".azure/helm/{consts.HELM_VERSION}/{operating_system}-amd64/helm" ) @@ -717,8 +722,8 @@ def test_upgrade(self, resource_group): {managed_cluster_name}-admin" ) response = requests.post( - f'https://{CONFIG["location"]}.dp.kubernetesconfiguration.azure.com/azure-\ - arc-k8sagents/GetLatestHelmPackagePath?api-version=2019-11-01-preview&releaseTrain=stable' + f"https://{CONFIG['location']}.dp.kubernetesconfiguration.azure.com/azure-\ + arc-k8sagents/GetLatestHelmPackagePath?api-version=2019-11-01-preview&releaseTrain=stable" ) jsonData = json.loads(response.text) repo_path = jsonData["repositoryPath"] @@ -1009,14 +1014,11 @@ def test_proxy(self, resource_group): operating_system = platform.system() windows_os = "Windows" proxy_process_name = None - if operating_system == windows_os: - proxy_process_name = ( - f"arcProxy{operating_system}{consts.CLIENT_PROXY_VERSION}.exe" - ) - else: - proxy_process_name = ( - f"arcProxy{operating_system}{consts.CLIENT_PROXY_VERSION}" - ) + client_operating_system = proxybinaryutils._get_client_operating_system() + client_architecture = proxybinaryutils._get_client_architeture() + proxy_process_name = proxybinaryutils._get_proxy_filename( + client_operating_system, client_architecture + ) # There cannot be more than one connectedk8s proxy running, since they would use the same port. script = [ diff --git a/src/connectedk8s/setup.py b/src/connectedk8s/setup.py index e28f498f682..6c86af143d7 100644 --- a/src/connectedk8s/setup.py +++ b/src/connectedk8s/setup.py @@ -34,6 +34,7 @@ "kubernetes==24.2.0", "pycryptodome==3.20.0", "azure-mgmt-hybridcompute==7.0.0", + "oras==0.2.25", ] with open("README.md", "r", encoding="utf-8") as f: From 23ac335ac773c8a7c04e5a112a7b8a60d85a4595 Mon Sep 17 00:00:00 2001 From: Atchut Kumar Barli Date: Tue, 28 Jan 2025 16:01:15 -0800 Subject: [PATCH 13/42] tests --- src/connectedk8s/HISTORY.rst | 3 ++ src/connectedk8s/azext_connectedk8s/_utils.py | 50 ++++++++++++++++++- .../tests/unittests/test_utils_.py | 40 +++++++++++++++ testing/pipeline/k8s-custom-pipelines.yml | 42 +++++++++++++++- 4 files changed, 133 insertions(+), 2 deletions(-) create mode 100644 src/connectedk8s/azext_connectedk8s/tests/unittests/test_utils_.py diff --git a/src/connectedk8s/HISTORY.rst b/src/connectedk8s/HISTORY.rst index 42a48c4dc42..d366e59761b 100644 --- a/src/connectedk8s/HISTORY.rst +++ b/src/connectedk8s/HISTORY.rst @@ -2,6 +2,9 @@ Release History =============== +1.10.5 +++++++ +* Added telemetry for Helm install errors. 1.10.4 ++++++ diff --git a/src/connectedk8s/azext_connectedk8s/_utils.py b/src/connectedk8s/azext_connectedk8s/_utils.py index 18c55883752..cf8c2cada13 100644 --- a/src/connectedk8s/azext_connectedk8s/_utils.py +++ b/src/connectedk8s/azext_connectedk8s/_utils.py @@ -7,6 +7,7 @@ import contextlib import json import os +import re import shutil import subprocess import sys @@ -1311,6 +1312,9 @@ def helm_install_release( "Context.Default.AzureCLI.onboardingErrorType": consts.Install_HelmRelease_Fault_Type, "Context.Default.AzureCLI.onboardingErrorMessage": helm_install_error_message } + # Replace the existing calls with the new function + helm_error_detail = process_helm_error_detail(helm_error_detail) + telemetry.add_extension_event("connectedk8s", helm_error_detail) if any( message in helm_install_error_message @@ -1331,7 +1335,51 @@ def helm_install_release( raise CLIInternalError( f"Unable to install helm release: {helm_install_error_message}" ) - +def process_helm_error_detail(helm_error_detail): + helm_error_detail = remove_rsa_private_key(helm_error_detail) + helm_error_detail = scrub_proxy_url(helm_error_detail) + helm_error_detail = redact_base64_strings(helm_error_detail) + helm_error_detail = redact_sensitive_fields_from_string(helm_error_detail) + + return helm_error_detail + +def remove_rsa_private_key(input_text): + # Regex to identify RSA private key + rsa_key_pattern = re.compile( + r"-----BEGIN RSA PRIVATE KEY-----.*?-----END RSA PRIVATE KEY-----", + re.DOTALL + ) + # Search for the key in the input text + if rsa_key_pattern.search(input_text): + # Remove the RSA private key + return rsa_key_pattern.sub("[RSA PRIVATE KEY REMOVED]", input_text) + else: + return input_text + +def scrub_proxy_url(proxy_url_str): + regex = re.compile(r"://.*?:.*?@") + # Replace matches with "://[REDACTED]:[REDACTED]@" + proxy_url_str = regex.sub("://[REDACTED]:[REDACTED]@", proxy_url_str) + return proxy_url_str + +def redact_base64_strings(content): + base64_pattern = r"\b[A-Za-z0-9+/=]{40,}\b" + return re.sub(base64_pattern, "[REDACTED]", content) + +def redact_sensitive_fields_from_string(input_text): + # Define regex patterns for keys + patterns = { + r"(username:\s*).*": r"\1[REDACTED]", + r"(password:\s*).*": r"\1[REDACTED]", + r"(token:\s*).*": r"\1[REDACTED]" + } + + # Apply regex to redact sensitive fields + for pattern, replacement in patterns.items(): + input_text = re.sub(pattern, replacement, input_text) + + # Return the redacted text + return input_text def get_release_namespace( kube_config: str | None, diff --git a/src/connectedk8s/azext_connectedk8s/tests/unittests/test_utils_.py b/src/connectedk8s/azext_connectedk8s/tests/unittests/test_utils_.py new file mode 100644 index 00000000000..6f19547f0b6 --- /dev/null +++ b/src/connectedk8s/azext_connectedk8s/tests/unittests/test_utils_.py @@ -0,0 +1,40 @@ +import sys +import os +import unittest +from unittest import mock + +sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '../../..'))) +from azext_connectedk8s._utils import * + + +class TestUtils(unittest.TestCase): + + def test_remove_rsa_private_key(self): + input_text = "Error: -----BEGIN RSA PRIVATE KEY-----\nMIIEowIBAAKCAQEA7\n-----END RSA PRIVATE KEY-----" + expected_output = "Error: [RSA PRIVATE KEY REMOVED]" + self.assertEqual(remove_rsa_private_key(input_text), expected_output) + + input_text_no_key = "Error: No RSA key here" + self.assertEqual(remove_rsa_private_key(input_text_no_key), input_text_no_key) + + + def test_scrub_proxy_url_with_url(self): + input_text = "text with proxy URL http://proxy:pass@example.com:8080 in it" + expected_output = "text with proxy URL http://[REDACTED]:[REDACTED]@example.com:8080 in it" + self.assertEqual(scrub_proxy_url(input_text), expected_output) + + def test_scrub_proxy_url_without_url(self): + input_text = "text without proxy URL" + self.assertEqual(scrub_proxy_url(input_text), input_text) + + def test_process_helm_error_detail(self): + input_text = "Some text\n-----BEGIN RSA PRIVATE KEY-----\nkey\n-----END RSA PRIVATE KEY-----\nwith proxy URL http://proxy:pass@example.com:8080 in it" + expected_output = "Some text\n[RSA PRIVATE KEY REMOVED]\nwith proxy URL http://[REDACTED]:[REDACTED]@example.com:8080 in it" + self.assertEqual(process_helm_error_detail(input_text), expected_output) + + def test_process_helm_error_detail_no_changes(self): + input_text = "Some text without RSA key or proxy URL" + self.assertEqual(process_helm_error_detail(input_text), input_text) +if __name__ == '__main__': + unittest.main() + \ No newline at end of file diff --git a/testing/pipeline/k8s-custom-pipelines.yml b/testing/pipeline/k8s-custom-pipelines.yml index 6d647bd87b3..cca2fc63de1 100644 --- a/testing/pipeline/k8s-custom-pipelines.yml +++ b/testing/pipeline/k8s-custom-pipelines.yml @@ -140,6 +140,46 @@ stages: python ./scripts/ci/test_index.py -v displayName: "Verify Extensions Index" + - job: UnitTests + displayName: "Unit Tests" + pool: + vmImage: 'ubuntu-latest' + steps: + - task: UsePythonVersion@0 + displayName: 'Use Python 3.12' + inputs: + versionSpec: '3.12' + - 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 connectedk8s + pwd + pip install pytest + cd tests\unittests + pytest --junitxml=test-results.xml + + displayName: 'Run UnitTests test' + - task: PublishTestResults@2 + inputs: + testResultsFormat: 'JUnit' + testResultsFiles: '**/test-results.xml' + failTaskOnFailedTests: true + condition: succeededOrFaile - job: SourceTests displayName: "Integration Tests, Build Tests" pool: @@ -178,7 +218,7 @@ stages: azdev --version azdev setup -c ../azure-cli -r ./ -e connectedk8s - azdev test connectedk8s + azdev test connectedk8s --live displayName: 'Run integration test and build test' - job: AzdevLinterModifiedExtensions From ad84e7c2378243aaf03e83bf367be5cb090822ed Mon Sep 17 00:00:00 2001 From: Atchut Kumar Barli Date: Tue, 28 Jan 2025 16:19:49 -0800 Subject: [PATCH 14/42] typo --- testing/pipeline/k8s-custom-pipelines.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/testing/pipeline/k8s-custom-pipelines.yml b/testing/pipeline/k8s-custom-pipelines.yml index cca2fc63de1..a6e1ace5080 100644 --- a/testing/pipeline/k8s-custom-pipelines.yml +++ b/testing/pipeline/k8s-custom-pipelines.yml @@ -179,7 +179,7 @@ stages: testResultsFormat: 'JUnit' testResultsFiles: '**/test-results.xml' failTaskOnFailedTests: true - condition: succeededOrFaile + condition: succeededOrFailed - job: SourceTests displayName: "Integration Tests, Build Tests" pool: From eae2569d3b3f5cc0d8ffa91b6eb4909a0ebb22ff Mon Sep 17 00:00:00 2001 From: Atchut Kumar Barli Date: Wed, 29 Jan 2025 10:55:43 -0800 Subject: [PATCH 15/42] indent --- testing/pipeline/k8s-custom-pipelines.yml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/testing/pipeline/k8s-custom-pipelines.yml b/testing/pipeline/k8s-custom-pipelines.yml index a6e1ace5080..3755f57e598 100644 --- a/testing/pipeline/k8s-custom-pipelines.yml +++ b/testing/pipeline/k8s-custom-pipelines.yml @@ -170,7 +170,7 @@ stages: azdev setup -c ../azure-cli -r ./ -e connectedk8s pwd pip install pytest - cd tests\unittests + cd tests/unittests pytest --junitxml=test-results.xml displayName: 'Run UnitTests test' @@ -178,8 +178,7 @@ stages: inputs: testResultsFormat: 'JUnit' testResultsFiles: '**/test-results.xml' - failTaskOnFailedTests: true - condition: succeededOrFailed + failTaskOnFailedTests: true - job: SourceTests displayName: "Integration Tests, Build Tests" pool: From fe23db0e5b2d8dca46797e91954cfec1871f2f4c Mon Sep 17 00:00:00 2001 From: Atchut Kumar Barli Date: Wed, 29 Jan 2025 16:25:58 -0800 Subject: [PATCH 16/42] Update k8s-custom-pipelines.yml --- testing/pipeline/k8s-custom-pipelines.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/testing/pipeline/k8s-custom-pipelines.yml b/testing/pipeline/k8s-custom-pipelines.yml index 3755f57e598..000a26ce0f3 100644 --- a/testing/pipeline/k8s-custom-pipelines.yml +++ b/testing/pipeline/k8s-custom-pipelines.yml @@ -168,9 +168,10 @@ stages: azdev --version azdev setup -c ../azure-cli -r ./ -e connectedk8s - pwd + current_dir=$(pwd) + echo "Current directory: $current_dir" pip install pytest - cd tests/unittests + cd /home/vsts/work/1/s/src/connectedk8s/azext_connectedk8s/tests/unittests pytest --junitxml=test-results.xml displayName: 'Run UnitTests test' From cd9468b94e71691de40a75a7b2423d7682eb57d0 Mon Sep 17 00:00:00 2001 From: Atchut Kumar Barli Date: Wed, 29 Jan 2025 17:19:16 -0800 Subject: [PATCH 17/42] format --- src/connectedk8s/azext_connectedk8s/_utils.py | 30 +++++++++++-------- .../tests/unittests/test_utils_.py | 14 +++++---- 2 files changed, 27 insertions(+), 17 deletions(-) diff --git a/src/connectedk8s/azext_connectedk8s/_utils.py b/src/connectedk8s/azext_connectedk8s/_utils.py index cf8c2cada13..1e1c131391b 100644 --- a/src/connectedk8s/azext_connectedk8s/_utils.py +++ b/src/connectedk8s/azext_connectedk8s/_utils.py @@ -1314,7 +1314,6 @@ def helm_install_release( } # Replace the existing calls with the new function helm_error_detail = process_helm_error_detail(helm_error_detail) - telemetry.add_extension_event("connectedk8s", helm_error_detail) if any( message in helm_install_error_message @@ -1335,36 +1334,42 @@ def helm_install_release( raise CLIInternalError( f"Unable to install helm release: {helm_install_error_message}" ) + + def process_helm_error_detail(helm_error_detail): helm_error_detail = remove_rsa_private_key(helm_error_detail) helm_error_detail = scrub_proxy_url(helm_error_detail) helm_error_detail = redact_base64_strings(helm_error_detail) - helm_error_detail = redact_sensitive_fields_from_string(helm_error_detail) - + helm_error_detail = redact_sensitive_fields_from_string(input_text) + return helm_error_detail + def remove_rsa_private_key(input_text): # Regex to identify RSA private key rsa_key_pattern = re.compile( - r"-----BEGIN RSA PRIVATE KEY-----.*?-----END RSA PRIVATE KEY-----", - re.DOTALL - ) + r"-----BEGIN RSA PRIVATE KEY-----.*?-----END RSA PRIVATE KEY-----", + re.DOTALL + ) # Search for the key in the input text - if rsa_key_pattern.search(input_text): + if rsa_key_pattern.search(input_text): # Remove the RSA private key return rsa_key_pattern.sub("[RSA PRIVATE KEY REMOVED]", input_text) - else: + else: return input_text - -def scrub_proxy_url(proxy_url_str): + + +def scrub_proxy_url(proxy_url_str): regex = re.compile(r"://.*?:.*?@") # Replace matches with "://[REDACTED]:[REDACTED]@" proxy_url_str = regex.sub("://[REDACTED]:[REDACTED]@", proxy_url_str) return proxy_url_str + def redact_base64_strings(content): - base64_pattern = r"\b[A-Za-z0-9+/=]{40,}\b" - return re.sub(base64_pattern, "[REDACTED]", content) + base64_pattern = r"\b[A-Za-z0-9+/=]{40,}\b" + return re.sub(base64_pattern, "[REDACTED]", content) + def redact_sensitive_fields_from_string(input_text): # Define regex patterns for keys @@ -1381,6 +1386,7 @@ def redact_sensitive_fields_from_string(input_text): # Return the redacted text return input_text + def get_release_namespace( kube_config: str | None, kube_context: str | None, diff --git a/src/connectedk8s/azext_connectedk8s/tests/unittests/test_utils_.py b/src/connectedk8s/azext_connectedk8s/tests/unittests/test_utils_.py index 6f19547f0b6..7be2f3a579f 100644 --- a/src/connectedk8s/azext_connectedk8s/tests/unittests/test_utils_.py +++ b/src/connectedk8s/azext_connectedk8s/tests/unittests/test_utils_.py @@ -1,10 +1,14 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- +from azext_connectedk8s._utils import * import sys import os import unittest from unittest import mock sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '../../..'))) -from azext_connectedk8s._utils import * class TestUtils(unittest.TestCase): @@ -16,17 +20,16 @@ def test_remove_rsa_private_key(self): input_text_no_key = "Error: No RSA key here" self.assertEqual(remove_rsa_private_key(input_text_no_key), input_text_no_key) - def test_scrub_proxy_url_with_url(self): input_text = "text with proxy URL http://proxy:pass@example.com:8080 in it" - expected_output = "text with proxy URL http://[REDACTED]:[REDACTED]@example.com:8080 in it" + expected_output = "text with proxy URL http://[REDACTED]:[REDACTED]@example.com:8080 in it" self.assertEqual(scrub_proxy_url(input_text), expected_output) def test_scrub_proxy_url_without_url(self): input_text = "text without proxy URL" self.assertEqual(scrub_proxy_url(input_text), input_text) - + def test_process_helm_error_detail(self): input_text = "Some text\n-----BEGIN RSA PRIVATE KEY-----\nkey\n-----END RSA PRIVATE KEY-----\nwith proxy URL http://proxy:pass@example.com:8080 in it" expected_output = "Some text\n[RSA PRIVATE KEY REMOVED]\nwith proxy URL http://[REDACTED]:[REDACTED]@example.com:8080 in it" @@ -35,6 +38,7 @@ def test_process_helm_error_detail(self): def test_process_helm_error_detail_no_changes(self): input_text = "Some text without RSA key or proxy URL" self.assertEqual(process_helm_error_detail(input_text), input_text) + + if __name__ == '__main__': unittest.main() - \ No newline at end of file From 38d7eaa0b3c03729f5cf7e2122ca58253ecff3d7 Mon Sep 17 00:00:00 2001 From: Atchut Kumar Barli Date: Wed, 29 Jan 2025 17:54:08 -0800 Subject: [PATCH 18/42] tests --- src/connectedk8s/azext_connectedk8s/_utils.py | 2 +- .../tests/unittests/test_utils_.py | 63 +++++++++++-------- 2 files changed, 38 insertions(+), 27 deletions(-) diff --git a/src/connectedk8s/azext_connectedk8s/_utils.py b/src/connectedk8s/azext_connectedk8s/_utils.py index 1e1c131391b..39b69fc6665 100644 --- a/src/connectedk8s/azext_connectedk8s/_utils.py +++ b/src/connectedk8s/azext_connectedk8s/_utils.py @@ -1340,7 +1340,7 @@ def process_helm_error_detail(helm_error_detail): helm_error_detail = remove_rsa_private_key(helm_error_detail) helm_error_detail = scrub_proxy_url(helm_error_detail) helm_error_detail = redact_base64_strings(helm_error_detail) - helm_error_detail = redact_sensitive_fields_from_string(input_text) + helm_error_detail = redact_sensitive_fields_from_string(helm_error_detail) return helm_error_detail diff --git a/src/connectedk8s/azext_connectedk8s/tests/unittests/test_utils_.py b/src/connectedk8s/azext_connectedk8s/tests/unittests/test_utils_.py index 7be2f3a579f..49ee3464e92 100644 --- a/src/connectedk8s/azext_connectedk8s/tests/unittests/test_utils_.py +++ b/src/connectedk8s/azext_connectedk8s/tests/unittests/test_utils_.py @@ -2,43 +2,54 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. See License.txt in the project root for license information. # -------------------------------------------------------------------------------------------- -from azext_connectedk8s._utils import * import sys import os -import unittest -from unittest import mock sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '../../..'))) +import pytest +from azext_connectedk8s._utils import ( + remove_rsa_private_key, + scrub_proxy_url, + process_helm_error_detail +) -class TestUtils(unittest.TestCase): +def test_remove_rsa_private_key(): + input_text = "Error: -----BEGIN RSA PRIVATE KEY-----\nMIIEowIBAAKCAQEA7\n-----END RSA PRIVATE KEY-----" + expected_output = "Error: [RSA PRIVATE KEY REMOVED]" + assert remove_rsa_private_key(input_text) == expected_output - def test_remove_rsa_private_key(self): - input_text = "Error: -----BEGIN RSA PRIVATE KEY-----\nMIIEowIBAAKCAQEA7\n-----END RSA PRIVATE KEY-----" - expected_output = "Error: [RSA PRIVATE KEY REMOVED]" - self.assertEqual(remove_rsa_private_key(input_text), expected_output) + input_text_no_key = "Error: No RSA key here" + assert remove_rsa_private_key(input_text_no_key) == input_text_no_key - input_text_no_key = "Error: No RSA key here" - self.assertEqual(remove_rsa_private_key(input_text_no_key), input_text_no_key) - def test_scrub_proxy_url_with_url(self): - input_text = "text with proxy URL http://proxy:pass@example.com:8080 in it" - expected_output = "text with proxy URL http://[REDACTED]:[REDACTED]@example.com:8080 in it" - self.assertEqual(scrub_proxy_url(input_text), expected_output) +def test_scrub_proxy_url_with_url(): + input_text = "text with proxy URL http://proxy:pass@example.com:8080 in it" + expected_output = "text with proxy URL http://[REDACTED]:[REDACTED]@example.com:8080 in it" + assert scrub_proxy_url(input_text) == expected_output - def test_scrub_proxy_url_without_url(self): - input_text = "text without proxy URL" - self.assertEqual(scrub_proxy_url(input_text), input_text) - def test_process_helm_error_detail(self): - input_text = "Some text\n-----BEGIN RSA PRIVATE KEY-----\nkey\n-----END RSA PRIVATE KEY-----\nwith proxy URL http://proxy:pass@example.com:8080 in it" - expected_output = "Some text\n[RSA PRIVATE KEY REMOVED]\nwith proxy URL http://[REDACTED]:[REDACTED]@example.com:8080 in it" - self.assertEqual(process_helm_error_detail(input_text), expected_output) +def test_scrub_proxy_url_without_url(): + input_text = "text without proxy URL" + assert scrub_proxy_url(input_text) == input_text - def test_process_helm_error_detail_no_changes(self): - input_text = "Some text without RSA key or proxy URL" - self.assertEqual(process_helm_error_detail(input_text), input_text) +def test_process_helm_error_detail(): + input_text = ( + "Some text\n-----BEGIN RSA PRIVATE KEY-----\nkey\n-----END RSA PRIVATE KEY-----\n" + "with proxy URL http://proxy:pass@example.com:8080 in it" + ) + expected_output = ( + "Some text\n[RSA PRIVATE KEY REMOVED]\n" + "with proxy URL http://[REDACTED]:[REDACTED]@example.com:8080 in it" + ) + assert process_helm_error_detail(input_text) == expected_output -if __name__ == '__main__': - unittest.main() + +def test_process_helm_error_detail_no_changes(): + input_text = "Some text without RSA key or proxy URL" + assert process_helm_error_detail(input_text) == input_text + + +if __name__ == "__main__": + pytest.main() From e9efcadd7f08a7e89adc290b8ef7ec2fa53b5496 Mon Sep 17 00:00:00 2001 From: Atchut Kumar Barli Date: Thu, 30 Jan 2025 10:35:45 -0800 Subject: [PATCH 19/42] formatting --- src/connectedk8s/azext_connectedk8s/_utils.py | 13 +++++++------ .../tests/unittests/test_utils_.py | 14 ++++++++------ testing/pipeline/k8s-custom-pipelines.yml | 2 +- 3 files changed, 16 insertions(+), 13 deletions(-) diff --git a/src/connectedk8s/azext_connectedk8s/_utils.py b/src/connectedk8s/azext_connectedk8s/_utils.py index 39b69fc6665..069709d828a 100644 --- a/src/connectedk8s/azext_connectedk8s/_utils.py +++ b/src/connectedk8s/azext_connectedk8s/_utils.py @@ -1308,12 +1308,13 @@ def helm_install_release( _, error_helm_install = response_helm_install.communicate() if response_helm_install.returncode != 0: helm_install_error_message = error_helm_install.decode("ascii") + helm_install_error_message = process_helm_error_detail(helm_install_error_message) helm_error_detail = { "Context.Default.AzureCLI.onboardingErrorType": consts.Install_HelmRelease_Fault_Type, "Context.Default.AzureCLI.onboardingErrorMessage": helm_install_error_message } # Replace the existing calls with the new function - helm_error_detail = process_helm_error_detail(helm_error_detail) + telemetry.add_extension_event("connectedk8s", helm_error_detail) if any( message in helm_install_error_message @@ -1336,7 +1337,7 @@ def helm_install_release( ) -def process_helm_error_detail(helm_error_detail): +def process_helm_error_detail(helm_error_detail: str) -> str: helm_error_detail = remove_rsa_private_key(helm_error_detail) helm_error_detail = scrub_proxy_url(helm_error_detail) helm_error_detail = redact_base64_strings(helm_error_detail) @@ -1345,7 +1346,7 @@ def process_helm_error_detail(helm_error_detail): return helm_error_detail -def remove_rsa_private_key(input_text): +def remove_rsa_private_key(input_text: str) -> str: # Regex to identify RSA private key rsa_key_pattern = re.compile( r"-----BEGIN RSA PRIVATE KEY-----.*?-----END RSA PRIVATE KEY-----", @@ -1359,19 +1360,19 @@ def remove_rsa_private_key(input_text): return input_text -def scrub_proxy_url(proxy_url_str): +def scrub_proxy_url(proxy_url_str: str) -> str: regex = re.compile(r"://.*?:.*?@") # Replace matches with "://[REDACTED]:[REDACTED]@" proxy_url_str = regex.sub("://[REDACTED]:[REDACTED]@", proxy_url_str) return proxy_url_str -def redact_base64_strings(content): +def redact_base64_strings(content: str) -> str: base64_pattern = r"\b[A-Za-z0-9+/=]{40,}\b" return re.sub(base64_pattern, "[REDACTED]", content) -def redact_sensitive_fields_from_string(input_text): +def redact_sensitive_fields_from_string(input_text: str) -> str: # Define regex patterns for keys patterns = { r"(username:\s*).*": r"\1[REDACTED]", diff --git a/src/connectedk8s/azext_connectedk8s/tests/unittests/test_utils_.py b/src/connectedk8s/azext_connectedk8s/tests/unittests/test_utils_.py index 49ee3464e92..c7bbbadbd89 100644 --- a/src/connectedk8s/azext_connectedk8s/tests/unittests/test_utils_.py +++ b/src/connectedk8s/azext_connectedk8s/tests/unittests/test_utils_.py @@ -2,18 +2,20 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. See License.txt in the project root for license information. # -------------------------------------------------------------------------------------------- -import sys import os - -sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '../../..'))) +import sys import pytest + from azext_connectedk8s._utils import ( - remove_rsa_private_key, - scrub_proxy_url, - process_helm_error_detail + process_helm_error_detail, + remove_rsa_private_key, + scrub_proxy_url, ) +sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '../../..'))) + + def test_remove_rsa_private_key(): input_text = "Error: -----BEGIN RSA PRIVATE KEY-----\nMIIEowIBAAKCAQEA7\n-----END RSA PRIVATE KEY-----" expected_output = "Error: [RSA PRIVATE KEY REMOVED]" diff --git a/testing/pipeline/k8s-custom-pipelines.yml b/testing/pipeline/k8s-custom-pipelines.yml index 000a26ce0f3..712d6f67762 100644 --- a/testing/pipeline/k8s-custom-pipelines.yml +++ b/testing/pipeline/k8s-custom-pipelines.yml @@ -218,7 +218,7 @@ stages: azdev --version azdev setup -c ../azure-cli -r ./ -e connectedk8s - azdev test connectedk8s --live + azdev test connectedk8s displayName: 'Run integration test and build test' - job: AzdevLinterModifiedExtensions From a1e4bff31420865eea4248179c7108f776f91a14 Mon Sep 17 00:00:00 2001 From: Atchut Kumar Barli Date: Thu, 30 Jan 2025 11:20:57 -0800 Subject: [PATCH 20/42] format --- .../azext_connectedk8s/_troubleshootutils.py | 3 +-- src/connectedk8s/azext_connectedk8s/_utils.py | 14 +++++----- src/connectedk8s/azext_connectedk8s/custom.py | 4 ++- .../latest/test_connectedk8s_scenario.py | 26 +++++++++++++------ .../tests/unittests/test_utils_.py | 7 ++--- 5 files changed, 33 insertions(+), 21 deletions(-) diff --git a/src/connectedk8s/azext_connectedk8s/_troubleshootutils.py b/src/connectedk8s/azext_connectedk8s/_troubleshootutils.py index ccbe101f772..831f36e0762 100644 --- a/src/connectedk8s/azext_connectedk8s/_troubleshootutils.py +++ b/src/connectedk8s/azext_connectedk8s/_troubleshootutils.py @@ -1769,8 +1769,7 @@ def check_msi_expiry(connected_cluster: ConnectedCluster) -> str: # To handle any exception that may occur during the execution except Exception as e: logger.exception( - "An exception has occured while performing msi expiry check on the " - "cluster." + "An exception has occured while performing msi expiry check on the cluster." ) telemetry.set_exception( exception=e, diff --git a/src/connectedk8s/azext_connectedk8s/_utils.py b/src/connectedk8s/azext_connectedk8s/_utils.py index 069709d828a..9ee35ba53f1 100644 --- a/src/connectedk8s/azext_connectedk8s/_utils.py +++ b/src/connectedk8s/azext_connectedk8s/_utils.py @@ -1308,10 +1308,12 @@ def helm_install_release( _, error_helm_install = response_helm_install.communicate() if response_helm_install.returncode != 0: helm_install_error_message = error_helm_install.decode("ascii") - helm_install_error_message = process_helm_error_detail(helm_install_error_message) + helm_install_error_message = process_helm_error_detail( + helm_install_error_message + ) helm_error_detail = { "Context.Default.AzureCLI.onboardingErrorType": consts.Install_HelmRelease_Fault_Type, - "Context.Default.AzureCLI.onboardingErrorMessage": helm_install_error_message + "Context.Default.AzureCLI.onboardingErrorMessage": helm_install_error_message, } # Replace the existing calls with the new function @@ -1349,15 +1351,13 @@ def process_helm_error_detail(helm_error_detail: str) -> str: def remove_rsa_private_key(input_text: str) -> str: # Regex to identify RSA private key rsa_key_pattern = re.compile( - r"-----BEGIN RSA PRIVATE KEY-----.*?-----END RSA PRIVATE KEY-----", - re.DOTALL + r"-----BEGIN RSA PRIVATE KEY-----.*?-----END RSA PRIVATE KEY-----", re.DOTALL ) # Search for the key in the input text if rsa_key_pattern.search(input_text): # Remove the RSA private key return rsa_key_pattern.sub("[RSA PRIVATE KEY REMOVED]", input_text) - else: - return input_text + return input_text def scrub_proxy_url(proxy_url_str: str) -> str: @@ -1377,7 +1377,7 @@ def redact_sensitive_fields_from_string(input_text: str) -> str: patterns = { r"(username:\s*).*": r"\1[REDACTED]", r"(password:\s*).*": r"\1[REDACTED]", - r"(token:\s*).*": r"\1[REDACTED]" + r"(token:\s*).*": r"\1[REDACTED]", } # Apply regex to redact sensitive fields diff --git a/src/connectedk8s/azext_connectedk8s/custom.py b/src/connectedk8s/azext_connectedk8s/custom.py index 1ad75b2546f..785e0dfc85b 100644 --- a/src/connectedk8s/azext_connectedk8s/custom.py +++ b/src/connectedk8s/azext_connectedk8s/custom.py @@ -178,8 +178,10 @@ def create_connectedk8s( else get_subscription_id(cmd.cli_ctx) ) - resource_id = f"/subscriptions/{subscription_id}/resourcegroups/{resource_group_name}/providers/Microsoft.\ + resource_id = ( + f"/subscriptions/{subscription_id}/resourcegroups/{resource_group_name}/providers/Microsoft.\ Kubernetes/connectedClusters/{cluster_name}/location/{location}" + ) telemetry.add_extension_event( "connectedk8s", {"Context.Default.AzureCLI.resourceid": resource_id} ) diff --git a/src/connectedk8s/azext_connectedk8s/tests/latest/test_connectedk8s_scenario.py b/src/connectedk8s/azext_connectedk8s/tests/latest/test_connectedk8s_scenario.py index cca97a6ef15..3eacc03ecf9 100644 --- a/src/connectedk8s/azext_connectedk8s/tests/latest/test_connectedk8s_scenario.py +++ b/src/connectedk8s/azext_connectedk8s/tests/latest/test_connectedk8s_scenario.py @@ -43,8 +43,10 @@ CONFIG = json.load(f) for key in CONFIG: if not CONFIG[key]: - raise RequiredArgumentMissingError(f"Missing required configuration in {config_path} file. Make sure all \ -properties are populated.") + raise RequiredArgumentMissingError( + f"Missing required configuration in {config_path} file. Make sure all \ +properties are populated." + ) def _get_test_data_file(filename): @@ -68,15 +70,19 @@ def install_helm_client(): # Set helm binary download & install locations if operating_system == "windows": - download_location_string = f".azure\\helm\\{consts.HELM_VERSION}\\helm-{consts.HELM_VERSION}-\ + download_location_string = ( + f".azure\\helm\\{consts.HELM_VERSION}\\helm-{consts.HELM_VERSION}-\ {operating_system}-amd64.zip" + ) install_location_string = ( f".azure\\helm\\{consts.HELM_VERSION}\\{operating_system}-amd64\\helm.exe" ) requestUri = f"{consts.HELM_STORAGE_URL}/helm/helm-{consts.HELM_VERSION}-{operating_system}-amd64.zip" elif operating_system == "linux" or operating_system == "darwin": - download_location_string = f".azure/helm/{consts.HELM_VERSION}/helm-{consts.HELM_VERSION}-\ + download_location_string = ( + f".azure/helm/{consts.HELM_VERSION}/helm-{consts.HELM_VERSION}-\ {operating_system}-amd64.tar.gz" + ) install_location_string = ( f".azure/helm/{consts.HELM_VERSION}/{operating_system}-amd64/helm" ) @@ -148,8 +154,10 @@ def install_kubectl_client(): elif operating_system == "linux" or operating_system == "darwin": kubectl_path = os.path.join(kubectl_filepath, "kubectl") else: - logger.warning(f"The {operating_system} platform is not currently supported for installing kubectl \ -client.") + logger.warning( + f"The {operating_system} platform is not currently supported for installing kubectl \ +client." + ) return if os.path.isfile(kubectl_path): @@ -712,8 +720,10 @@ def test_upgrade(self, resource_group): "connectedk8s upgrade -g {rg} -n {name} --kube-config {kubeconfig} --kube-context \ {managed_cluster_name}-admin" ) - response = requests.post(f'https://{CONFIG["location"]}.dp.kubernetesconfiguration.azure.com/azure-\ - arc-k8sagents/GetLatestHelmPackagePath?api-version=2019-11-01-preview&releaseTrain=stable') + response = requests.post( + f"https://{CONFIG['location']}.dp.kubernetesconfiguration.azure.com/azure-\ + arc-k8sagents/GetLatestHelmPackagePath?api-version=2019-11-01-preview&releaseTrain=stable" + ) jsonData = json.loads(response.text) repo_path = jsonData["repositoryPath"] index_value = 0 diff --git a/src/connectedk8s/azext_connectedk8s/tests/unittests/test_utils_.py b/src/connectedk8s/azext_connectedk8s/tests/unittests/test_utils_.py index c7bbbadbd89..2c355ad263a 100644 --- a/src/connectedk8s/azext_connectedk8s/tests/unittests/test_utils_.py +++ b/src/connectedk8s/azext_connectedk8s/tests/unittests/test_utils_.py @@ -7,14 +7,13 @@ import pytest +sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "../../.."))) from azext_connectedk8s._utils import ( process_helm_error_detail, remove_rsa_private_key, scrub_proxy_url, ) -sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '../../..'))) - def test_remove_rsa_private_key(): input_text = "Error: -----BEGIN RSA PRIVATE KEY-----\nMIIEowIBAAKCAQEA7\n-----END RSA PRIVATE KEY-----" @@ -27,7 +26,9 @@ def test_remove_rsa_private_key(): def test_scrub_proxy_url_with_url(): input_text = "text with proxy URL http://proxy:pass@example.com:8080 in it" - expected_output = "text with proxy URL http://[REDACTED]:[REDACTED]@example.com:8080 in it" + expected_output = ( + "text with proxy URL http://[REDACTED]:[REDACTED]@example.com:8080 in it" + ) assert scrub_proxy_url(input_text) == expected_output From e6b5d0cf2645d2f93434c2635b48f8b174a51061 Mon Sep 17 00:00:00 2001 From: Atchut Kumar Barli Date: Thu, 30 Jan 2025 13:12:14 -0800 Subject: [PATCH 21/42] add test --- src/connectedk8s/HISTORY.rst | 3 --- .../tests/unittests/test_utils_.py | 12 ++++++++++++ 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/src/connectedk8s/HISTORY.rst b/src/connectedk8s/HISTORY.rst index d366e59761b..42a48c4dc42 100644 --- a/src/connectedk8s/HISTORY.rst +++ b/src/connectedk8s/HISTORY.rst @@ -2,9 +2,6 @@ Release History =============== -1.10.5 -++++++ -* Added telemetry for Helm install errors. 1.10.4 ++++++ diff --git a/src/connectedk8s/azext_connectedk8s/tests/unittests/test_utils_.py b/src/connectedk8s/azext_connectedk8s/tests/unittests/test_utils_.py index 2c355ad263a..f81d38f3c5e 100644 --- a/src/connectedk8s/azext_connectedk8s/tests/unittests/test_utils_.py +++ b/src/connectedk8s/azext_connectedk8s/tests/unittests/test_utils_.py @@ -10,6 +10,7 @@ sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "../../.."))) from azext_connectedk8s._utils import ( process_helm_error_detail, + redact_sensitive_fields_from_string, remove_rsa_private_key, scrub_proxy_url, ) @@ -53,6 +54,17 @@ def test_process_helm_error_detail_no_changes(): input_text = "Some text without RSA key or proxy URL" assert process_helm_error_detail(input_text) == input_text +def test_redact_sensitive_fields_from_string(): + input_text = "username: admin\npassword: secret\ntoken: abc123" + expected_output = "username: [REDACTED]\npassword: [REDACTED]\ntoken: [REDACTED]" + assert redact_sensitive_fields_from_string(input_text) == expected_output + + input_text_no_sensitive = "No sensitive data here" + assert redact_sensitive_fields_from_string(input_text_no_sensitive) == input_text_no_sensitive + + input_text_partial = "username: user1\nhello_data: safe\npassword: mypass" + expected_output_partial = "username: [REDACTED]\nhello_data: safe\npassword: [REDACTED]" + assert redact_sensitive_fields_from_string(input_text_partial) == expected_output_partial if __name__ == "__main__": pytest.main() From 61d84b3e4124f9cf837386773b3f70bcdac84455 Mon Sep 17 00:00:00 2001 From: Atchut Kumar Barli Date: Thu, 30 Jan 2025 15:14:49 -0800 Subject: [PATCH 22/42] initial --- src/connectedk8s/azext_connectedk8s/custom.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/connectedk8s/azext_connectedk8s/custom.py b/src/connectedk8s/azext_connectedk8s/custom.py index ab1c49cc3b5..7b446825147 100644 --- a/src/connectedk8s/azext_connectedk8s/custom.py +++ b/src/connectedk8s/azext_connectedk8s/custom.py @@ -3822,8 +3822,7 @@ def check_cl_registration_and_get_oid( cmd: CLICommmand, cl_oid: str | None, subscription_id: str | None ) -> tuple[bool, str]: print( - f"Step: {utils.get_utctimestring()}: Checking Microsoft.ExtendedLocation RP Registration state for this Subscription, and get OID, " - "if registered " + f"Step: {utils.get_utctimestring()}: Checking Custom Location(Microsoft.ExtendedLocation) RP Registration state for this Subscription, and attempt to get the Custom Location Object ID (OID),if registered" ) enable_custom_locations = True custom_locations_oid = "" @@ -3900,7 +3899,8 @@ def get_custom_locations_oid(cmd: CLICommmand, cl_oid: str | None) -> str: return cl_oid except Exception as e: # Encountered exeption while fetching OID, log error - log_string = "Unable to fetch the Object ID of the Azure AD application used by Azure Arc service. " + log_string = "Unable to fetch the Custom Location OID with permissions set on this account. The account does not have sufficient permissions to fetch or validate the OID." + log_string += "If the OID is invalid, custom location may not be properly enabled." telemetry.set_exception( exception=e, fault_type=consts.Custom_Locations_OID_Fetch_Fault_Type_Exception, @@ -3908,7 +3908,7 @@ def get_custom_locations_oid(cmd: CLICommmand, cl_oid: str | None) -> str: ) # If Cl OID was input, use that if cl_oid: - log_string += "Proceeding with the Object ID provided to enable the 'custom-locations' feature." + log_string += "Proceeding with using the OID manually provided to enable the 'custom-locations' feature without validation." logger.warning(log_string) return cl_oid # If no Cl OID was input, log a Warning and return empty for OID From 62287e9ab47290bb25e12c00ae5bab0971f9304f Mon Sep 17 00:00:00 2001 From: Atchut Kumar Barli Date: Thu, 30 Jan 2025 17:25:34 -0800 Subject: [PATCH 23/42] final message --- src/connectedk8s/azext_connectedk8s/_constants.py | 4 ++++ src/connectedk8s/azext_connectedk8s/custom.py | 10 ++++++---- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/src/connectedk8s/azext_connectedk8s/_constants.py b/src/connectedk8s/azext_connectedk8s/_constants.py index b8eaa6f048c..84b1e1ef1c0 100644 --- a/src/connectedk8s/azext_connectedk8s/_constants.py +++ b/src/connectedk8s/azext_connectedk8s/_constants.py @@ -454,6 +454,10 @@ "Failed to save cluster diagnostic checks job log" ) +Manual_Custom_Location_Oid_Warning="""Important! Custom Location feature enablement can’t be validated when using a manually provided OID. If the custom location feature is not enabled, you may encounter an error when creating the custom location. +After creating the custom location, run `az customlocation show` and check that ProvisioningState is Succeeded. If ProvisoningState is Failed, then re-try this command with a valid custom location OID to enable the feature. +For guidance, refer to: https://aka.ms/enable-customlocation""" + # Diagnostic Results Name Outbound_Connectivity_Check_Result_String = "Outbound Network Connectivity" Outbound_Connectivity_Check_Failed_For_Cluster_Connect = ( diff --git a/src/connectedk8s/azext_connectedk8s/custom.py b/src/connectedk8s/azext_connectedk8s/custom.py index 7b446825147..6939a4c0295 100644 --- a/src/connectedk8s/azext_connectedk8s/custom.py +++ b/src/connectedk8s/azext_connectedk8s/custom.py @@ -968,7 +968,8 @@ def create_connectedk8s( raise CLIInternalError( "Timed out waiting for Agent State to reach terminal state." ) - + if(cl_oid and enable_custom_locations and cl_oid!=custom_locations_oid): + print (consts.Manual_Upgrade_Called_In_Auto_Update_Enabled) return put_cc_response @@ -3846,8 +3847,8 @@ def check_cl_registration_and_get_oid( except Exception as e: enable_custom_locations = False warn_msg = ( - "Unable to fetch registration state of 'Microsoft.ExtendedLocation'. Failed to enable " - "'custom-locations' feature. This is fine if not required. Proceeding with helm install." + "The custom location feature was not enabled because the custom location OID could not be retrieved. Please refer to: https://aka.ms/enable-customlocation " + "Proceeding with helm install..." ) logger.warning(warn_msg) telemetry.set_exception( @@ -3900,7 +3901,7 @@ def get_custom_locations_oid(cmd: CLICommmand, cl_oid: str | None) -> str: except Exception as e: # Encountered exeption while fetching OID, log error log_string = "Unable to fetch the Custom Location OID with permissions set on this account. The account does not have sufficient permissions to fetch or validate the OID." - log_string += "If the OID is invalid, custom location may not be properly enabled." + telemetry.set_exception( exception=e, fault_type=consts.Custom_Locations_OID_Fetch_Fault_Type_Exception, @@ -3908,6 +3909,7 @@ def get_custom_locations_oid(cmd: CLICommmand, cl_oid: str | None) -> str: ) # If Cl OID was input, use that if cl_oid: + log_string += "If the manual OID is invalid, custom location may not be properly enabled." log_string += "Proceeding with using the OID manually provided to enable the 'custom-locations' feature without validation." logger.warning(log_string) return cl_oid From 358d2de1aff71c88e8e3913abf24d4cc6b0d7707 Mon Sep 17 00:00:00 2001 From: Atchut Kumar Barli Date: Fri, 31 Jan 2025 12:25:31 -0800 Subject: [PATCH 24/42] format --- .../tests/unittests/test_utils_.py | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/src/connectedk8s/azext_connectedk8s/tests/unittests/test_utils_.py b/src/connectedk8s/azext_connectedk8s/tests/unittests/test_utils_.py index f81d38f3c5e..768290b902a 100644 --- a/src/connectedk8s/azext_connectedk8s/tests/unittests/test_utils_.py +++ b/src/connectedk8s/azext_connectedk8s/tests/unittests/test_utils_.py @@ -54,17 +54,27 @@ def test_process_helm_error_detail_no_changes(): input_text = "Some text without RSA key or proxy URL" assert process_helm_error_detail(input_text) == input_text + def test_redact_sensitive_fields_from_string(): input_text = "username: admin\npassword: secret\ntoken: abc123" expected_output = "username: [REDACTED]\npassword: [REDACTED]\ntoken: [REDACTED]" assert redact_sensitive_fields_from_string(input_text) == expected_output input_text_no_sensitive = "No sensitive data here" - assert redact_sensitive_fields_from_string(input_text_no_sensitive) == input_text_no_sensitive + assert ( + redact_sensitive_fields_from_string(input_text_no_sensitive) + == input_text_no_sensitive + ) input_text_partial = "username: user1\nhello_data: safe\npassword: mypass" - expected_output_partial = "username: [REDACTED]\nhello_data: safe\npassword: [REDACTED]" - assert redact_sensitive_fields_from_string(input_text_partial) == expected_output_partial + expected_output_partial = ( + "username: [REDACTED]\nhello_data: safe\npassword: [REDACTED]" + ) + assert ( + redact_sensitive_fields_from_string(input_text_partial) + == expected_output_partial + ) + if __name__ == "__main__": pytest.main() From 2d97b66b8750ab1af08fba6f1e2a7e4d76ef4b7b Mon Sep 17 00:00:00 2001 From: Atchut Kumar Barli Date: Fri, 31 Jan 2025 15:12:56 -0800 Subject: [PATCH 25/42] multiline --- src/connectedk8s/azext_connectedk8s/custom.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/connectedk8s/azext_connectedk8s/custom.py b/src/connectedk8s/azext_connectedk8s/custom.py index 6939a4c0295..72a58b62018 100644 --- a/src/connectedk8s/azext_connectedk8s/custom.py +++ b/src/connectedk8s/azext_connectedk8s/custom.py @@ -968,8 +968,8 @@ def create_connectedk8s( raise CLIInternalError( "Timed out waiting for Agent State to reach terminal state." ) - if(cl_oid and enable_custom_locations and cl_oid!=custom_locations_oid): - print (consts.Manual_Upgrade_Called_In_Auto_Update_Enabled) + if cl_oid and enable_custom_locations and cl_oid != custom_locations_oid: + print(consts.Manual_Upgrade_Called_In_Auto_Update_Enabled) return put_cc_response @@ -3901,7 +3901,7 @@ def get_custom_locations_oid(cmd: CLICommmand, cl_oid: str | None) -> str: except Exception as e: # Encountered exeption while fetching OID, log error log_string = "Unable to fetch the Custom Location OID with permissions set on this account. The account does not have sufficient permissions to fetch or validate the OID." - + telemetry.set_exception( exception=e, fault_type=consts.Custom_Locations_OID_Fetch_Fault_Type_Exception, From d2a3f308e04b5c959ef7b79143fce54f10408e88 Mon Sep 17 00:00:00 2001 From: Bavneet Singh <33008256+bavneetsingh16@users.noreply.github.com> Date: Wed, 5 Feb 2025 13:29:25 -0800 Subject: [PATCH 26/42] update connectedk8s cli version and release notes (#31) --- src/connectedk8s/HISTORY.rst | 7 +++++++ src/connectedk8s/setup.py | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/connectedk8s/HISTORY.rst b/src/connectedk8s/HISTORY.rst index 42a48c4dc42..165af28b9bd 100644 --- a/src/connectedk8s/HISTORY.rst +++ b/src/connectedk8s/HISTORY.rst @@ -3,6 +3,13 @@ Release History =============== +1.10.5 +++++++ +* Fixed bug impacting long-running operations of the az connectedk8s proxy command. +* Refactored code to reduce proxy command startup time. +* Added support for downloading proxy binaries from MCR, including more architecture-specific versions. +* Enhanced telemetry to capture detailed error information during Helm installation failures. + 1.10.4 ++++++ * Fixed the issue where the 'connectedk8s proxy' command would fail if the kubeconfig file was empty. diff --git a/src/connectedk8s/setup.py b/src/connectedk8s/setup.py index 6c86af143d7..ca86f052e70 100644 --- a/src/connectedk8s/setup.py +++ b/src/connectedk8s/setup.py @@ -13,7 +13,7 @@ # TODO: Confirm this is the right version number you want and it matches your # HISTORY.rst entry. -VERSION = "1.10.4" +VERSION = "1.10.5" # The full list of classifiers is available at # https://pypi.python.org/pypi?%3Aaction=list_classifiers From c922645dceac5198654120de43fd886e6807bf32 Mon Sep 17 00:00:00 2001 From: Atchut Kumar Barli Date: Fri, 7 Feb 2025 14:23:00 -0800 Subject: [PATCH 27/42] features --- .../azext_connectedk8s/_constants.py | 5 +++- src/connectedk8s/azext_connectedk8s/custom.py | 24 ++++++++++--------- 2 files changed, 17 insertions(+), 12 deletions(-) diff --git a/src/connectedk8s/azext_connectedk8s/_constants.py b/src/connectedk8s/azext_connectedk8s/_constants.py index 84b1e1ef1c0..ba66da0bb0d 100644 --- a/src/connectedk8s/azext_connectedk8s/_constants.py +++ b/src/connectedk8s/azext_connectedk8s/_constants.py @@ -454,10 +454,13 @@ "Failed to save cluster diagnostic checks job log" ) -Manual_Custom_Location_Oid_Warning="""Important! Custom Location feature enablement can’t be validated when using a manually provided OID. If the custom location feature is not enabled, you may encounter an error when creating the custom location. +Manual_Custom_Location_Oid_Warning="""Important! Custom Location feature enablement can’t be validated when using a manually provided OID. If the custom location feature is not enabled, you may encounter an error when creating the custom location. After creating the custom location, run `az customlocation show` and check that ProvisioningState is Succeeded. If ProvisoningState is Failed, then re-try this command with a valid custom location OID to enable the feature. For guidance, refer to: https://aka.ms/enable-customlocation""" +Custom_Location_Enable_Failed_warning="""Important! Custom Location feature wasn’t enabled due to insufficient privileges on the Service Principal Name. If the custom location feature is not enabled, you will encounter an error when creating the custom location. Refer to: https://aka.ms/enable-cl-spn""" + + # Diagnostic Results Name Outbound_Connectivity_Check_Result_String = "Outbound Network Connectivity" Outbound_Connectivity_Check_Failed_For_Cluster_Connect = ( diff --git a/src/connectedk8s/azext_connectedk8s/custom.py b/src/connectedk8s/azext_connectedk8s/custom.py index 72a58b62018..477e510057c 100644 --- a/src/connectedk8s/azext_connectedk8s/custom.py +++ b/src/connectedk8s/azext_connectedk8s/custom.py @@ -968,8 +968,8 @@ def create_connectedk8s( raise CLIInternalError( "Timed out waiting for Agent State to reach terminal state." ) - if cl_oid and enable_custom_locations and cl_oid != custom_locations_oid: - print(consts.Manual_Upgrade_Called_In_Auto_Update_Enabled) + if cl_oid and enable_custom_locations and cl_oid == custom_locations_oid: + logger.warn(consts.Manual_Custom_Location_Oid_Warning) return put_cc_response @@ -2791,7 +2791,7 @@ def enable_features( cl_oid: str | None = None, ) -> str: logger.warning("This operation might take a while...\n") - + # Validate custom token operation custom_token_passed, _ = utils.validate_custom_token( cmd, resource_group_name, "dummyLocation" @@ -2840,16 +2840,17 @@ def enable_features( if custom_token_passed is True else get_subscription_id(cmd.cli_ctx) ) - enable_cl, custom_locations_oid = check_cl_registration_and_get_oid( + final_enable_cl, custom_locations_oid = check_cl_registration_and_get_oid( cmd, cl_oid, subscription_id ) - if not enable_cluster_connect and enable_cl: + if not enable_cluster_connect and final_enable_cl: enable_cluster_connect = True logger.warning( "Enabling 'custom-locations' feature will enable 'cluster-connect' feature too." ) - if not enable_cl: + if not final_enable_cl: features.remove("custom-locations") + logger.warn(consts.Custom_Location_Enable_Failed_warning) if len(features) == 0: raise ClientRequestError("Failed to enable 'custom-locations' feature.") @@ -2973,7 +2974,7 @@ def enable_features( cmd_helm_upgrade.extend( ["--set", "systemDefaultValues.clusterconnect-agent.enabled=true"] ) - if enable_cl: + if final_enable_cl: cmd_helm_upgrade.extend( ["--set", "systemDefaultValues.customLocations.enabled=true"] ) @@ -3001,7 +3002,8 @@ def enable_features( raise CLIInternalError( str.format(consts.Error_enabling_Features, helm_upgrade_error_message) ) - + if cl_oid and final_enable_cl and cl_oid == custom_locations_oid: + logger.warn(consts.Manual_Custom_Location_Oid_Warning) return str.format( consts.Successfully_Enabled_Features, features, connected_cluster.name ) @@ -3909,12 +3911,12 @@ def get_custom_locations_oid(cmd: CLICommmand, cl_oid: str | None) -> str: ) # If Cl OID was input, use that if cl_oid: - log_string += "If the manual OID is invalid, custom location may not be properly enabled." - log_string += "Proceeding with using the OID manually provided to enable the 'custom-locations' feature without validation." + log_string += "\nProceeding with using the OID manually provided to enable the 'custom-locations' feature without validation." + log_string += "\nIf the manual OID is invalid, custom location may not be properly enabled." logger.warning(log_string) return cl_oid # If no Cl OID was input, log a Warning and return empty for OID - log_string += "Unable to enable the 'custom-locations' feature. " + str(e) + log_string += "\nException encountered: " + str(e) logger.warning(log_string) return "" From 51f72b96f7184dfd449d100b1e090b104dc566c1 Mon Sep 17 00:00:00 2001 From: Atchut Kumar Barli Date: Mon, 10 Feb 2025 14:51:20 -0800 Subject: [PATCH 28/42] format --- src/connectedk8s/azext_connectedk8s/_constants.py | 4 ++-- src/connectedk8s/azext_connectedk8s/custom.py | 12 ++++++------ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/connectedk8s/azext_connectedk8s/_constants.py b/src/connectedk8s/azext_connectedk8s/_constants.py index ba66da0bb0d..88d609c22f5 100644 --- a/src/connectedk8s/azext_connectedk8s/_constants.py +++ b/src/connectedk8s/azext_connectedk8s/_constants.py @@ -454,11 +454,11 @@ "Failed to save cluster diagnostic checks job log" ) -Manual_Custom_Location_Oid_Warning="""Important! Custom Location feature enablement can’t be validated when using a manually provided OID. If the custom location feature is not enabled, you may encounter an error when creating the custom location. +Manual_Custom_Location_Oid_Warning = """Important! Custom Location feature enablement can’t be validated when using a manually provided OID. If the custom location feature is not enabled, you may encounter an error when creating the custom location. After creating the custom location, run `az customlocation show` and check that ProvisioningState is Succeeded. If ProvisoningState is Failed, then re-try this command with a valid custom location OID to enable the feature. For guidance, refer to: https://aka.ms/enable-customlocation""" -Custom_Location_Enable_Failed_warning="""Important! Custom Location feature wasn’t enabled due to insufficient privileges on the Service Principal Name. If the custom location feature is not enabled, you will encounter an error when creating the custom location. Refer to: https://aka.ms/enable-cl-spn""" +Custom_Location_Enable_Failed_warning = """Important! Custom Location feature wasn’t enabled due to insufficient privileges on the Service Principal Name. If the custom location feature is not enabled, you will encounter an error when creating the custom location. Refer to: https://aka.ms/enable-cl-spn""" # Diagnostic Results Name diff --git a/src/connectedk8s/azext_connectedk8s/custom.py b/src/connectedk8s/azext_connectedk8s/custom.py index 477e510057c..7dd3cc6034c 100644 --- a/src/connectedk8s/azext_connectedk8s/custom.py +++ b/src/connectedk8s/azext_connectedk8s/custom.py @@ -969,7 +969,7 @@ def create_connectedk8s( "Timed out waiting for Agent State to reach terminal state." ) if cl_oid and enable_custom_locations and cl_oid == custom_locations_oid: - logger.warn(consts.Manual_Custom_Location_Oid_Warning) + logger.warn(consts.Manual_Custom_Location_Oid_Warning) return put_cc_response @@ -2791,7 +2791,7 @@ def enable_features( cl_oid: str | None = None, ) -> str: logger.warning("This operation might take a while...\n") - + # Validate custom token operation custom_token_passed, _ = utils.validate_custom_token( cmd, resource_group_name, "dummyLocation" @@ -2843,7 +2843,7 @@ def enable_features( final_enable_cl, custom_locations_oid = check_cl_registration_and_get_oid( cmd, cl_oid, subscription_id ) - if not enable_cluster_connect and final_enable_cl: + if not enable_cluster_connect and final_enable_cl: enable_cluster_connect = True logger.warning( "Enabling 'custom-locations' feature will enable 'cluster-connect' feature too." @@ -2974,7 +2974,7 @@ def enable_features( cmd_helm_upgrade.extend( ["--set", "systemDefaultValues.clusterconnect-agent.enabled=true"] ) - if final_enable_cl: + if final_enable_cl: cmd_helm_upgrade.extend( ["--set", "systemDefaultValues.customLocations.enabled=true"] ) @@ -3002,8 +3002,8 @@ def enable_features( raise CLIInternalError( str.format(consts.Error_enabling_Features, helm_upgrade_error_message) ) - if cl_oid and final_enable_cl and cl_oid == custom_locations_oid: - logger.warn(consts.Manual_Custom_Location_Oid_Warning) + if cl_oid and final_enable_cl and cl_oid == custom_locations_oid: + logger.warn(consts.Manual_Custom_Location_Oid_Warning) return str.format( consts.Successfully_Enabled_Features, features, connected_cluster.name ) From 97be14391b9cb58770c893214ed69983cdb0870c Mon Sep 17 00:00:00 2001 From: Atchut Kumar Barli Date: Mon, 10 Feb 2025 15:05:34 -0800 Subject: [PATCH 29/42] format --- src/connectedk8s/azext_connectedk8s/_constants.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/connectedk8s/azext_connectedk8s/_constants.py b/src/connectedk8s/azext_connectedk8s/_constants.py index 88d609c22f5..09aa6cd8db0 100644 --- a/src/connectedk8s/azext_connectedk8s/_constants.py +++ b/src/connectedk8s/azext_connectedk8s/_constants.py @@ -454,8 +454,8 @@ "Failed to save cluster diagnostic checks job log" ) -Manual_Custom_Location_Oid_Warning = """Important! Custom Location feature enablement can’t be validated when using a manually provided OID. If the custom location feature is not enabled, you may encounter an error when creating the custom location. -After creating the custom location, run `az customlocation show` and check that ProvisioningState is Succeeded. If ProvisoningState is Failed, then re-try this command with a valid custom location OID to enable the feature. +Manual_Custom_Location_Oid_Warning = """Important! Custom Location feature enablement can’t be validated when using a manually provided OID. If the custom location feature is not enabled, you may encounter an error when creating the custom location. +After creating the custom location, run `az customlocation show` and check that ProvisioningState is Succeeded. If ProvisoningState is Failed, then re-try this command with a valid custom location OID to enable the feature. For guidance, refer to: https://aka.ms/enable-customlocation""" Custom_Location_Enable_Failed_warning = """Important! Custom Location feature wasn’t enabled due to insufficient privileges on the Service Principal Name. If the custom location feature is not enabled, you will encounter an error when creating the custom location. Refer to: https://aka.ms/enable-cl-spn""" From 15707ee200add872fea777b1de01169788330c58 Mon Sep 17 00:00:00 2001 From: Atchut Kumar Barli Date: Tue, 11 Feb 2025 12:06:29 -0800 Subject: [PATCH 30/42] singlequote --- src/connectedk8s/azext_connectedk8s/_constants.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/connectedk8s/azext_connectedk8s/_constants.py b/src/connectedk8s/azext_connectedk8s/_constants.py index 09aa6cd8db0..1279c99a0cb 100644 --- a/src/connectedk8s/azext_connectedk8s/_constants.py +++ b/src/connectedk8s/azext_connectedk8s/_constants.py @@ -454,11 +454,11 @@ "Failed to save cluster diagnostic checks job log" ) -Manual_Custom_Location_Oid_Warning = """Important! Custom Location feature enablement can’t be validated when using a manually provided OID. If the custom location feature is not enabled, you may encounter an error when creating the custom location. +Manual_Custom_Location_Oid_Warning = """Important! Custom Location feature enablement can't be validated when using a manually provided OID. If the custom location feature is not enabled, you may encounter an error when creating the custom location. After creating the custom location, run `az customlocation show` and check that ProvisioningState is Succeeded. If ProvisoningState is Failed, then re-try this command with a valid custom location OID to enable the feature. For guidance, refer to: https://aka.ms/enable-customlocation""" -Custom_Location_Enable_Failed_warning = """Important! Custom Location feature wasn’t enabled due to insufficient privileges on the Service Principal Name. If the custom location feature is not enabled, you will encounter an error when creating the custom location. Refer to: https://aka.ms/enable-cl-spn""" +Custom_Location_Enable_Failed_warning = """Important! Custom Location feature wasn't enabled due to insufficient privileges on the Service Principal Name. If the custom location feature is not enabled, you will encounter an error when creating the custom location. Refer to: https://aka.ms/enable-cl-spn""" # Diagnostic Results Name From 4194fecf10d931a34ae37fa267e98d9b72143196 Mon Sep 17 00:00:00 2001 From: Atchut Kumar Barli Date: Tue, 11 Feb 2025 12:58:07 -0800 Subject: [PATCH 31/42] warnings --- src/connectedk8s/azext_connectedk8s/custom.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/connectedk8s/azext_connectedk8s/custom.py b/src/connectedk8s/azext_connectedk8s/custom.py index 7dd3cc6034c..b384571614c 100644 --- a/src/connectedk8s/azext_connectedk8s/custom.py +++ b/src/connectedk8s/azext_connectedk8s/custom.py @@ -969,7 +969,7 @@ def create_connectedk8s( "Timed out waiting for Agent State to reach terminal state." ) if cl_oid and enable_custom_locations and cl_oid == custom_locations_oid: - logger.warn(consts.Manual_Custom_Location_Oid_Warning) + logger.warning(consts.Manual_Custom_Location_Oid_Warning) return put_cc_response @@ -2850,7 +2850,7 @@ def enable_features( ) if not final_enable_cl: features.remove("custom-locations") - logger.warn(consts.Custom_Location_Enable_Failed_warning) + logger.warning(consts.Custom_Location_Enable_Failed_warning) if len(features) == 0: raise ClientRequestError("Failed to enable 'custom-locations' feature.") @@ -3003,7 +3003,7 @@ def enable_features( str.format(consts.Error_enabling_Features, helm_upgrade_error_message) ) if cl_oid and final_enable_cl and cl_oid == custom_locations_oid: - logger.warn(consts.Manual_Custom_Location_Oid_Warning) + logger.warning(consts.Manual_Custom_Location_Oid_Warning) return str.format( consts.Successfully_Enabled_Features, features, connected_cluster.name ) From 41d6f64c3e2b304b0d1d6af8166baecaf4ad293a Mon Sep 17 00:00:00 2001 From: Bavneet Singh <33008256+bavneetsingh16@users.noreply.github.com> Date: Thu, 13 Feb 2025 14:38:48 -0800 Subject: [PATCH 32/42] change helm package download path to mcr (#32) --- .../azext_connectedk8s/_constants.py | 2 +- src/connectedk8s/azext_connectedk8s/custom.py | 46 +++---- .../latest/test_connectedk8s_scenario.py | 114 ++++-------------- 3 files changed, 45 insertions(+), 117 deletions(-) diff --git a/src/connectedk8s/azext_connectedk8s/_constants.py b/src/connectedk8s/azext_connectedk8s/_constants.py index 1279c99a0cb..651f132ee92 100644 --- a/src/connectedk8s/azext_connectedk8s/_constants.py +++ b/src/connectedk8s/azext_connectedk8s/_constants.py @@ -482,7 +482,7 @@ # URL constants CLIENT_PROXY_MCR_TARGET = "mcr.microsoft.com/azureconnectivity/proxy" -HELM_STORAGE_URL = "https://k8connecthelm.azureedge.net" +HELM_MCR_URL = "mcr.microsoft.com/azurearck8s/helm" HELM_VERSION = "v3.12.2" Download_And_Install_Kubectl_Fault_Type = "Failed to download and install kubectl" Azure_Access_Token_Variable = "AZURE_ACCESS_TOKEN" diff --git a/src/connectedk8s/azext_connectedk8s/custom.py b/src/connectedk8s/azext_connectedk8s/custom.py index b384571614c..d4a061654dd 100644 --- a/src/connectedk8s/azext_connectedk8s/custom.py +++ b/src/connectedk8s/azext_connectedk8s/custom.py @@ -16,12 +16,12 @@ import stat import tempfile import time -import urllib.request from base64 import b64decode, b64encode from concurrent.futures import ThreadPoolExecutor from subprocess import DEVNULL, PIPE, Popen from typing import TYPE_CHECKING, Any, Iterable +import oras.client # type: ignore[import-untyped] import yaml from azure.cli.command_modules.role import graph_client_factory from azure.cli.core import get_default_cli, telemetry @@ -1170,20 +1170,23 @@ def install_helm_client() -> str: telemetry.add_extension_event( "connectedk8s", {"Context.Default.AzureCLI.MachineType": machine_type} ) - # Set helm binary download & install locations if operating_system == "windows": - download_location_string = f".azure\\helm\\{consts.HELM_VERSION}\\helm-{consts.HELM_VERSION}-{operating_system}-amd64.zip" + download_location_string = f".azure\\helm\\{consts.HELM_VERSION}" + download_file_name = f"helm-{consts.HELM_VERSION}-{operating_system}-amd64.zip" install_location_string = ( f".azure\\helm\\{consts.HELM_VERSION}\\{operating_system}-amd64\\helm.exe" ) - requestUri = f"{consts.HELM_STORAGE_URL}/helmsigned/helm-{consts.HELM_VERSION}-{operating_system}-amd64.zip" + artifactTag = f"helm-{consts.HELM_VERSION}-{operating_system}-amd64" elif operating_system == "linux" or operating_system == "darwin": - download_location_string = f".azure/helm/{consts.HELM_VERSION}/helm-{consts.HELM_VERSION}-{operating_system}-amd64.tar.gz" + download_location_string = f".azure/helm/{consts.HELM_VERSION}" + download_file_name = ( + f"helm-{consts.HELM_VERSION}-{operating_system}-amd64.tar.gz" + ) install_location_string = ( f".azure/helm/{consts.HELM_VERSION}/{operating_system}-amd64/helm" ) - requestUri = f"{consts.HELM_STORAGE_URL}/helm/helm-{consts.HELM_VERSION}-{operating_system}-amd64.tar.gz" + artifactTag = f"helm-{consts.HELM_VERSION}-{operating_system}-amd64" else: telemetry.set_exception( exception="Unsupported OS for installing helm client", @@ -1216,11 +1219,15 @@ def install_helm_client() -> str: logger.warning( "Downloading helm client for first time. This can take few minutes..." ) + client = oras.client.OrasClient() retry_count = 3 retry_delay = 5 for i in range(retry_count): try: - response = urllib.request.urlopen(requestUri) + client.pull( + target=f"{consts.HELM_MCR_URL}:{artifactTag}", + outdir=download_location, + ) break except Exception as e: if i == retry_count - 1: @@ -1237,26 +1244,13 @@ def install_helm_client() -> str: ) time.sleep(retry_delay) - responseContent = response.read() - response.close() - - # Creating the compressed helm binaries - try: - with open(download_location, "wb") as f: - f.write(responseContent) - except Exception as e: - telemetry.set_exception( - exception=e, - fault_type=consts.Create_HelmExe_Fault_Type, - summary="Unable to create helm executable", - ) - reco_str = f"Please ensure that you delete the directory '{download_dir}' before trying again." - raise ClientRequestError( - "Failed to create helm executable." + str(e), recommendation=reco_str - ) # Extract the archive. try: - shutil.unpack_archive(download_location, download_dir) + extract_dir = download_location + download_location = os.path.expanduser( + os.path.join(download_location, download_file_name) + ) + shutil.unpack_archive(download_location, extract_dir) os.chmod(install_location, os.stat(install_location).st_mode | stat.S_IXUSR) except Exception as e: telemetry.set_exception( @@ -1264,7 +1258,7 @@ def install_helm_client() -> str: fault_type=consts.Extract_HelmExe_Fault_Type, summary="Unable to extract helm executable", ) - reco_str = f"Please ensure that you delete the directory '{download_dir}' before trying again." + reco_str = f"Please ensure that you delete the directory '{extract_dir}' before trying again." raise ClientRequestError( "Failed to extract helm executable." + str(e), recommendation=reco_str ) diff --git a/src/connectedk8s/azext_connectedk8s/tests/latest/test_connectedk8s_scenario.py b/src/connectedk8s/azext_connectedk8s/tests/latest/test_connectedk8s_scenario.py index aa55f2800ee..9d899e786da 100644 --- a/src/connectedk8s/azext_connectedk8s/tests/latest/test_connectedk8s_scenario.py +++ b/src/connectedk8s/azext_connectedk8s/tests/latest/test_connectedk8s_scenario.py @@ -12,9 +12,9 @@ import stat import subprocess import time -import urllib.request from subprocess import PIPE +import oras.client # type: ignore[import-untyped] import psutil import requests from azure.cli.core import get_default_cli @@ -71,23 +71,21 @@ def install_helm_client(): # Set helm binary download & install locations if operating_system == "windows": - download_location_string = ( - f".azure\\helm\\{consts.HELM_VERSION}\\helm-{consts.HELM_VERSION}-\ - {operating_system}-amd64.zip" - ) + download_location_string = f".azure\\helm\\{consts.HELM_VERSION}" + download_file_name = f"helm-{consts.HELM_VERSION}-{operating_system}-amd64.zip" install_location_string = ( f".azure\\helm\\{consts.HELM_VERSION}\\{operating_system}-amd64\\helm.exe" ) - requestUri = f"{consts.HELM_STORAGE_URL}/helm/helm-{consts.HELM_VERSION}-{operating_system}-amd64.zip" + artifactTag = f"helm-{consts.HELM_VERSION}-{operating_system}-amd64" elif operating_system == "linux" or operating_system == "darwin": - download_location_string = ( - f".azure/helm/{consts.HELM_VERSION}/helm-{consts.HELM_VERSION}-\ - {operating_system}-amd64.tar.gz" + download_location_string = f".azure/helm/{consts.HELM_VERSION}" + download_file_name = ( + f"helm-{consts.HELM_VERSION}-{operating_system}-amd64.tar.gz" ) install_location_string = ( f".azure/helm/{consts.HELM_VERSION}/{operating_system}-amd64/helm" ) - requestUri = f"{consts.HELM_STORAGE_URL}/helm/helm-{consts.HELM_VERSION}-{operating_system}-amd64.tar.gz" + artifactTag = f"helm-{consts.HELM_VERSION}-{operating_system}-amd64" else: logger.warning( f"The {operating_system} platform is not currently supported for installing helm client." @@ -98,8 +96,8 @@ def install_helm_client(): download_dir = os.path.dirname(download_location) install_location = os.path.expanduser(os.path.join("~", install_location_string)) - # Download compressed helm binary if not already present - if not os.path.isfile(download_location): + # Download compressed Helm binary if not already present + if not os.path.isfile(install_location): # Creating the helm folder if it doesnt exist if not os.path.exists(download_dir): try: @@ -109,27 +107,25 @@ def install_helm_client(): return # Downloading compressed helm client executable + logger.warning( + "Downloading helm client for first time. This can take few minutes..." + ) + client = oras.client.OrasClient() try: - response = urllib.request.urlopen(requestUri) + client.pull( + target=f"{consts.HELM_MCR_URL}:{artifactTag}", outdir=download_location + ) except Exception as e: logger.warning("Failed to download helm client." + str(e)) return - responseContent = response.read() - response.close() - - # Creating the compressed helm binaries + # Extract the archive. try: - with open(download_location, "wb") as f: - f.write(responseContent) - except Exception as e: - logger.warning("Failed to extract helm executable" + str(e)) - return - - # Extract compressed helm binary - if not os.path.isfile(install_location): - try: - shutil.unpack_archive(download_location, download_dir) + extract_dir = download_location + download_location = os.path.expanduser( + os.path.join(download_location, download_file_name) + ) + shutil.unpack_archive(download_location, extract_dir) os.chmod(install_location, os.stat(install_location).st_mode | stat.S_IXUSR) except Exception as e: logger.warning("Failed to extract helm executable" + str(e)) @@ -220,52 +216,6 @@ def test_connect(self, resource_group): # delete the kube config os.remove(_get_test_data_file(managed_cluster_name + "-config.yaml")) - @live_only() - @ResourceGroupPreparer( - name_prefix="conk8stest", location=CONFIG["location"], random_name_length=16 - ) - def test_connect_withoidcandworkloadidentity(self, resource_group): - managed_cluster_name = self.create_random_name(prefix="test-connect", length=24) - kubeconfig = _get_test_data_file(managed_cluster_name + "-config.yaml") - self.kwargs.update( - { - "rg": resource_group, - "name": self.create_random_name(prefix="cc-", length=12), - "kubeconfig": kubeconfig, - "managed_cluster_name": managed_cluster_name, - "location": CONFIG["location"], - } - ) - - # scenario - oidc issuer and workload identity enabled - self.cmd("aks create -g {rg} -n {managed_cluster_name} --generate-ssh-keys") - self.cmd( - "aks get-credentials -g {rg} -n {managed_cluster_name} -f {kubeconfig} --admin" - ) - self.cmd( - "connectedk8s connect -n {name} -g {rg} -l {location} --tags foo=doo --enable-oidc-issuer \ - --enable-workload-identity --kube-config {kubeconfig} --kube-context {managed_cluster_name}-admin" - ) - self.cmd( - "connectedk8s show -g {rg} -n {name}", - checks=[ - self.check("tags.foo", "doo"), - self.check("name", "{name}"), - self.check("resourceGroup", "{rg}"), - self.check("oidcIssuerProfile.enabled", True), - self.check("securityProfile.workloadIndentity.enabled", True), - ], - ) - - self.cmd( - "connectedk8s delete -g {rg} -n {name} --kube-config {kubeconfig} --kube-context \ - {managed_cluster_name}-admin -y" - ) - self.cmd("aks delete -g {rg} -n {managed_cluster_name} -y") - - # delete the kube config - os.remove(_get_test_data_file(managed_cluster_name + "-config.yaml")) - @live_only() @ResourceGroupPreparer( name_prefix="conk8stest", location=CONFIG["location"], random_name_length=16 @@ -470,7 +420,7 @@ def test_enable_disable_features(self, resource_group): with self.assertRaisesRegex( CLIError, "Disabling 'cluster-connect' feature is not allowed when \ -'custom-locations' feature is enabled.", +'custom-locations' feature is enabled", ): self.cmd( "connectedk8s disable-features -n {name} -g {rg} --features cluster-connect --kube-config \ @@ -847,22 +797,6 @@ def test_update(self, resource_group): ], ) - # scenario - oidc issuer and workload identity enabled - self.cmd( - "connectedk8s update -n {name} -g {rg} --enable-oidc-issuer \ - --enable-workload-identity --kube-config {kubeconfig} \ - --kube-context {managed_cluster_name}-admin" - ) - self.cmd( - "connectedk8s show -g {rg} -n {name}", - checks=[ - self.check("name", "{name}"), - self.check("resourceGroup", "{rg}"), - self.check("oidcIssuerProfile.enabled", True), - self.check("securityProfile.workloadIndentity.enabled", True), - ], - ) - # scenario - oidc issuer enabled and self hosted issuer url set self.cmd( 'connectedk8s update -n {name} -g {rg} --enable-oidc-issuer \ From 9ab5e6349abf8ac28c1cc09b7047d809fc39b9b4 Mon Sep 17 00:00:00 2001 From: Bavneet Singh Date: Fri, 14 Feb 2025 09:49:34 -0800 Subject: [PATCH 33/42] update connectedk8s cli version and release notes --- src/connectedk8s/HISTORY.rst | 5 +++++ src/connectedk8s/setup.py | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/connectedk8s/HISTORY.rst b/src/connectedk8s/HISTORY.rst index 165af28b9bd..4a5a19426ea 100644 --- a/src/connectedk8s/HISTORY.rst +++ b/src/connectedk8s/HISTORY.rst @@ -3,6 +3,11 @@ Release History =============== +1.10.6 +++++++ +* Added support for downloading helm binaries from MCR. +* Update Messages for custom location oid discovery. + 1.10.5 ++++++ * Fixed bug impacting long-running operations of the az connectedk8s proxy command. diff --git a/src/connectedk8s/setup.py b/src/connectedk8s/setup.py index ca86f052e70..f90cb8ba719 100644 --- a/src/connectedk8s/setup.py +++ b/src/connectedk8s/setup.py @@ -13,7 +13,7 @@ # TODO: Confirm this is the right version number you want and it matches your # HISTORY.rst entry. -VERSION = "1.10.5" +VERSION = "1.10.6" # The full list of classifiers is available at # https://pypi.python.org/pypi?%3Aaction=list_classifiers From cf44613065edcd4f802e633cc73e9d9c941643f0 Mon Sep 17 00:00:00 2001 From: Bavneet Singh Date: Fri, 14 Feb 2025 09:50:31 -0800 Subject: [PATCH 34/42] update release notes --- src/connectedk8s/HISTORY.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/connectedk8s/HISTORY.rst b/src/connectedk8s/HISTORY.rst index 4a5a19426ea..db00f63bfee 100644 --- a/src/connectedk8s/HISTORY.rst +++ b/src/connectedk8s/HISTORY.rst @@ -6,7 +6,7 @@ Release History 1.10.6 ++++++ * Added support for downloading helm binaries from MCR. -* Update Messages for custom location oid discovery. +* Added warnings for custom location feature based on Service Principal Name permissions and OID validation. 1.10.5 ++++++ From e9c4fd831170192fe026a97c5057ce3b120867ed Mon Sep 17 00:00:00 2001 From: Bavneet Singh Date: Fri, 14 Feb 2025 10:32:03 -0800 Subject: [PATCH 35/42] update description in release notes --- src/connectedk8s/HISTORY.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/connectedk8s/HISTORY.rst b/src/connectedk8s/HISTORY.rst index db00f63bfee..aa8788b36a2 100644 --- a/src/connectedk8s/HISTORY.rst +++ b/src/connectedk8s/HISTORY.rst @@ -6,7 +6,7 @@ Release History 1.10.6 ++++++ * Added support for downloading helm binaries from MCR. -* Added warnings for custom location feature based on Service Principal Name permissions and OID validation. +* Added warnings for custom location feature based on Service Principal Name or User permissions to retrieve OID. 1.10.5 ++++++ From 26dc437585046a20886f9ac9b99784a1b786ff68 Mon Sep 17 00:00:00 2001 From: Atchut Kumar Barli Date: Wed, 26 Feb 2025 11:06:19 -0800 Subject: [PATCH 36/42] Adds new distro discovery and fix to send the infra instead of generic (#35) * add distros * format * ruffcheck * ruffformat --- src/connectedk8s/azext_connectedk8s/_utils.py | 2 +- src/connectedk8s/azext_connectedk8s/custom.py | 34 ++++++- .../tests/unittests/test_custom.py | 88 +++++++++++++++++++ 3 files changed, 119 insertions(+), 5 deletions(-) create mode 100644 src/connectedk8s/azext_connectedk8s/tests/unittests/test_custom.py diff --git a/src/connectedk8s/azext_connectedk8s/_utils.py b/src/connectedk8s/azext_connectedk8s/_utils.py index e589f0e970e..55396ebcb11 100644 --- a/src/connectedk8s/azext_connectedk8s/_utils.py +++ b/src/connectedk8s/azext_connectedk8s/_utils.py @@ -1033,7 +1033,7 @@ def validate_infrastructure_type(infra: str) -> str | None: for s in consts.Infrastructure_Enum_Values[1:]: # First value is "auto" if s.lower() == infra.lower(): return s - return None + return infra def get_values_file() -> str | None: diff --git a/src/connectedk8s/azext_connectedk8s/custom.py b/src/connectedk8s/azext_connectedk8s/custom.py index d4a061654dd..8f481f1111b 100644 --- a/src/connectedk8s/azext_connectedk8s/custom.py +++ b/src/connectedk8s/azext_connectedk8s/custom.py @@ -1356,14 +1356,16 @@ def get_private_key(key_pair: RsaKey) -> str: return PEM.encode(privKey_DER, "RSA PRIVATE KEY") +# Updated function to include more Kubernetes distributions based on provided criteria def get_kubernetes_distro(api_response: V1NodeList) -> str: # Heuristic if api_response is None: return "generic" try: for node in api_response.items: - labels = node.metadata.labels + labels = node.metadata.labels or {} provider_id = str(node.spec.provider_id) - annotations = node.metadata.annotations + annotations = node.metadata.annotations or {} + if labels.get("node.openshift.io/os_id"): return "openshift" if labels.get("kubernetes.azure.com/node-image-version"): @@ -1388,6 +1390,28 @@ def get_kubernetes_distro(api_response: V1NodeList) -> str: # Heuristic "rke.cattle.io/internal-ip" ): return "rancher_rke" + if any(label.startswith("snap.microk8s") for label in labels): + return "microk8s" + if any(label.startswith("k3os.io") for label in labels): + return "k3os" + if any(label.startswith("talos.dev") for label in labels): + return "talos" + if any(key.startswith("rke2.io") for key in annotations): + return "rke2" + if any( + label.startswith("node-role.kubernetes.io") for label in labels + ) or any( + key.startswith("kubeadm.alpha.kubernetes.io") for key in annotations + ): + return "kubeadm" + if any(label.startswith("run.tanzu.vmware.com") for label in labels): + return "tkg" + if any(label.startswith("openebs.io") for label in labels): + return "openebs" + if any(label.startswith("flatcar-linux") for label in labels): + return "flatcar" + if any(label.startswith("k0s.k0sproject.io") for label in labels): + return "k0s" return "generic" except Exception as e: # pylint: disable=broad-except logger.debug( @@ -1409,8 +1433,10 @@ def get_kubernetes_infra(api_response: V1NodeList) -> str: # Heuristic for node in api_response.items: provider_id = str(node.spec.provider_id) infra = provider_id.split(":")[0] - if infra == "k3s" or infra == "kind": - return "generic" + if infra == "k3s": + return "k3s" + if infra == "kind": + return "kind" if infra == "azure": return "azure" if infra == "gce": diff --git a/src/connectedk8s/azext_connectedk8s/tests/unittests/test_custom.py b/src/connectedk8s/azext_connectedk8s/tests/unittests/test_custom.py new file mode 100644 index 00000000000..1693db1fefc --- /dev/null +++ b/src/connectedk8s/azext_connectedk8s/tests/unittests/test_custom.py @@ -0,0 +1,88 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- +import os +import sys +from typing import Dict, Optional + +import pytest +from kubernetes.client.models import V1Node, V1NodeList, V1NodeSpec, V1ObjectMeta + +sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "../../.."))) +from azext_connectedk8s.custom import get_kubernetes_distro, get_kubernetes_infra + + +def create_node( + provider_id: Optional[str] = None, + labels: Optional[Dict[str, str]] = None, + annotations: Optional[Dict[str, str]] = None, +) -> V1Node: + spec = V1NodeSpec(provider_id=provider_id) + metadata = V1ObjectMeta(labels=labels or {}, annotations=annotations or {}) + return V1Node(spec=spec, metadata=metadata) + + +@pytest.mark.parametrize( + "provider_id, expected", + [ + ("k3s://node1", "k3s"), + ("kind://node1", "kind"), + ("azure://node1", "azure"), + ("gce://node1", "gcp"), + ("aws://node1", "aws"), + ("unknown://node1", "unknown"), + (None, "generic"), + ], +) +def test_get_kubernetes_infra(provider_id, expected): + node = create_node(provider_id) if provider_id is not None else None + api_response = V1NodeList(items=[node]) if node else None + assert get_kubernetes_infra(api_response) == expected + + +def test_empty_items(): + api_response = V1NodeList(items=[]) + assert get_kubernetes_infra(api_response) == "generic" + + +def test_invalid_provider_id(): + node = create_node(None) + api_response = V1NodeList(items=[node]) + assert get_kubernetes_infra(api_response) == "None" + + +# --------------------- Tests for get_kubernetes_distro --------------------- +@pytest.mark.parametrize( + "labels, annotations, provider_id, expected", + [ + ({"node.openshift.io/os_id": "rhcos"}, {}, None, "openshift"), + ({"kubernetes.azure.com/node-image-version": "2022.11.01"}, {}, None, "aks"), + ({"cloud.google.com/gke-nodepool": "default-pool"}, {}, None, "gke"), + ({"cloud.google.com/gke-os-distribution": "cos"}, {}, None, "gke"), + ({"eks.amazonaws.com/nodegroup": "nodegroup-1"}, {}, None, "eks"), + ({"minikube.k8s.io/version": "v1.25.0"}, {}, None, "minikube"), + ({}, {"node.aksedge.io/distro": "aks_edge_k3s"}, None, "aks_edge_k3s"), + ({}, {"node.aksedge.io/distro": "aks_edge_k8s"}, None, "aks_edge_k8s"), + ({}, {}, "kind://node1", "kind"), + ({}, {}, "k3s://node1", "k3s"), + ({}, {"rke.cattle.io/external-ip": "192.168.1.1"}, None, "rancher_rke"), + ({}, {"rke.cattle.io/internal-ip": "10.0.0.1"}, None, "rancher_rke"), + ({}, {}, None, "generic"), + ], +) +def test_get_kubernetes_distro(labels, annotations, provider_id, expected): + node = create_node(provider_id=provider_id, labels=labels, annotations=annotations) + api_response = V1NodeList(items=[node]) + assert get_kubernetes_distro(api_response) == expected + + +def test_distro_empty_items(): + api_response = V1NodeList(items=[]) + assert get_kubernetes_distro(api_response) == "generic" + + +def test_distro_invalid_metadata(): + node = create_node(provider_id="aws://node1", labels=None, annotations=None) + api_response = V1NodeList(items=[node]) + assert get_kubernetes_distro(api_response) == "generic" From 87476ba60dbd9296b0091ba3141981b1293885a5 Mon Sep 17 00:00:00 2001 From: Atchut Kumar Barli Date: Wed, 5 Mar 2025 17:40:42 -0800 Subject: [PATCH 37/42] Add documentation update for connectedk8s cli (#33) * add prereq * standard * format * typo * brackets --- src/connectedk8s/azext_connectedk8s/_help.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/connectedk8s/azext_connectedk8s/_help.py b/src/connectedk8s/azext_connectedk8s/_help.py index 93ee46274a5..ebdf83064cc 100644 --- a/src/connectedk8s/azext_connectedk8s/_help.py +++ b/src/connectedk8s/azext_connectedk8s/_help.py @@ -16,6 +16,7 @@ helps["connectedk8s connect"] = """ type: command short-summary: Onboard a connected kubernetes cluster to azure. + long-summary: The Kubernetes cluster to be onboarded as a connected cluster must be the default cluster in kubeconfig. Run kubectl config get-contexts to confirm the target context name. Then set the default context to the right cluster by running kubectl config use-context target-cluster-name. examples: - name: Onboard a connected kubernetes cluster with default kube config and kube context. text: az connectedk8s connect -g resourceGroupName -n connectedClusterName From b36f4f3ebccdeb66d77805960704ce579c0d10ee Mon Sep 17 00:00:00 2001 From: Atchut Kumar Barli Date: Thu, 6 Mar 2025 11:53:55 -0800 Subject: [PATCH 38/42] update release version to 1.10.7 (#36) * add release history * whl test * remove whl --- src/connectedk8s/HISTORY.rst | 5 +++++ src/connectedk8s/setup.py | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/connectedk8s/HISTORY.rst b/src/connectedk8s/HISTORY.rst index aa8788b36a2..6d199aa7b8b 100644 --- a/src/connectedk8s/HISTORY.rst +++ b/src/connectedk8s/HISTORY.rst @@ -3,6 +3,11 @@ Release History =============== +1.10.7 +++++++ +* Added support for discovering additional k8s distributions and Infrastructure. +* Updated Connect command help to indicate the kubeconfig prerequisite. + 1.10.6 ++++++ * Added support for downloading helm binaries from MCR. diff --git a/src/connectedk8s/setup.py b/src/connectedk8s/setup.py index f90cb8ba719..7625c142670 100644 --- a/src/connectedk8s/setup.py +++ b/src/connectedk8s/setup.py @@ -13,7 +13,7 @@ # TODO: Confirm this is the right version number you want and it matches your # HISTORY.rst entry. -VERSION = "1.10.6" +VERSION = "1.10.7" # The full list of classifiers is available at # https://pypi.python.org/pypi?%3Aaction=list_classifiers From 62239d7baa1520189e63f1e48199459257bc1290 Mon Sep 17 00:00:00 2001 From: Jorge Daboub <52983326+JorgeDaboub@users.noreply.github.com> Date: Thu, 6 Mar 2025 17:05:15 -0800 Subject: [PATCH 39/42] Update get_login_credentials flow (#37) --- src/connectedk8s/HISTORY.rst | 1 + src/connectedk8s/azext_connectedk8s/azext_metadata.json | 2 +- .../azext_connectedk8s/clientproxyhelper/_utils.py | 3 +-- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/connectedk8s/HISTORY.rst b/src/connectedk8s/HISTORY.rst index 6d199aa7b8b..d44cb3899fb 100644 --- a/src/connectedk8s/HISTORY.rst +++ b/src/connectedk8s/HISTORY.rst @@ -7,6 +7,7 @@ Release History ++++++ * Added support for discovering additional k8s distributions and Infrastructure. * Updated Connect command help to indicate the kubeconfig prerequisite. +* Fixed the issue where the 'connectedk8s proxy' command would fail with newer versions of the Azure CLI. 1.10.6 ++++++ diff --git a/src/connectedk8s/azext_connectedk8s/azext_metadata.json b/src/connectedk8s/azext_connectedk8s/azext_metadata.json index 9e34ce71fa8..cc564987cca 100644 --- a/src/connectedk8s/azext_connectedk8s/azext_metadata.json +++ b/src/connectedk8s/azext_connectedk8s/azext_metadata.json @@ -1,4 +1,4 @@ { "name": "connectedk8s", - "azext.minCliCoreVersion": "2.64.0" + "azext.minCliCoreVersion": "2.70.0" } \ No newline at end of file diff --git a/src/connectedk8s/azext_connectedk8s/clientproxyhelper/_utils.py b/src/connectedk8s/azext_connectedk8s/clientproxyhelper/_utils.py index dcf9469d430..7ff5fea032e 100644 --- a/src/connectedk8s/azext_connectedk8s/clientproxyhelper/_utils.py +++ b/src/connectedk8s/azext_connectedk8s/clientproxyhelper/_utils.py @@ -146,8 +146,7 @@ def fetch_and_post_at_to_csp( profile = Profile(cli_ctx=cmd.cli_ctx) try: credential, _, _ = profile.get_login_credentials( - subscription_id=profile.get_subscription()["id"], - resource=consts.KAP_1P_Server_App_Scope, + subscription_id=profile.get_subscription()["id"] ) accessToken = credential.get_token( consts.KAP_1P_Server_App_Scope, data=token_data From 4145d5d09b16c5ec123a377a5aaaeb77c092eb11 Mon Sep 17 00:00:00 2001 From: "Matthew McNeal (from Dev Box)" Date: Tue, 18 Mar 2025 17:49:15 -0400 Subject: [PATCH 40/42] Add parameterization for the airgapped clouds --- .../azext_connectedk8s/_constants.py | 20 ++-- .../azext_connectedk8s/_precheckutils.py | 19 +++- .../clientproxyhelper/_binaryutils.py | 22 +++- .../clientproxyhelper/_proxylogic.py | 4 +- src/connectedk8s/azext_connectedk8s/custom.py | 106 ++++++++++++------ .../latest/test_connectedk8s_scenario.py | 2 +- 6 files changed, 120 insertions(+), 53 deletions(-) diff --git a/src/connectedk8s/azext_connectedk8s/_constants.py b/src/connectedk8s/azext_connectedk8s/_constants.py index 4f173c668a2..1f23246a68a 100644 --- a/src/connectedk8s/azext_connectedk8s/_constants.py +++ b/src/connectedk8s/azext_connectedk8s/_constants.py @@ -63,13 +63,13 @@ AHB_Enum_Values = ["True", "False", "NotApplicable"] Feature_Values = ["cluster-connect", "azure-rbac", "custom-locations"] CRD_FOR_FORCE_DELETE = [ - "arccertificates.clusterconfig.azure.com", - "azureclusteridentityrequests.clusterconfig.azure.com", - "azureextensionidentities.clusterconfig.azure.com", - "connectedclusters.arc.azure.com", - "customlocationsettings.clusterconfig.azure.com", - "extensionconfigs.clusterconfig.azure.com", - "gitconfigs.clusterconfig.azure.com", + "arccertificates.clusterconfig.azure", + "azureclusteridentityrequests.clusterconfig.azure", + "azureextensionidentities.clusterconfig.azure", + "connectedclusters.arc.azure", + "customlocationsettings.clusterconfig.azure", + "extensionconfigs.clusterconfig.azure", + "gitconfigs.clusterconfig.azure", ] Helm_Install_Release_Userfault_Messages = [ "forbidden", @@ -418,7 +418,7 @@ # Connect Precheck Diagnoser constants Cluster_Diagnostic_Checks_Job_Registry_Path = ( - "mcr.microsoft.com/azurearck8s/helmchart/stable/clusterdiagnosticchecks:0.2.2" + "azurearck8s/helmchart/stable/clusterdiagnosticchecks:0.2.2" ) Cluster_Diagnostic_Checks_Helm_Install_Failed_Fault_Type = ( "Error while installing cluster diagnostic checks helm release" @@ -481,8 +481,8 @@ DEFAULT_MAX_ONBOARDING_TIMEOUT_HELMVALUE_SECONDS = "1200" # URL constants -CLIENT_PROXY_MCR_TARGET = "mcr.microsoft.com/azureconnectivity/proxy" -HELM_MCR_URL = "mcr.microsoft.com/azurearck8s/helm" +CLIENT_PROXY_MCR_TARGET = "azureconnectivity/proxy" +HELM_MCR_URL = "azurearck8s/helm" HELM_VERSION = "v3.12.2" Download_And_Install_Kubectl_Fault_Type = "Failed to download and install kubectl" Azure_Access_Token_Variable = "AZURE_ACCESS_TOKEN" diff --git a/src/connectedk8s/azext_connectedk8s/_precheckutils.py b/src/connectedk8s/azext_connectedk8s/_precheckutils.py index 257fde55463..5cb90893243 100644 --- a/src/connectedk8s/azext_connectedk8s/_precheckutils.py +++ b/src/connectedk8s/azext_connectedk8s/_precheckutils.py @@ -21,6 +21,7 @@ if TYPE_CHECKING: from kubernetes.client import BatchV1Api, CoreV1Api + from knack.commands import CLICommand logger = get_logger(__name__) # pylint: disable=unused-argument, too-many-locals, too-many-branches, too-many-statements, line-too-long @@ -30,6 +31,7 @@ def fetch_diagnostic_checks_results( + cmd: CLICommand, corev1_api_instance: CoreV1Api, batchv1_api_instance: BatchV1Api, helm_client_location: str, @@ -52,6 +54,7 @@ def fetch_diagnostic_checks_results( # Executing the cluster_diagnostic_checks job and fetching the logs obtained cluster_diagnostic_checks_container_log = ( executing_cluster_diagnostic_checks_job( + cmd, corev1_api_instance, batchv1_api_instance, helm_client_location, @@ -135,6 +138,7 @@ def fetch_diagnostic_checks_results( def executing_cluster_diagnostic_checks_job( + cmd: CLICommand, corev1_api_instance: CoreV1Api, batchv1_api_instance: BatchV1Api, helm_client_location: str, @@ -208,8 +212,21 @@ def executing_cluster_diagnostic_checks_job( ) return None + active_directory_array = cmd.cli_ctx.cloud.endpoints.active_directory.split(".") + + # default for public, mc, ff clouds + mcr_postfix = active_directory_array[2] + # special cases for USSec, exclude part of suffix + if len(active_directory_array) == 4 and active_directory_array[2] == "microsoft": + mcr_postfix = active_directory_array[3] + # special case for USNat + elif len(active_directory_array) == 5: + mcr_postfix = active_directory_array[2] + "." + active_directory_array[3] + "." + active_directory_array[4] + + mcr_url = f"mcr.microsoft.{mcr_postfix}" + chart_path = azext_utils.get_chart_path( - consts.Cluster_Diagnostic_Checks_Job_Registry_Path, + f"{mcr_url}/{consts.Cluster_Diagnostic_Checks_Job_Registry_Path}", kube_config, kube_context, helm_client_location, diff --git a/src/connectedk8s/azext_connectedk8s/clientproxyhelper/_binaryutils.py b/src/connectedk8s/azext_connectedk8s/clientproxyhelper/_binaryutils.py index 56f7b218b7e..22b07f306a8 100644 --- a/src/connectedk8s/azext_connectedk8s/clientproxyhelper/_binaryutils.py +++ b/src/connectedk8s/azext_connectedk8s/clientproxyhelper/_binaryutils.py @@ -13,6 +13,7 @@ from azure.cli.core import azclierror, telemetry from azure.cli.core.style import Style, print_styled_text from knack import log +from knack.commands import CLICommand import azext_connectedk8s._constants as consts import azext_connectedk8s._fileutils as file_utils @@ -22,6 +23,7 @@ # Downloads client side proxy to connect to Arc Connectivity Platform def install_client_side_proxy( + cmd: CLICommand, arc_proxy_folder: Optional[str], debug: bool = False ) -> str: client_operating_system = _get_client_operating_system() @@ -48,7 +50,7 @@ def install_client_side_proxy( ) _download_proxy_from_MCR( - install_dir, proxy_name, client_operating_system, client_architecture + cmd, install_dir, proxy_name, client_operating_system, client_architecture ) _check_proxy_installation(install_dir, proxy_name, debug) @@ -64,9 +66,23 @@ def install_client_side_proxy( def _download_proxy_from_MCR( - dest_dir: str, proxy_name: str, operating_system: str, architecture: str + cmd: CLICommand, dest_dir: str, proxy_name: str, operating_system: str, architecture: str ) -> None: - mar_target = f"{consts.CLIENT_PROXY_MCR_TARGET}/{operating_system.lower()}/{architecture}/arc-proxy" + + active_directory_array = cmd.cli_ctx.cloud.endpoints.active_directory.split(".") + + # default for public, mc, ff clouds + mcr_postfix = active_directory_array[2] + # special cases for USSec, exclude part of suffix + if len(active_directory_array) == 4 and active_directory_array[2] == "microsoft": + mcr_postfix = active_directory_array[3] + # special case for USNat + elif len(active_directory_array) == 5: + mcr_postfix = active_directory_array[2] + "." + active_directory_array[3] + "." + active_directory_array[4] + + mcr_url = f"mcr.microsoft.{mcr_postfix}" + + mar_target = f"{mcr_url}/{consts.CLIENT_PROXY_MCR_TARGET}/{operating_system.lower()}/{architecture}/arc-proxy" logger.debug( "Downloading Arc Connectivity Proxy from %s in Microsoft Artifact Regristy.", mar_target, diff --git a/src/connectedk8s/azext_connectedk8s/clientproxyhelper/_proxylogic.py b/src/connectedk8s/azext_connectedk8s/clientproxyhelper/_proxylogic.py index 71345064af6..f72074d1b6e 100644 --- a/src/connectedk8s/azext_connectedk8s/clientproxyhelper/_proxylogic.py +++ b/src/connectedk8s/azext_connectedk8s/clientproxyhelper/_proxylogic.py @@ -18,7 +18,7 @@ if TYPE_CHECKING: from subprocess import Popen - from knack.commands import CLICommmand + from knack.commands import CLICommand from requests.models import Response from azext_connectedk8s.vendored_sdks.preview_2024_07_01.models import ( @@ -30,7 +30,7 @@ def handle_post_at_to_csp( - cmd: CLICommmand, + cmd: CLICommand, api_server_port: int, tenant_id: str, clientproxy_process: Popen[bytes], diff --git a/src/connectedk8s/azext_connectedk8s/custom.py b/src/connectedk8s/azext_connectedk8s/custom.py index 8f481f1111b..241f2ca6154 100644 --- a/src/connectedk8s/azext_connectedk8s/custom.py +++ b/src/connectedk8s/azext_connectedk8s/custom.py @@ -81,7 +81,7 @@ from azure.cli.core.commands import AzCliCommand from azure.core.polling import LROPoller from Crypto.PublicKey.RSA import RsaKey - from knack.commands import CLICommmand + from knack.commands import CLICommand from kubernetes.client import V1NodeList from kubernetes.config.kube_config import ConfigNode from requests.models import Response @@ -99,7 +99,7 @@ def create_connectedk8s( - cmd: CLICommmand, + cmd: CLICommand, client: ConnectedClusterOperations, resource_group_name: str, cluster_name: str, @@ -301,7 +301,7 @@ def create_connectedk8s( # Install kubectl and helm try: kubectl_client_location = install_kubectl_client() - helm_client_location = install_helm_client() + helm_client_location = install_helm_client(cmd) except Exception as e: raise CLIInternalError( f"An exception has occured while trying to perform kubectl or helm install: {e}" @@ -344,6 +344,7 @@ def create_connectedk8s( # Performing cluster-diagnostic-checks diagnostic_checks, storage_space_available = ( precheckutils.fetch_diagnostic_checks_results( + cmd, api_instance, batchv1_api_instance, helm_client_location, @@ -742,7 +743,7 @@ def create_connectedk8s( "Cleaning up the stale arc agents present on the cluster before starting new onboarding." ) # Explicit CRD Deletion - crd_cleanup_force_delete(kubectl_client_location, kube_config, kube_context) + crd_cleanup_force_delete(cmd, kubectl_client_location, kube_config, kube_context) # Cleaning up the cluster utils.delete_arc_agents( release_namespace, @@ -773,7 +774,7 @@ def create_connectedk8s( raise ArgumentUsageError(err_msg, recommendation=reco_msg) # cleanup of stuck CRD if release namespace is not present/deleted - crd_cleanup_force_delete(kubectl_client_location, kube_config, kube_context) + crd_cleanup_force_delete(cmd, kubectl_client_location, kube_config, kube_context) print( f"Step: {utils.get_utctimestring()}: Check if ResourceGroup exists. Try to create if it doesn't" @@ -1043,7 +1044,7 @@ def validate_existing_provisioned_cluster_for_reput( raise InvalidArgumentValueError(err_msg) -def send_cloud_telemetry(cmd: CLICommmand) -> str: +def send_cloud_telemetry(cmd: CLICommand) -> str: telemetry.add_extension_event( "connectedk8s", {"Context.Default.AzureCLI.AzureCloud": cmd.cli_ctx.cloud.name} ) @@ -1153,7 +1154,7 @@ def check_kube_connection() -> str: assert False -def install_helm_client() -> str: +def install_helm_client(cmd: CLICommand) -> str: print( f"Step: {utils.get_utctimestring()}: Install Helm client if it does not exist" ) @@ -1219,13 +1220,26 @@ def install_helm_client() -> str: logger.warning( "Downloading helm client for first time. This can take few minutes..." ) + active_directory_array = cmd.cli_ctx.cloud.endpoints.active_directory.split(".") + + # default for public, mc, ff clouds + mcr_postfix = active_directory_array[2] + # special cases for USSec, exclude part of suffix + if len(active_directory_array) == 4 and active_directory_array[2] == "microsoft": + mcr_postfix = active_directory_array[3] + # special case for USNat + elif len(active_directory_array) == 5: + mcr_postfix = active_directory_array[2] + "." + active_directory_array[3] + "." + active_directory_array[4] + + mcr_url = f"mcr.microsoft.{mcr_postfix}" + client = oras.client.OrasClient() retry_count = 3 retry_delay = 5 for i in range(retry_count): try: client.pull( - target=f"{consts.HELM_MCR_URL}:{artifactTag}", + target=f"{mcr_url}/{consts.HELM_MCR_URL}:{artifactTag}", outdir=download_location, ) break @@ -1289,8 +1303,16 @@ def connected_cluster_exists( return True -def get_default_config_dp_endpoint(cmd: CLICommmand, location: str) -> str: - cloud_based_domain = cmd.cli_ctx.cloud.endpoints.active_directory.split(".")[2] +def get_default_config_dp_endpoint(cmd: CLICommand, location: str) -> str: + active_directory_array = cmd.cli_ctx.cloud.endpoints.active_directory.split(".") + # default for public, mc, ff clouds + cloud_based_domain = active_directory_array[2] + # special cases for USSec/USNat clouds + if len(active_directory_array) == 4: + cloud_based_domain = active_directory_array[2] + "." + active_directory_array[3] + elif len(active_directory_array) == 5: + cloud_based_domain = active_directory_array[2] + "." + active_directory_array[3] + "." + active_directory_array[4] + config_dp_endpoint = ( f"https://{location}.dp.kubernetesconfiguration.azure.{cloud_based_domain}" ) @@ -1298,7 +1320,7 @@ def get_default_config_dp_endpoint(cmd: CLICommmand, location: str) -> str: def get_config_dp_endpoint( - cmd: CLICommmand, + cmd: CLICommand, location: str, values_file: str | None, arm_metadata: dict[str, Any] | None = None, @@ -1733,7 +1755,7 @@ def list_connectedk8s( def delete_connectedk8s( - cmd: CLICommmand, + cmd: CLICommand, client: ConnectedClusterOperations, resource_group_name: str, cluster_name: str, @@ -1785,7 +1807,7 @@ def delete_connectedk8s( check_kube_connection() # Install helm client - helm_client_location = install_helm_client() + helm_client_location = install_helm_client(cmd) # Check Release Existance release_namespace = utils.get_release_namespace( @@ -1805,7 +1827,7 @@ def delete_connectedk8s( delete_cc_resource(client, resource_group_name, cluster_name, no_wait).result() # Explicit CRD Deletion - crd_cleanup_force_delete(kubectl_client_location, kube_config, kube_context) + crd_cleanup_force_delete(cmd, kubectl_client_location, kube_config, kube_context) if release_namespace: utils.delete_arc_agents( @@ -1995,7 +2017,7 @@ def update_connected_cluster_internal( def update_connected_cluster( - cmd: CLICommmand, + cmd: CLICommand, client: ConnectedClusterOperations, resource_group_name: str, cluster_name: str, @@ -2158,7 +2180,7 @@ def update_connected_cluster( kubernetes_version = check_kube_connection() # Install helm client - helm_client_location = install_helm_client() + helm_client_location = install_helm_client(cmd) release_namespace = validate_release_namespace( client, @@ -2351,7 +2373,7 @@ def update_connected_cluster( def upgrade_agents( - cmd: CLICommmand, + cmd: CLICommand, client: ConnectedClusterOperations, resource_group_name: str, cluster_name: str, @@ -2396,7 +2418,7 @@ def upgrade_agents( api_instance = kube_client.CoreV1Api() # Install helm client - helm_client_location = install_helm_client() + helm_client_location = install_helm_client(cmd) # Check Release Existence release_namespace = utils.get_release_namespace( @@ -2797,7 +2819,7 @@ def get_all_helm_values( def enable_features( - cmd: CLICommmand, + cmd: CLICommand, client: ConnectedClusterOperations, resource_group_name: str, cluster_name: str, @@ -2892,7 +2914,7 @@ def enable_features( kubernetes_version = check_kube_connection() # Install helm client - helm_client_location = install_helm_client() + helm_client_location = install_helm_client(cmd) release_namespace = validate_release_namespace( client, @@ -3030,7 +3052,7 @@ def enable_features( def disable_features( - cmd: CLICommmand, + cmd: CLICommand, client: ConnectedClusterOperations, resource_group_name: str, cluster_name: str, @@ -3085,7 +3107,7 @@ def disable_features( kubernetes_version = check_kube_connection() # Install helm client - helm_client_location = install_helm_client() + helm_client_location = install_helm_client(cmd) release_namespace = validate_release_namespace( client, @@ -3169,7 +3191,7 @@ def disable_features( def get_chart_and_disable_features( - cmd: CLICommmand, + cmd: CLICommand, connected_cluster: ConnectedCluster, kube_config: str | None, kube_context: str | None, @@ -3260,7 +3282,7 @@ def get_chart_and_disable_features( def disable_cluster_connect( - cmd: CLICommmand, + cmd: CLICommand, client: ConnectedClusterOperations, resource_group_name: str, cluster_name: str, @@ -3467,7 +3489,7 @@ def handle_merge( def client_side_proxy_wrapper( - cmd: CLICommmand, + cmd: CLICommand, client: ConnectedClusterOperations, resource_group_name: str, cluster_name: str, @@ -3535,7 +3557,7 @@ def client_side_proxy_wrapper( if "--debug" in cmd.cli_ctx.data["safe_params"]: debug_mode = True - install_location = proxybinaryutils.install_client_side_proxy(None, debug_mode) + install_location = proxybinaryutils.install_client_side_proxy(cmd, debug_mode) args.append(install_location) install_dir = os.path.dirname(install_location) @@ -3638,7 +3660,7 @@ def client_side_proxy_wrapper( def client_side_proxy_main( - cmd: CLICommmand, + cmd: CLICommand, tenant_id: str, client: ConnectedClusterOperations, resource_group_name: str, @@ -3709,7 +3731,7 @@ def client_side_proxy_main( def client_side_proxy( - cmd: CLICommmand, + cmd: CLICommand, tenant_id: str, client: ConnectedClusterOperations, resource_group_name: str, @@ -3842,7 +3864,7 @@ def client_side_proxy( def check_cl_registration_and_get_oid( - cmd: CLICommmand, cl_oid: str | None, subscription_id: str | None + cmd: CLICommand, cl_oid: str | None, subscription_id: str | None ) -> tuple[bool, str]: print( f"Step: {utils.get_utctimestring()}: Checking Custom Location(Microsoft.ExtendedLocation) RP Registration state for this Subscription, and attempt to get the Custom Location Object ID (OID),if registered" @@ -3881,7 +3903,7 @@ def check_cl_registration_and_get_oid( return enable_custom_locations, custom_locations_oid -def get_custom_locations_oid(cmd: CLICommmand, cl_oid: str | None) -> str: +def get_custom_locations_oid(cmd: CLICommand, cl_oid: str | None) -> str: try: graph_client = graph_client_factory(cmd.cli_ctx) app_id = "bc313c14-388c-4e7d-a58e-70017303ee3b" @@ -3942,7 +3964,7 @@ def get_custom_locations_oid(cmd: CLICommmand, cl_oid: str | None) -> str: def troubleshoot( - cmd: CLICommmand, + cmd: CLICommand, client: ConnectedClusterOperations, resource_group_name: str, cluster_name: str, @@ -3985,7 +4007,7 @@ def troubleshoot( load_kube_config(kube_config, kube_context, skip_ssl_verification) # Install helm client - helm_client_location = install_helm_client() + helm_client_location = install_helm_client(cmd) # Install kubectl client kubectl_client_location = install_kubectl_client() @@ -4392,16 +4414,27 @@ def install_kubectl_client() -> str: def crd_cleanup_force_delete( - kubectl_client_location: str, kube_config: str | None, kube_context: str | None + cmd: CLICommand, kubectl_client_location: str, kube_config: str | None, kube_context: str | None ) -> None: print(f"Step: {utils.get_utctimestring()}: Deleting Arc CRDs") + + active_directory_array = cmd.cli_ctx.cloud.endpoints.active_directory.split(".") + # default for public, mc, ff clouds + cloud_based_domain = active_directory_array[2] + # special cases for USSec/USNat clouds + if len(active_directory_array) == 4: + cloud_based_domain = active_directory_array[2] + "." + active_directory_array[3] + elif len(active_directory_array) == 5: + cloud_based_domain = active_directory_array[2] + "." + active_directory_array[3] + "." + active_directory_array[4] + timeout_for_crd_deletion = "20s" for crds in consts.CRD_FOR_FORCE_DELETE: + full_crds = f"{crds}.{cloud_based_domain}" cmd_helm_delete = [ kubectl_client_location, "delete", "crds", - crds, + full_crds, "--ignore-not-found", "--wait", "--timeout", @@ -4424,7 +4457,8 @@ def crd_cleanup_force_delete( # Patch if CRD is in Terminating state for crds in consts.CRD_FOR_FORCE_DELETE: - cmd = [kubectl_client_location, "get", "crd", crds, "-ojson"] + full_crds = f"{crds}.{cloud_based_domain}" + cmd = [kubectl_client_location, "get", "crd", full_crds, "-ojson"] if kube_config: cmd.extend(["--kubeconfig", kube_config]) if kube_context: @@ -4441,7 +4475,7 @@ def crd_cleanup_force_delete( kubectl_client_location, "patch", "crd", - crds, + full_crds, "--type=merge", "--patch-file", yaml_file_path, diff --git a/src/connectedk8s/azext_connectedk8s/tests/latest/test_connectedk8s_scenario.py b/src/connectedk8s/azext_connectedk8s/tests/latest/test_connectedk8s_scenario.py index 9d899e786da..1210aa7bc53 100644 --- a/src/connectedk8s/azext_connectedk8s/tests/latest/test_connectedk8s_scenario.py +++ b/src/connectedk8s/azext_connectedk8s/tests/latest/test_connectedk8s_scenario.py @@ -113,7 +113,7 @@ def install_helm_client(): client = oras.client.OrasClient() try: client.pull( - target=f"{consts.HELM_MCR_URL}:{artifactTag}", outdir=download_location + target=f"mcr.microsoft.com/{consts.HELM_MCR_URL}:{artifactTag}", outdir=download_location ) except Exception as e: logger.warning("Failed to download helm client." + str(e)) From ad88b47816cc812cc5af4d394c1bae96e7965ce4 Mon Sep 17 00:00:00 2001 From: Bavneet Singh Date: Tue, 18 Mar 2025 15:11:00 -0700 Subject: [PATCH 41/42] add pester tests for connectedk8s cli extension --- testing/.gitignore | 9 + testing/Bootstrap.ps1 | 30 ++ testing/README.md | 116 ++++++ testing/Test.ps1 | 99 +++++ .../bin/connectedk8s-1.0.0-py3-none-any.whl | Bin 0 -> 62802 bytes testing/bin/connectedk8s-values.yaml | 3 + .../k8s_configuration-1.0.0-py3-none-any.whl | Bin 0 -> 42351 bytes .../bin/k8s_extension-0.3.0-py3-none-any.whl | Bin 0 -> 52893 bytes testing/owners.txt | 2 + testing/pipeline/k8s-custom-pipelines.yml | 374 ++++++++++++++++++ testing/pipeline/templates/run-test.yml | 112 ++++++ testing/settings.template.json | 12 + .../test/configurations/AutoUpdate.Tests.ps1 | 62 +++ .../configurations/BasicOnboarding.Tests.ps1 | 62 +++ .../configurations/ConnectProxy.Tests.ps1 | 98 +++++ testing/test/configurations/Gateway.Tests.ps1 | 116 ++++++ testing/test/configurations/Proxy.Tests.ps1 | 65 +++ .../configurations/Troubleshoot.Tests.ps1 | 40 ++ .../configurations/WorkloadIdentity.Tests.ps1 | 239 +++++++++++ testing/test/helper/Constants.ps1 | 5 + 20 files changed, 1444 insertions(+) create mode 100644 testing/.gitignore create mode 100644 testing/Bootstrap.ps1 create mode 100644 testing/README.md create mode 100644 testing/Test.ps1 create mode 100644 testing/bin/connectedk8s-1.0.0-py3-none-any.whl create mode 100644 testing/bin/connectedk8s-values.yaml create mode 100644 testing/bin/k8s_configuration-1.0.0-py3-none-any.whl create mode 100644 testing/bin/k8s_extension-0.3.0-py3-none-any.whl create mode 100644 testing/owners.txt create mode 100644 testing/pipeline/k8s-custom-pipelines.yml create mode 100644 testing/pipeline/templates/run-test.yml create mode 100644 testing/settings.template.json create mode 100644 testing/test/configurations/AutoUpdate.Tests.ps1 create mode 100644 testing/test/configurations/BasicOnboarding.Tests.ps1 create mode 100644 testing/test/configurations/ConnectProxy.Tests.ps1 create mode 100644 testing/test/configurations/Gateway.Tests.ps1 create mode 100644 testing/test/configurations/Proxy.Tests.ps1 create mode 100644 testing/test/configurations/Troubleshoot.Tests.ps1 create mode 100644 testing/test/configurations/WorkloadIdentity.Tests.ps1 create mode 100644 testing/test/helper/Constants.ps1 diff --git a/testing/.gitignore b/testing/.gitignore new file mode 100644 index 00000000000..29f33294b8b --- /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.3.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..ad21cfddad2 --- /dev/null +++ b/testing/Bootstrap.ps1 @@ -0,0 +1,30 @@ +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 +} + +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 + } +} + + +Copy-Item $HOME/.kube/config -Destination $PSScriptRoot/tmp/KUBECONFIG \ No newline at end of file diff --git a/testing/README.md b/testing/README.md new file mode 100644 index 00000000000..33f12b5b1a3 --- /dev/null +++ b/testing/README.md @@ -0,0 +1,116 @@ +# 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: 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 4: 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 5: 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 6: 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..7c6f522d082 --- /dev/null +++ b/testing/Test.ps1 @@ -0,0 +1,99 @@ +param ( + [string] $Path, + [switch] $SkipInstall, + [switch] $CI, + [switch] $ParallelCI, + [switch] $OnlyPublicTests, + + [Parameter(Mandatory=$True)] + [ValidateSet('connectedk8s')] + [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 + +# Install the powershell-yaml module +# Needed to parse the kubeconfig file +Install-Module -Name powershell-yaml -Force -Scope CurrentUser + +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 'connectedk8s') { + $connectedk8sVersion = $ENVCONFIG.extensionVersion.'connectedk8s' + if (!$SkipInstall) { + Write-Host "Removing the old connectedk8s extension..." + az extension remove -n connectedk8s + Write-Host "Installing connectedk8s version $connectedk8sVersion..." + az extension add --source ./bin/connectedk8s-$connectedk8sVersion-py2.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 + } +} \ No newline at end of file diff --git a/testing/bin/connectedk8s-1.0.0-py3-none-any.whl b/testing/bin/connectedk8s-1.0.0-py3-none-any.whl new file mode 100644 index 0000000000000000000000000000000000000000..08f34250036f455aad7e3e820c65d08d790e1201 GIT binary patch literal 62802 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

YVX(toUj*0P8con1X*tXmm22nWyt~gf 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

4OLf9N*^x0^yG1@+&DtXglO();gI7nFsJg6 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 b*Hx{YBsky%^Yh?A4x|a#NYixu{O$h(4U{Xp literal 0 HcmV?d00001 diff --git a/testing/owners.txt b/testing/owners.txt new file mode 100644 index 00000000000..c1bbe9a9e5c --- /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..712d6f67762 --- /dev/null +++ b/testing/pipeline/k8s-custom-pipelines.yml @@ -0,0 +1,374 @@ +resources: +- repo: self + +trigger: + batch: true + branches: + include: + - 'main' + +pr: + branches: + include: + - '*' + +stages: +- stage: BuildTestPublishExtension + displayName: "Build, Test, and Publish Extension" + variables: + TEST_PATH: $(Agent.BuildDirectory)/s/testing + CLI_REPO_PATH: $(Agent.BuildDirectory)/s + EXTENSION_NAME: "connectedk8s" + EXTENSION_FILE_NAME: "connectedk8s" + SUBSCRIPTION_ID: "15c06b1b-01d6-407b-bb21-740b8617dea3" + RESOURCE_GROUP: "K8sPartnerExtensionTest" + BASE_CLUSTER_NAME: "connectedk8s-cluster" + jobs: + - template: ./templates/run-test.yml + parameters: + jobName: BasicOnboardingTest + path: ./test/configurations/BasicOnboarding.Tests.ps1 + - template: ./templates/run-test.yml + parameters: + jobName: AutoUpdateTest + path: ./test/configurations/AutoUpdate.Tests.ps1 + - template: ./templates/run-test.yml + parameters: + jobName: ProxyTest + path: ./test/configurations/Proxy.Tests.ps1 + - template: ./templates/run-test.yml + parameters: + jobName: GatewayTest + path: ./test/configurations/Gateway.Tests.ps1 + - template: ./templates/run-test.yml + parameters: + jobName: WorkloadIdentityTest + path: ./test/configurations/WorkloadIdentity.Tests.ps1 + - template: ./templates/run-test.yml + parameters: + jobName: TroubleshootTest + path: ./test/configurations/Troubleshoot.Tests.ps1 + - template: ./templates/run-test.yml + parameters: + jobName: Connectedk8sProxyTest + path: ./test/configurations/ConnectProxy.Tests.ps1 + - job: BuildPublishExtension + pool: + vmImage: 'ubuntu-20.04' + displayName: "Build and Publish the Extension Artifact" + variables: + CLI_REPO_PATH: $(Agent.BuildDirectory)/s + EXTENSION_NAME: "connectedk8s" + steps: + - 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 + pip install --upgrade pip + pip install azdev + + ls $(CLI_REPO_PATH) + + azdev --version + azdev setup -r $(CLI_REPO_PATH) -e $(EXTENSION_NAME) + azdev extension build $(EXTENSION_NAME) + workingDirectory: $(CLI_REPO_PATH) + displayName: "Setup and Build Extension with azdev" + - 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: 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: UnitTests + displayName: "Unit Tests" + pool: + vmImage: 'ubuntu-latest' + steps: + - task: UsePythonVersion@0 + displayName: 'Use Python 3.12' + inputs: + versionSpec: '3.12' + - 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 connectedk8s + current_dir=$(pwd) + echo "Current directory: $current_dir" + pip install pytest + cd /home/vsts/work/1/s/src/connectedk8s/azext_connectedk8s/tests/unittests + pytest --junitxml=test-results.xml + + displayName: 'Run UnitTests test' + - task: PublishTestResults@2 + inputs: + testResultsFormat: 'JUnit' + testResultsFiles: '**/test-results.xml' + failTaskOnFailedTests: true + - job: SourceTests + displayName: "Integration Tests, Build Tests" + pool: + vmImage: 'ubuntu-latest' + strategy: + matrix: + Python39: + python.version: '3.9' + Python310: + python.version: '3.10' + Python311: + python.version: '3.11' + Python312: + python.version: '3.12' + 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 connectedk8s + azdev test connectedk8s + displayName: 'Run integration test and build test' + + - job: AzdevLinterModifiedExtensions + displayName: "azdev linter on Modified Extensions" + pool: + vmImage: 'ubuntu-latest' + steps: + - task: UsePythonVersion@0 + displayName: 'Use Python 3.12' + inputs: + versionSpec: 3.12 + - 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 connectedk8s + # Installing setuptools with a version higher than 70.0.0 will not generate metadata.json + pip install setuptools==70.0.0 + pip list -v + + # 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 connectedk8s + displayName: "CLI Linter on Modified Extension" + env: + ADO_PULL_REQUEST_LATEST_COMMIT: $(System.PullRequest.SourceCommitId) + ADO_PULL_REQUEST_TARGET_BRANCH: $(System.PullRequest.TargetBranch) + + - job: AzdevStyleModifiedExtensions + displayName: "azdev style on Modified Extensions" + continueOnError: true + pool: + vmImage: 'ubuntu-latest' + steps: + - task: UsePythonVersion@0 + displayName: 'Use Python 3.12' + inputs: + versionSpec: 3.12 + - 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 azdev + + azdev --version + + azdev setup -c ../azure-cli -r ./ -e connectedk8s + # Installing setuptools with a version higher than 70.0.0 will not generate metadata.json + pip install setuptools==70.0.0 + pip list -v + az --version + + # overwrite the default AZURE_EXTENSION_DIR set by ADO + AZURE_EXTENSION_DIR=~/.azure/cliextensions az --version + + AZURE_EXTENSION_DIR=~/.azure/cliextensions azdev style connectedk8s + displayName: "azdev style on Modified Extensions" + env: + ADO_PULL_REQUEST_LATEST_COMMIT: $(System.PullRequest.SourceCommitId) + ADO_PULL_REQUEST_TARGET_BRANCH: $(System.PullRequest.TargetBranch) + + - job: RuffCheck + displayName: "Lint connectedk8s with ruff check" + pool: + vmImage: 'ubuntu-latest' + steps: + - task: UsePythonVersion@0 + displayName: 'Use Python 3.12' + inputs: + versionSpec: 3.12 + - bash: | + set -ev + + # prepare and activate virtualenv + cd src/connectedk8s + python -m venv env + source ./env/bin/activate + + pip install --upgrade pip + pip install azure-cli --editable .[linting] + + ruff check + + displayName: "ruff check" + + - job: RuffFormat + displayName: "Check connected8ks formatting with ruff" + pool: + vmImage: 'ubuntu-latest' + steps: + - task: UsePythonVersion@0 + displayName: 'Use Python 3.12' + inputs: + versionSpec: 3.12 + - bash: | + set -ev + + # prepare and activate virtualenv + cd src/connectedk8s + python -m venv env + source ./env/bin/activate + + pip install --upgrade pip + pip install azure-cli --editable .[linting] + + ruff format --check + + displayName: "ruff format check" + + - job: TypeChecking + displayName: "Typecheck connected8ks with mypy" + pool: + vmImage: 'ubuntu-latest' + steps: + - task: UsePythonVersion@0 + displayName: 'Use Python 3.12' + inputs: + versionSpec: 3.12 + - bash: | + set -ev + + # prepare and activate virtualenv + cd src/connectedk8s + python -m venv env + source ./env/bin/activate + + pip install --upgrade pip + pip install azure-cli --editable .[linting] + + mypy + + displayName: "mypy" diff --git a/testing/pipeline/templates/run-test.yml b/testing/pipeline/templates/run-test.yml new file mode 100644 index 00000000000..f1d42ae9714 --- /dev/null +++ b/testing/pipeline/templates/run-test.yml @@ -0,0 +1,112 @@ +parameters: + jobName: '' + path: '' + +jobs: +- job: ${{ parameters.jobName}} + pool: + vmImage: 'ubuntu-20.04' + 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" + + - 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 $(CLI_REPO_PATH) + azdev --version + azdev setup -c ../azure-cli -r $(CLI_REPO_PATH) -e $(EXTENSION_NAME) + azdev extension build $(EXTENSION_NAME) + workingDirectory: $(CLI_REPO_PATH) + displayName: "Setup and Build Extension with azdev" + + - bash: | + K8S_CONFIG_VERSION=$(ls ${EXTENSION_FILE_NAME}* | cut -d "-" -f2) + echo "##vso[task.setvariable variable=K8S_CONFIG_VERSION]$K8S_CONFIG_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_CONFIG_VERSION "$K8S_CONFIG_VERSION" \ + '{subscriptionId: $SUB_ID, resourceGroup: $RG, aksClusterName: $AKS_CLUSTER_NAME, arcClusterName: $ARC_CLUSTER_NAME, extensionVersion: {"connectedk8s": $K8S_CONFIG_VERSION}}') + 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.24.0/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" + + - bash: | + 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 + displayName: "Install Helm" + + - 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 connectedk8s + workingDirectory: $(TEST_PATH) + continueOnError: true + + - task: PublishTestResults@2 + inputs: + testResultsFormat: 'JUnit' + testResultsFiles: '**/testing/results/*.xml' + failTaskOnFailedTests: true + condition: succeededOrFailed() diff --git a/testing/settings.template.json b/testing/settings.template.json new file mode 100644 index 00000000000..657126c20aa --- /dev/null +++ b/testing/settings.template.json @@ -0,0 +1,12 @@ +{ + "subscriptionId": "", + "resourceGroup": "", + "aksClusterName": "", + "arcClusterName": "", + + "extensionVersion": { + "k8s-extension": "0.3.0", + "k8s-extension-private": "0.1.0", + "connectedk8s": "1.0.0" + } +} \ No newline at end of file diff --git a/testing/test/configurations/AutoUpdate.Tests.ps1 b/testing/test/configurations/AutoUpdate.Tests.ps1 new file mode 100644 index 00000000000..d55029ceeb8 --- /dev/null +++ b/testing/test/configurations/AutoUpdate.Tests.ps1 @@ -0,0 +1,62 @@ +Describe 'Auto Upgrade Scenario' { + BeforeAll { + . $PSScriptRoot/../helper/Constants.ps1 + } + + It 'Check if basic onboarding works with auto-upgrade disabled' { + az connectedk8s connect -n $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup -l $ARC_LOCATION --disable-auto-upgrade --no-wait + $? | Should -BeTrue + Start-Sleep -Seconds 10 + + # Loop and retry until the configuration installs + $n = 0 + do + { + $output = az connectedk8s show -n $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup + $jsonOutput = [System.Text.Json.JsonDocument]::Parse($output) + $provisioningState = ($output | ConvertFrom-Json).provisioningState + $autoUpdate = $jsonOutput.RootElement.GetProperty("arcAgentProfile").GetProperty("agentAutoUpgrade").GetString() + Write-Host "Provisioning State: $provisioningState" + Write-Host "Auto Update: $autoUpdate" + if ($provisioningState -eq $SUCCEEDED -and $autoUpdate -eq "Disabled") { + break + } + Start-Sleep -Seconds 10 + $n += 1 + } while ($n -le $MAX_RETRY_ATTEMPTS) + $n | Should -BeLessOrEqual $MAX_RETRY_ATTEMPTS + } + + It 'Enable auto-upgrade using update cmd' { + az connectedk8s update -n $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --auto-upgrade true + $? | Should -BeTrue + Start-Sleep -Seconds 10 + + # Loop and retry until the configuration installs + $n = 0 + do + { + $output = az connectedk8s show -n $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup + $jsonOutput = [System.Text.Json.JsonDocument]::Parse($output) + $provisioningState = ($output | ConvertFrom-Json).provisioningState + $autoUpdate = $jsonOutput.RootElement.GetProperty("arcAgentProfile").GetProperty("agentAutoUpgrade").GetString() + Write-Host "Provisioning State: $provisioningState" + Write-Host "Auto Update: $autoUpdate" + if ($provisioningState -eq $SUCCEEDED -and $autoUpdate -eq "Enabled") { + break + } + Start-Sleep -Seconds 10 + $n += 1 + } while ($n -le $MAX_RETRY_ATTEMPTS) + $n | Should -BeLessOrEqual $MAX_RETRY_ATTEMPTS + } + + It "Delete the connected instance" { + az connectedk8s delete -n $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --force -y + $? | Should -BeTrue + + # Configuration should be removed from the resource model + az connectedk8s show -n $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup + $? | Should -BeFalse + } +} \ No newline at end of file diff --git a/testing/test/configurations/BasicOnboarding.Tests.ps1 b/testing/test/configurations/BasicOnboarding.Tests.ps1 new file mode 100644 index 00000000000..541327682c0 --- /dev/null +++ b/testing/test/configurations/BasicOnboarding.Tests.ps1 @@ -0,0 +1,62 @@ +Describe 'Basic Onboarding Scenario' { + BeforeAll { + . $PSScriptRoot/../helper/Constants.ps1 + } + + It 'Check if basic onboarding works correctly' { + az connectedk8s connect -n $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup -l $ARC_LOCATION --no-wait + $? | Should -BeTrue + Start-Sleep -Seconds 10 + + # Loop and retry until the configuration installs + $n = 0 + do + { + $output = az connectedk8s show -n $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup + $jsonOutput = [System.Text.Json.JsonDocument]::Parse($output) + $provisioningState = ($output | ConvertFrom-Json).provisioningState + $autoUpdate = $jsonOutput.RootElement.GetProperty("arcAgentProfile").GetProperty("agentAutoUpgrade").GetString() + Write-Host "Provisioning State: $provisioningState" + Write-Host "Auto Update: $autoUpdate" + if ($provisioningState -eq $SUCCEEDED -and $autoUpdate -eq "Enabled") { + break + } + Start-Sleep -Seconds 10 + $n += 1 + } while ($n -le $MAX_RETRY_ATTEMPTS) + $n | Should -BeLessOrEqual $MAX_RETRY_ATTEMPTS + } + + It 'Disable auto-upgrade' { + az connectedk8s update -n $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --auto-upgrade false + $? | Should -BeTrue + Start-Sleep -Seconds 10 + + # Loop and retry until the configuration installs + $n = 0 + do + { + $output = az connectedk8s show -n $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup + $jsonOutput = [System.Text.Json.JsonDocument]::Parse($output) + $provisioningState = ($output | ConvertFrom-Json).provisioningState + $autoUpdate = $jsonOutput.RootElement.GetProperty("arcAgentProfile").GetProperty("agentAutoUpgrade").GetString() + Write-Host "Provisioning State: $provisioningState" + Write-Host "Auto Update: $autoUpdate" + if ($provisioningState -eq $SUCCEEDED -and $autoUpdate -eq "Disabled") { + break + } + Start-Sleep -Seconds 10 + $n += 1 + } while ($n -le $MAX_RETRY_ATTEMPTS) + $n | Should -BeLessOrEqual $MAX_RETRY_ATTEMPTS + } + + It "Delete the connected instance" { + az connectedk8s delete -n $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --force -y + $? | Should -BeTrue + + # Configuration should be removed from the resource model + az connectedk8s show -n $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup + $? | Should -BeFalse + } +} \ No newline at end of file diff --git a/testing/test/configurations/ConnectProxy.Tests.ps1 b/testing/test/configurations/ConnectProxy.Tests.ps1 new file mode 100644 index 00000000000..4de00bbeba0 --- /dev/null +++ b/testing/test/configurations/ConnectProxy.Tests.ps1 @@ -0,0 +1,98 @@ +Describe 'Connectedk8s Proxy Scenario' { + BeforeAll { + . $PSScriptRoot/../helper/Constants.ps1 + } + + It 'Check if basic onboarding works correctly' { + az connectedk8s connect -n $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup -l $ARC_LOCATION --no-wait + $? | Should -BeTrue + Start-Sleep -Seconds 10 + + # Loop and retry until the configuration installs + $n = 0 + do + { + $output = az connectedk8s show -n $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup + $provisioningState = ($output | ConvertFrom-Json).provisioningState + Write-Host "Provisioning State: $provisioningState" + if ($provisioningState -eq $SUCCEEDED) { + break + } + Start-Sleep -Seconds 10 + $n += 1 + } while ($n -le $MAX_RETRY_ATTEMPTS) + $n | Should -BeLessOrEqual $MAX_RETRY_ATTEMPTS + } + + It 'Connectedk8s proxy test with non-empty kubeconfig' { + # Start the proxy command as a background job + $proxyJob = Start-Job -ScriptBlock { + param($ClusterName, $ResourceGroup) + + # Capture output and errors + try { + $output = az connectedk8s proxy -n $ClusterName -g $ResourceGroup 2>&1 + return @{ Success = $LASTEXITCODE -eq 0; Output = $output } + } catch { + return @{ Success = $false; Output = $_.Exception.Message } + } + } -ArgumentList $ENVCONFIG.arcClusterName, $ENVCONFIG.resourceGroup + + # Wait for a certain amount of time (e.g., 30 seconds) + Start-Sleep -Seconds 60 + + # Display the output + Write-Host "Proxy Job State: $($proxyJob.State)" + + # Check if the job ran successfully + $proxyJob.State | Should -Be 'Running' + + # Check if the kubeconfig file has been updated to use the proxy + $kubeconfigPath = "~/.kube/config" + $kubeconfig = Get-Content $kubeconfigPath -Raw | ConvertFrom-Yaml + # Extract the current context + $currentContext = $kubeconfig.'current-context' + + # Validate that the current context is for the arc machine + $currentContext | Should -Be $ENVCONFIG.arcClusterName + + # Find the cluster associated with the current context + $context = $kubeconfig.contexts | Where-Object { $_.name -eq $currentContext } + $clusterName = $context.context.cluster + + # Retrieve the server URL for the cluster + $cluster = $kubeconfig.clusters | Where-Object { $_.name -eq $clusterName } + $server = $cluster.cluster.server + + # Validate the server URL + $server | Should -Match "^https://127.0.0.1:47011/proxies/" + + # Check if the proxy command ran successfully + $kubectlJob = Start-Job -ScriptBlock { + try { + $output = kubectl get pods -n azure-arc 2>&1 + return @{ Success = $LASTEXITCODE -eq 0; Output = $output } + } catch { + return @{ Success = $false; Output = $_.Exception.Message } + } + } + + $kubectlJob | Wait-Job + $kubectlResult = Receive-Job -Job $kubectlJob + + # Assert that the result is 0 + $kubectlResult.Success | Should -BeTrue + + Stop-Job -Job $proxyJob + Remove-Job -Job $proxyJob + } + + It "Delete the connected instance" { + az connectedk8s delete -n $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --force -y + $? | Should -BeTrue + + # Configuration should be removed from the resource model + az connectedk8s show -n $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup + $? | Should -BeFalse + } +} \ No newline at end of file diff --git a/testing/test/configurations/Gateway.Tests.ps1 b/testing/test/configurations/Gateway.Tests.ps1 new file mode 100644 index 00000000000..37dab0eccc9 --- /dev/null +++ b/testing/test/configurations/Gateway.Tests.ps1 @@ -0,0 +1,116 @@ +Describe 'Onboarding with Gateway Scenario' { + BeforeAll { + . $PSScriptRoot/../helper/Constants.ps1 + + $gatewayResourceId = "/subscriptions/15c06b1b-01d6-407b-bb21-740b8617dea3/resourceGroups/connectedk8sCLITestResources/providers/Microsoft.HybridCompute/gateways/gateway-test-cli" + } + + It 'Check if onboarding works with gateway enabled' { + az connectedk8s connect -n $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup -l $ARC_LOCATION --gateway-resource-id $gatewayResourceId --no-wait + $? | Should -BeTrue + Start-Sleep -Seconds 10 + + # Loop and retry until the configuration installs + $n = 0 + do + { + $output = az connectedk8s show -n $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup + $jsonOutput = [System.Text.Json.JsonDocument]::Parse($output) + $provisioningState = ($output | ConvertFrom-Json).provisioningState + $gatewayStatus = $jsonOutput.RootElement.GetProperty("gateway").GetProperty("enabled").GetBoolean() + $gatewayId = $jsonOutput.RootElement.GetProperty("gateway").GetProperty("resourceId").GetString() + Write-Host "Provisioning State: $provisioningState" + Write-Host "Gateway Status: $gatewayStatus" + Write-Host "Gateway Id: $gatewayId" + if ($provisioningState -eq $SUCCEEDED -and $gatewayStatus -eq $true -and $gatewayId -eq $gatewayResourceId) { + break + } + Start-Sleep -Seconds 10 + $n += 1 + } while ($n -le $MAX_RETRY_ATTEMPTS) + $n | Should -BeLessOrEqual $MAX_RETRY_ATTEMPTS + } + + It 'Disable the gateway' { + az connectedk8s update -n $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --disable-gateway + $? | Should -BeTrue + Start-Sleep -Seconds 10 + + # Loop and retry until the configuration installs + $n = 0 + do + { + $output = az connectedk8s show -n $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup + $jsonOutput = [System.Text.Json.JsonDocument]::Parse($output) + $provisioningState = ($output | ConvertFrom-Json).provisioningState + $gatewayStatus = $jsonOutput.RootElement.GetProperty("gateway").GetProperty("enabled").GetBoolean() + Write-Host "Provisioning State: $provisioningState" + Write-Host "Gateway Status: $gatewayStatus" + if ($provisioningState -eq $SUCCEEDED -and $gatewayStatus -eq $false) { + break + } + Start-Sleep -Seconds 10 + $n += 1 + } while ($n -le $MAX_RETRY_ATTEMPTS) + $n | Should -BeLessOrEqual $MAX_RETRY_ATTEMPTS + } + + It 'Update the cluster to use gateway again using update cmd' { + az connectedk8s update -n $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --gateway-resource-id $gatewayResourceId + $? | Should -BeTrue + Start-Sleep -Seconds 10 + + # Loop and retry until the configuration installs + $n = 0 + do + { + $output = az connectedk8s show -n $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup + $jsonOutput = [System.Text.Json.JsonDocument]::Parse($output) + $provisioningState = ($output | ConvertFrom-Json).provisioningState + $gatewayStatus = $jsonOutput.RootElement.GetProperty("gateway").GetProperty("enabled").GetBoolean() + $gatewayId = $jsonOutput.RootElement.GetProperty("gateway").GetProperty("resourceId").GetString() + Write-Host "Provisioning State: $provisioningState" + Write-Host "Gateway Status: $gatewayStatus" + Write-Host "Gateway Id: $gatewayId" + if ($provisioningState -eq $SUCCEEDED -and $gatewayStatus -eq $true -and $gatewayId -eq $gatewayResourceId) { + break + } + Start-Sleep -Seconds 10 + $n += 1 + } while ($n -le $MAX_RETRY_ATTEMPTS) + $n | Should -BeLessOrEqual $MAX_RETRY_ATTEMPTS + } + + It 'Disable the gateway' { + az connectedk8s update -n $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --disable-gateway + $? | Should -BeTrue + Start-Sleep -Seconds 10 + + # Loop and retry until the configuration installs + $n = 0 + do + { + $output = az connectedk8s show -n $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup + $jsonOutput = [System.Text.Json.JsonDocument]::Parse($output) + $provisioningState = ($output | ConvertFrom-Json).provisioningState + $gatewayStatus = $jsonOutput.RootElement.GetProperty("gateway").GetProperty("enabled").GetBoolean() + Write-Host "Provisioning State: $provisioningState" + Write-Host "Gateway Status: $gatewayStatus" + if ($provisioningState -eq $SUCCEEDED -and $gatewayStatus -eq $false) { + break + } + Start-Sleep -Seconds 10 + $n += 1 + } while ($n -le $MAX_RETRY_ATTEMPTS) + $n | Should -BeLessOrEqual $MAX_RETRY_ATTEMPTS + } + + It "Delete the connected instance" { + az connectedk8s delete -n $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --force -y + $? | Should -BeTrue + + # Configuration should be removed from the resource model + az connectedk8s show -n $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup + $? | Should -BeFalse + } +} \ No newline at end of file diff --git a/testing/test/configurations/Proxy.Tests.ps1 b/testing/test/configurations/Proxy.Tests.ps1 new file mode 100644 index 00000000000..bda7b06e4bc --- /dev/null +++ b/testing/test/configurations/Proxy.Tests.ps1 @@ -0,0 +1,65 @@ +Describe 'Proxy Scenario' { + BeforeAll { + . $PSScriptRoot/../helper/Constants.ps1 + } + + It 'Check if basic onboarding works correctly with proxy enabled' { + az connectedk8s connect -n $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup -l $ARC_LOCATION --proxy-skip-range logcollector --no-wait + $? | Should -BeTrue + Start-Sleep -Seconds 10 + + # Loop and retry until the configuration installs + $n = 0 + do + { + $output = az connectedk8s show -n $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup + $provisioningState = ($output | ConvertFrom-Json).provisioningState + Write-Host "Provisioning State: $provisioningState" + if ($provisioningState -eq $SUCCEEDED) { + $isProxyEnabled = helm get values -n azure-arc-release azure-arc -o yaml | grep isProxyEnabled + Write-Host "$isProxyEnabled" + if ($isProxyEnabled -match "isProxyEnabled: true") { + break + } + } + Start-Sleep -Seconds 10 + $n += 1 + } while ($n -le $MAX_RETRY_ATTEMPTS) + $n | Should -BeLessOrEqual $MAX_RETRY_ATTEMPTS + } + + It 'Disable proxy' { + az connectedk8s update -n $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --disable-proxy + $? | Should -BeTrue + Start-Sleep -Seconds 10 + + # Loop and retry until the configuration installs + $n = 0 + do + { + $output = az connectedk8s show -n $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup + $provisioningState = ($output | ConvertFrom-Json).provisioningState + Write-Host "Provisioning State: $provisioningState" + if ($provisioningState -eq $SUCCEEDED) { + $isProxyEnabled = helm get values -n azure-arc-release azure-arc -o yaml | grep isProxyEnabled + Write-Host "$isProxyEnabled" + if ($isProxyEnabled -match "isProxyEnabled: false") { + break + } + break + } + Start-Sleep -Seconds 10 + $n += 1 + } while ($n -le $MAX_RETRY_ATTEMPTS) + $n | Should -BeLessOrEqual $MAX_RETRY_ATTEMPTS + } + + It "Delete the connected instance" { + az connectedk8s delete -n $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --force -y + $? | Should -BeTrue + + # Configuration should be removed from the resource model + az connectedk8s show -n $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup + $? | Should -BeFalse + } +} \ No newline at end of file diff --git a/testing/test/configurations/Troubleshoot.Tests.ps1 b/testing/test/configurations/Troubleshoot.Tests.ps1 new file mode 100644 index 00000000000..c9cb4e26010 --- /dev/null +++ b/testing/test/configurations/Troubleshoot.Tests.ps1 @@ -0,0 +1,40 @@ +Describe 'Troubleshoot Scenario' { + BeforeAll { + . $PSScriptRoot/../helper/Constants.ps1 + } + + It 'Verify cluster onboarding process' { + az connectedk8s connect -n $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup -l $ARC_LOCATION --no-wait + $? | Should -BeTrue + Start-Sleep -Seconds 10 + + # Loop and retry until the configuration installs + $n = 0 + do + { + $output = az connectedk8s show -n $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup + $provisioningState = ($output | ConvertFrom-Json).provisioningState + Write-Host "Provisioning State: $provisioningState" + if ($provisioningState -eq $SUCCEEDED) { + break + } + Start-Sleep -Seconds 10 + $n += 1 + } while ($n -le $MAX_RETRY_ATTEMPTS) + $n | Should -BeLessOrEqual $MAX_RETRY_ATTEMPTS + } + + It 'Verify troubleshoot command functionality' { + az connectedk8s troubleshoot -n $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup + $? | Should -BeTrue + } + + It "Delete the connected instance" { + az connectedk8s delete -n $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --force -y + $? | Should -BeTrue + + # Configuration should be removed from the resource model + az connectedk8s show -n $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup + $? | Should -BeFalse + } +} \ No newline at end of file diff --git a/testing/test/configurations/WorkloadIdentity.Tests.ps1 b/testing/test/configurations/WorkloadIdentity.Tests.ps1 new file mode 100644 index 00000000000..c728b6a5236 --- /dev/null +++ b/testing/test/configurations/WorkloadIdentity.Tests.ps1 @@ -0,0 +1,239 @@ +Describe 'Onboarding with Workload Identity Scenario' { + BeforeAll { + . $PSScriptRoot/../helper/Constants.ps1 + } + + It 'Check if onboarding works with oidc and workload identity enabled' { + az connectedk8s connect -n $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup -l $ARC_LOCATION --enable-oidc-issuer --enable-workload-identity --no-wait + $? | Should -BeTrue + Start-Sleep -Seconds 10 + + # Loop and retry until the configuration installs + $n = 0 + do + { + $output = az connectedk8s show -n $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup + $jsonOutput = [System.Text.Json.JsonDocument]::Parse($output) + $provisioningState = ($output | ConvertFrom-Json).provisioningState + $securityProfile = $jsonOutput.RootElement.GetProperty("securityProfile").GetProperty("workloadIdentity").GetProperty("enabled").GetBoolean() + $oidcIssuerProfile = $jsonOutput.RootElement.GetProperty("oidcIssuerProfile").GetProperty("enabled").GetBoolean() + $issuerUrl = $jsonOutput.RootElement.GetProperty("oidcIssuerProfile").GetProperty("issuerUrl").GetString() + $selfHostedIssuerUrl = $jsonOutput.RootElement.GetProperty("oidcIssuerProfile").GetProperty("selfHostedIssuerUrl").GetString() + $agentState = $jsonOutput.RootElement.GetProperty("arcAgentProfile").GetProperty("agentState").GetString() + Write-Host "Provisioning State: $provisioningState" + Write-Host "Security Profile Status: $securityProfile" + Write-Host "OIDC Issuer Profile Status: $oidcIssuerProfile" + Write-Host "Issuer Url: $issuerUrl" + Write-Host "Self Hosted Issuer Url: $selfHostedIssuerUrl" + Write-Host "Agent State: $agentState" + if ( + $provisioningState -eq $SUCCEEDED -and + $securityProfile -eq $true -and + $oidcIssuerProfile -eq $true -and + ![string]::IsNullOrEmpty($issuerUrl) -and + $issuerUrl -like "*unitedkingdom*" -and + [string]::IsNullOrEmpty($selfHostedIssuerUrl) -and + $agentState -eq $SUCCEEDED + ) { + break + } + Start-Sleep -Seconds 10 + $n += 1 + } while ($n -le $MAX_RETRY_ATTEMPTS) + $n | Should -BeLessOrEqual $MAX_RETRY_ATTEMPTS + } + + It 'Disable workload identity' { + az connectedk8s update -n $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --disable-workload-identity + $? | Should -BeTrue + Start-Sleep -Seconds 10 + + # Loop and retry until the configuration installs + $n = 0 + do + { + $output = az connectedk8s show -n $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup + $jsonOutput = [System.Text.Json.JsonDocument]::Parse($output) + $provisioningState = ($output | ConvertFrom-Json).provisioningState + $securityProfile = $jsonOutput.RootElement.GetProperty("securityProfile").GetProperty("workloadIdentity").GetProperty("enabled").GetBoolean() + $agentState = $jsonOutput.RootElement.GetProperty("arcAgentProfile").GetProperty("agentState").GetString() + Write-Host "Provisioning State: $provisioningState" + Write-Host "Security Profile Status: $securityProfile" + Write-Host "Agent State: $agentState" + if ($provisioningState -eq $SUCCEEDED -and $securityProfile -eq $false -and $agentState -eq $SUCCEEDED) { + break + } + Start-Sleep -Seconds 10 + $n += 1 + } while ($n -le $MAX_RETRY_ATTEMPTS) + $n | Should -BeLessOrEqual $MAX_RETRY_ATTEMPTS + } + + It 'Update the cluster to use workload identity again using update cmd' { + az connectedk8s update -n $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --enable-workload-identity + $? | Should -BeTrue + Start-Sleep -Seconds 10 + + # Loop and retry until the configuration installs + $n = 0 + do + { + $output = az connectedk8s show -n $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup + $jsonOutput = [System.Text.Json.JsonDocument]::Parse($output) + $provisioningState = ($output | ConvertFrom-Json).provisioningState + $securityProfile = $jsonOutput.RootElement.GetProperty("securityProfile").GetProperty("workloadIdentity").GetProperty("enabled").GetBoolean() + $agentState = $jsonOutput.RootElement.GetProperty("arcAgentProfile").GetProperty("agentState").GetString() + Write-Host "Provisioning State: $provisioningState" + Write-Host "Security Profile Status: $securityProfile" + Write-Host "Agent State: $agentState" + if ( + $provisioningState -eq $SUCCEEDED -and + $securityProfile -eq $true -and + $agentState -eq $SUCCEEDED + ) { + break + } + Start-Sleep -Seconds 10 + $n += 1 + } while ($n -le $MAX_RETRY_ATTEMPTS) + $n | Should -BeLessOrEqual $MAX_RETRY_ATTEMPTS + } + + It "Delete the connected instance" { + az connectedk8s delete -n $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --force -y + $? | Should -BeTrue + + # Configuration should be removed from the resource model + az connectedk8s show -n $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup + $? | Should -BeFalse + Start-Sleep -Seconds 10 + } +} + +Describe 'Updating with Workload Identity Scenario' { + BeforeAll { + . $PSScriptRoot/../helper/Constants.ps1 + } + + It 'Onboard a cluster to arc' { + az connectedk8s connect -n $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup -l $ARC_LOCATION --no-wait + $? | Should -BeTrue + Start-Sleep -Seconds 10 + + # Loop and retry until the configuration installs + $n = 0 + do + { + $output = az connectedk8s show -n $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup + $provisioningState = ($output | ConvertFrom-Json).provisioningState + Write-Host "Provisioning State: $provisioningState" + if ($provisioningState -eq $SUCCEEDED) { + break + } + Start-Sleep -Seconds 10 + $n += 1 + } while ($n -le $MAX_RETRY_ATTEMPTS) + $n | Should -BeLessOrEqual $MAX_RETRY_ATTEMPTS + } + + It 'Update the cluster with oidc and workload identity enabled' { + az connectedk8s update -n $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --enable-oidc-issuer --enable-workload-identity + $? | Should -BeTrue + Start-Sleep -Seconds 10 + + # Loop and retry until the configuration installs + $n = 0 + do + { + $output = az connectedk8s show -n $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup + $jsonOutput = [System.Text.Json.JsonDocument]::Parse($output) + $provisioningState = ($output | ConvertFrom-Json).provisioningState + $securityProfile = $jsonOutput.RootElement.GetProperty("securityProfile").GetProperty("workloadIdentity").GetProperty("enabled").GetBoolean() + $oidcIssuerProfile = $jsonOutput.RootElement.GetProperty("oidcIssuerProfile").GetProperty("enabled").GetBoolean() + $issuerUrl = $jsonOutput.RootElement.GetProperty("oidcIssuerProfile").GetProperty("issuerUrl").GetString() + $selfHostedIssuerUrl = $jsonOutput.RootElement.GetProperty("oidcIssuerProfile").GetProperty("selfHostedIssuerUrl").GetString() + $agentState = $jsonOutput.RootElement.GetProperty("arcAgentProfile").GetProperty("agentState").GetString() + Write-Host "Provisioning State: $provisioningState" + Write-Host "Security Profile Status: $securityProfile" + Write-Host "OIDC Issuer Profile Status: $oidcIssuerProfile" + Write-Host "Issuer Url: $issuerUrl" + Write-Host "Self Hosted Issuer Url: $selfHostedIssuerUrl" + Write-Host "Agent State: $agentState" + if ( + $provisioningState -eq $SUCCEEDED -and + $securityProfile -eq $true -and + $oidcIssuerProfile -eq $true -and + ![string]::IsNullOrEmpty($issuerUrl) -and + $issuerUrl -like "*unitedkingdom*" -and + [string]::IsNullOrEmpty($selfHostedIssuerUrl) -and + $agentState -eq $SUCCEEDED + ) { + break + } + Start-Sleep -Seconds 10 + $n += 1 + } while ($n -le $MAX_RETRY_ATTEMPTS) + $n | Should -BeLessOrEqual $MAX_RETRY_ATTEMPTS + } + + It "Delete the connected instance" { + az connectedk8s delete -n $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --force -y + $? | Should -BeTrue + + # Configuration should be removed from the resource model + az connectedk8s show -n $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup + $? | Should -BeFalse + Start-Sleep -Seconds 10 + } +} + +Describe 'Creating with Workload Identity Scenario and Self Hosted Issuer' { + BeforeAll { + . $PSScriptRoot/../helper/Constants.ps1 + + $SelfHostedIssuer = "https://eastus.oic.prod-aks.azure.com/fc50e82b-3761-4218-8691-d98bcgb146da/e6c4bf03-84d9-480c-a269-37a41c28c5cb/" + } + + It 'Check if onboarding works with oidc enabled and self-hosted issuer url passed in' { + az connectedk8s connect -n $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup -l $ARC_LOCATION --enable-oidc-issuer --self-hosted-issuer $SelfHostedIssuer --no-wait + $? | Should -BeTrue + Start-Sleep -Seconds 10 + + # Loop and retry until the configuration installs + $n = 0 + do + { + $output = az connectedk8s show -n $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup + $jsonOutput = [System.Text.Json.JsonDocument]::Parse($output) + $provisioningState = ($output | ConvertFrom-Json).provisioningState + $oidcIssuerProfile = $jsonOutput.RootElement.GetProperty("oidcIssuerProfile").GetProperty("enabled").GetBoolean() + $issuerUrl = $jsonOutput.RootElement.GetProperty("oidcIssuerProfile").GetProperty("issuerUrl").GetString() + $selfHostedIssuerUrl = $jsonOutput.RootElement.GetProperty("oidcIssuerProfile").GetProperty("selfHostedIssuerUrl").GetString() + Write-Host "Provisioning State: $provisioningState" + Write-Host "OIDC Issuer Profile Status: $oidcIssuerProfile" + Write-Host "Issuer Url: $issuerUrl" + Write-Host "Self Hosted Issuer Url: $selfHostedIssuerUrl" + if ( + $provisioningState -eq $SUCCEEDED -and + $oidcIssuerProfile -eq $true -and + [string]::IsNullOrEmpty($issuerUrl) -and + ![string]::IsNullOrEmpty($selfHostedIssuerUrl) -and + $selfHostedIssuerUrl -eq $SelfHostedIssuer + ) { + break + } + Start-Sleep -Seconds 10 + $n += 1 + } while ($n -le $MAX_RETRY_ATTEMPTS) + $n | Should -BeLessOrEqual $MAX_RETRY_ATTEMPTS + } + + It "Delete the connected instance" { + az connectedk8s delete -n $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --force -y + $? | Should -BeTrue + + # Configuration should be removed from the resource model + az connectedk8s show -n $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup + $? | Should -BeFalse + } +} \ No newline at end of file diff --git a/testing/test/helper/Constants.ps1 b/testing/test/helper/Constants.ps1 new file mode 100644 index 00000000000..43006f78a69 --- /dev/null +++ b/testing/test/helper/Constants.ps1 @@ -0,0 +1,5 @@ +$ENVCONFIG = Get-Content -Path $PSScriptRoot/../../settings.json | ConvertFrom-Json + +$MAX_RETRY_ATTEMPTS = 30 +$ARC_LOCATION = "uksouth" +$SUCCEEDED = "Succeeded" \ No newline at end of file From 940942cb359bcc96abb2236f514a7a8866767aeb Mon Sep 17 00:00:00 2001 From: "Matthew McNeal (from Dev Box)" Date: Tue, 18 Mar 2025 18:29:40 -0400 Subject: [PATCH 42/42] Fix azdev style --- src/connectedk8s/azext_connectedk8s/_precheckutils.py | 2 +- .../azext_connectedk8s/clientproxyhelper/_binaryutils.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/connectedk8s/azext_connectedk8s/_precheckutils.py b/src/connectedk8s/azext_connectedk8s/_precheckutils.py index 5cb90893243..30128513e27 100644 --- a/src/connectedk8s/azext_connectedk8s/_precheckutils.py +++ b/src/connectedk8s/azext_connectedk8s/_precheckutils.py @@ -138,7 +138,7 @@ def fetch_diagnostic_checks_results( def executing_cluster_diagnostic_checks_job( - cmd: CLICommand, + cmd: CLICommand, corev1_api_instance: CoreV1Api, batchv1_api_instance: BatchV1Api, helm_client_location: str, diff --git a/src/connectedk8s/azext_connectedk8s/clientproxyhelper/_binaryutils.py b/src/connectedk8s/azext_connectedk8s/clientproxyhelper/_binaryutils.py index 22b07f306a8..9a3519ba010 100644 --- a/src/connectedk8s/azext_connectedk8s/clientproxyhelper/_binaryutils.py +++ b/src/connectedk8s/azext_connectedk8s/clientproxyhelper/_binaryutils.py @@ -68,7 +68,7 @@ def install_client_side_proxy( def _download_proxy_from_MCR( cmd: CLICommand, dest_dir: str, proxy_name: str, operating_system: str, architecture: str ) -> None: - + active_directory_array = cmd.cli_ctx.cloud.endpoints.active_directory.split(".") # default for public, mc, ff clouds