Skip to content

Add ARM64 support for Helm installation in connectedk8s#9815

Closed
ashnanze wants to merge 19 commits intoAzure:mainfrom
AzureArcForKubernetes:main
Closed

Add ARM64 support for Helm installation in connectedk8s#9815
ashnanze wants to merge 19 commits intoAzure:mainfrom
AzureArcForKubernetes:main

Conversation

@ashnanze
Copy link
Copy Markdown

Summary

  • Bumps Helm version from v3.12.2 to v3.20.1.
  • Detects host architecture via platform.machine() (maps aarch64/arm64 to arm64, defaults to amd64).
  • Uses the detected architecture in all Helm artifact tags, download file names, and install paths (previously hardcoded to amd64).
  • Adds _resolve_helm_pull_target() to resolve the correct OCI pull target: tries arch-specific tag first (helm-v3.20.1-linux-arm64), falls back to manifest list tag (helm-v3.20.1) with annotation-based matching.
  • Ensures the versioned local download directory exists before saving the Helm archive.

Why

  • Enables az connectedk8s connect to work on ARM64 machines by downloading the correct Helm binary.
  • Previously, ARM64 hosts would download the AMD64 binary, which would fail to execute. 

Validation

  • Built and installed the updated extension .whl.
  • Validated behavior on an ARM64 VM.
  • Validated behavior on an AMD64 VM.
  • Both VMs successfully completed az connectedk8s connect.
  • Confirmed the updated Helm installation flow works across both architectures.

Directory handling note

  • This fix also addresses a latent path issue in Helm download setup.
  • The download target file path is ~/.azure/helm/<version>/<archive-file>.
  • Previous logic only created the parent directory ~/.azure/helm, not the required version subdirectory ~/.azure/helm/<version>.
  • Failures occurred when ~/.azure/helm/<version> was missing, even if ~/.azure/helm already existed.
  • It could appear to work when that specific <version> directory had already been created by a prior run.
  • The fix now creates download_location (~/.azure/helm/<version>) directly before download.

Bavneet Singh and others added 19 commits September 30, 2025 09:40
* forcedelete

* format

* add code owner

* mypy
…add E2E coverage and improve logging (#20)

* add pester tests for connectedk8s cli extension

* Pass the force delete param to the API call (#4)

* forcedelete

* format

* add code owner

* mypy

* Parameterize for airgapped clouds (#5)

* Add parameterization for the airgapped clouds

* Fix azdev style

* MCR path function

* azdev, ruff, and mypy

---------

Co-authored-by: Matthew McNeal (from Dev Box) <mmcneal@microsoft.com>

* Oras client fix to work with different MCRs (#6)

Co-authored-by: mmcneal <mmcneal@microsoft.com>

* fix CI testcases for nodepool image issues (#8)

* update errors for the config and connectivity issues (#11)

* update errors

* format

* style

* update python version to 3.13 (#12)

* Update cluster diagnostics image to 1.29.3 (#7)

* Update cluster diagnostics helm chart to 1.29.3

* Fix lint issues

---------

Co-authored-by: bgriddaluru <bharath.griddaluru@microsoft.com>

* RBAC deprecation & fix the issue

* typo

* fix comments

* update tests

* add pester tests for connectedk8s cli extension

* Pass the force delete param to the API call (#4)

* forcedelete

* format

* add code owner

* mypy

* fix CI testcases for nodepool image issues (#8)

* update errors for the config and connectivity issues (#11)

* update errors

* format

* style

* update python version to 3.13 (#12)

* rebase

* fix tests

* fix version

* fix mypy, lint

* fix test

* fix test

* fix test

* fix test

* fix test

* rename test

* deprecate flags

* rebase

* rebase

* bump version for release

---------

Co-authored-by: Bavneet Singh <bavneetsingh@microsoft.com>
Co-authored-by: Atchut Kumar Barli <atchut@gmail.com>
Co-authored-by: mcnealm13 <57726243+mcnealm13@users.noreply.github.com>
Co-authored-by: Matthew McNeal (from Dev Box) <mmcneal@microsoft.com>
Co-authored-by: Bavneet Singh <33008256+bavneetsingh16@users.noreply.github.com>
Co-authored-by: bgriddaluru <117554445+bgriddaluru@users.noreply.github.com>
Co-authored-by: bgriddaluru <bharath.griddaluru@microsoft.com>
Co-authored-by: vithumma <vithumma@microsoft.com>
* add agc overrides

* update gns endpoint

* add indentation

* fix linter error

* fix ruff formatting

* move overrides to it's own method

* update method

* update ruff formatting
* add pester tests for connectedk8s cli extension

* Pass the force delete param to the API call (#4)

* forcedelete

* format

* add code owner

* mypy

* Parameterize for airgapped clouds (#5)

* Add parameterization for the airgapped clouds

* Fix azdev style

* MCR path function

* azdev, ruff, and mypy

---------

Co-authored-by: Matthew McNeal (from Dev Box) <mmcneal@microsoft.com>

* Oras client fix to work with different MCRs (#6)

Co-authored-by: mmcneal <mmcneal@microsoft.com>

* fix CI testcases for nodepool image issues (#8)

* update errors for the config and connectivity issues (#11)

* update errors

* format

* style

* update python version to 3.13 (#12)

* Update cluster diagnostics image to 1.29.3 (#7)

* Update cluster diagnostics helm chart to 1.29.3

* Fix lint issues

---------

Co-authored-by: bgriddaluru <bharath.griddaluru@microsoft.com>

* RBAC deprecation & fix the issue

* typo

* fix comments

* update tests

* add pester tests for connectedk8s cli extension

* Pass the force delete param to the API call (#4)

* forcedelete

* format

* add code owner

* mypy

* fix CI testcases for nodepool image issues (#8)

* update errors for the config and connectivity issues (#11)

* update errors

* format

* style

* update python version to 3.13 (#12)

* rebase

* fix tests

* fix version

* fix mypy, lint

* fix test

* fix test

* fix test

* fix test

* fix test

* rename test

* add pester tests for connectedk8s cli extension

* Pass the force delete param to the API call (#4)

* forcedelete

* format

* add code owner

* mypy

* fix CI testcases for nodepool image issues (#8)

* update python version to 3.13 (#12)

* changes to support gateway association/disassociation for api version '2025-08-01-preview' (#17)

* [Azure RBAC] Deprecate 3P mode flags, fix Azure RBAC enablement bug, add E2E coverage and improve logging (#20)

* add pester tests for connectedk8s cli extension

* Pass the force delete param to the API call (#4)

* forcedelete

* format

* add code owner

* mypy

* Parameterize for airgapped clouds (#5)

* Add parameterization for the airgapped clouds

* Fix azdev style

* MCR path function

* azdev, ruff, and mypy

---------

Co-authored-by: Matthew McNeal (from Dev Box) <mmcneal@microsoft.com>

* Oras client fix to work with different MCRs (#6)

Co-authored-by: mmcneal <mmcneal@microsoft.com>

* fix CI testcases for nodepool image issues (#8)

* update errors for the config and connectivity issues (#11)

* update errors

* format

* style

* update python version to 3.13 (#12)

* Update cluster diagnostics image to 1.29.3 (#7)

* Update cluster diagnostics helm chart to 1.29.3

* Fix lint issues

---------

Co-authored-by: bgriddaluru <bharath.griddaluru@microsoft.com>

* RBAC deprecation & fix the issue

* typo

* fix comments

* update tests

* add pester tests for connectedk8s cli extension

* Pass the force delete param to the API call (#4)

* forcedelete

* format

* add code owner

* mypy

* fix CI testcases for nodepool image issues (#8)

* update errors for the config and connectivity issues (#11)

* update errors

* format

* style

* update python version to 3.13 (#12)

* rebase

* fix tests

* fix version

* fix mypy, lint

* fix test

* fix test

* fix test

* fix test

* fix test

* rename test

* deprecate flags

* rebase

* rebase

* bump version for release

---------

Co-authored-by: Bavneet Singh <bavneetsingh@microsoft.com>
Co-authored-by: Atchut Kumar Barli <atchut@gmail.com>
Co-authored-by: mcnealm13 <57726243+mcnealm13@users.noreply.github.com>
Co-authored-by: Matthew McNeal (from Dev Box) <mmcneal@microsoft.com>
Co-authored-by: Bavneet Singh <33008256+bavneetsingh16@users.noreply.github.com>
Co-authored-by: bgriddaluru <117554445+bgriddaluru@users.noreply.github.com>
Co-authored-by: bgriddaluru <bharath.griddaluru@microsoft.com>
Co-authored-by: vithumma <vithumma@microsoft.com>

* remove breaking change announcement for removed flags

---------

Co-authored-by: Bavneet Singh <bavneetsingh@microsoft.com>
Co-authored-by: Atchut Kumar Barli <atchut@gmail.com>
Co-authored-by: mcnealm13 <57726243+mcnealm13@users.noreply.github.com>
Co-authored-by: Matthew McNeal (from Dev Box) <mmcneal@microsoft.com>
Co-authored-by: Bavneet Singh <33008256+bavneetsingh16@users.noreply.github.com>
Co-authored-by: bgriddaluru <117554445+bgriddaluru@users.noreply.github.com>
Co-authored-by: bgriddaluru <bharath.griddaluru@microsoft.com>
Co-authored-by: vithumma <vithumma@microsoft.com>
* adjusting arm64 support

* editing

* adding

* Revert k8s-extension changes from this repo

* changes

* changing order

* fix ruff: ternary for arch, remove unused artifactTag

* fix ruff format

* fix mypy type: ignore comments in _utils.py

* raise CLIInternalError instead of silent fallbacks in _resolve_helm_pull_target

* use requests instead of oras internals for manifest resolution

* add MCR anonymous bearer token auth for manifest resolution

* fix MCR resolution: add Accept header, fetch child manifests for annotations
the tests passed, the typecheck and extension index failures are not relevant to this PR
* switch

* fixswitch

* addhelm4test

* specificexception
Copilot AI review requested due to automatic review settings April 22, 2026 04:59
@azure-client-tools-bot-prd
Copy link
Copy Markdown

Validation for Breaking Change Starting...

Thanks for your contribution!

@yonzhan
Copy link
Copy Markdown
Collaborator

yonzhan commented Apr 22, 2026

Thank you for your contribution! We will review the pull request and get back to you soon.

@github-actions
Copy link
Copy Markdown
Contributor

The git hooks are available for azure-cli and azure-cli-extensions repos. They could help you run required checks before creating the PR.

Please sync the latest code with latest dev branch (for azure-cli) or main branch (for azure-cli-extensions).
After that please run the following commands to enable git hooks:

pip install azdev --upgrade
azdev setup -c <your azure-cli repo path> -r <your azure-cli-extensions repo path>

@github-actions
Copy link
Copy Markdown
Contributor

CodeGen Tools Feedback Collection

Thank you for using our CodeGen tool. We value your feedback, and we would like to know how we can improve our product. Please take a few minutes to fill our codegen survey

@github-actions
Copy link
Copy Markdown
Contributor

Hi @ashnanze

⚠️ Release Requirements

Module: connectedk8s

  • ⚠️ Please update VERSION to be 1.11.1 in src/connectedk8s/setup.py

Module: k8s-extension

  • Please log updates into to src/k8s-extension/HISTORY.rst
  • Update VERSION to 1.7.1 in src/k8s-extension/setup.py

Notes

@github-actions github-actions Bot added the release-version-block Updates do not qualify release version rules. NOTE: please do not edit it manually. label Apr 22, 2026
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR updates the connectedk8s extension’s Helm installation flow to support ARM64 hosts by selecting the correct architecture-specific Helm artifacts from MCR, while also bumping the embedded Helm version and adding an integration-test harness + pipeline assets under testing/.

Changes:

  • Bump Helm from v3.12.2 to v3.20.1 and detect host architecture (amd64 vs arm64) for Helm artifact selection and install paths.
  • Add _resolve_helm_pull_target() to resolve an arch-specific OCI tag first and fall back to a manifest-list lookup.
  • Introduce a testing/ Pester-based test suite and Azure DevOps pipelines/templates for running connectedk8s scenarios.

Reviewed changes

Copilot reviewed 31 out of 35 changed files in this pull request and generated 8 comments.

Show a summary per file
File Description
testing/test/helper/Constants.ps1 Adds shared constants/config loading for Pester scenarios.
testing/test/configurations/WorkloadIdentity.Tests.ps1 Adds workload identity onboarding/update/delete scenario coverage.
testing/test/configurations/Troubleshoot.Tests.ps1 Adds troubleshoot command scenario coverage.
testing/test/configurations/Proxy.Tests.ps1 Adds proxy enable/disable scenario coverage.
testing/test/configurations/Gateway.Tests.ps1 Adds gateway enable/disable scenario coverage.
testing/test/configurations/ForcedDelete.Tests.ps1 Adds forced delete scenario coverage.
testing/test/configurations/EnableDisableFeatures.Tests.ps1 Adds enable/disable features scenario coverage with helpers.
testing/test/configurations/ConnectProxy.Tests.ps1 Adds connectedk8s proxy scenario coverage including kubeconfig validation.
testing/test/configurations/BasicOnboarding.Tests.ps1 Adds basic onboarding + auto-upgrade toggle scenario coverage.
testing/test/configurations/AutoUpdate.Tests.ps1 Adds auto-upgrade disabled/enabled scenario coverage.
testing/settings.template.json Adds template config file used by the new test harness.
testing/pipeline/templates/run-test.yml Adds ADO job template to build/install extension and run Pester scenarios.
testing/pipeline/k8s-custom-pipelines.yml Adds a multi-job pipeline to run the new scenario tests and checks.
testing/owners.txt Adds owners list for the testing area.
testing/bin/k8s_configuration-1.0.0-py3-none-any.whl Adds a pinned wheel used by the test harness.
testing/bin/connectedk8s-values.yaml Adds default values file used by the test harness.
testing/Test.ps1 Adds the main Pester runner script for CI/local execution.
testing/README.md Documents how to set up and run the new test suite.
testing/Bootstrap.ps1 Adds bootstrap script for test environment preparation.
testing/.gitignore Ignores runtime-generated test artifacts and most bin/ contents with allowlist exceptions.
src/k8s-extension/azext_k8s_extension/utils.py Refactors get_mcr_path to accept an endpoint string and adjusts logic for gov/china.
src/k8s-extension/azext_k8s_extension/custom.py Updates callers to the new get_mcr_path(active_directory_endpoint) signature.
src/connectedk8s/setup.py Bumps extension version to 1.10.12.
src/connectedk8s/azext_connectedk8s/tests/unittests/test_utils_.py Adds unit tests for the refactored get_mcr_path.
src/connectedk8s/azext_connectedk8s/custom.py Adds ARM64 arch detection, Helm v3.20.1 update, directory-creation fix, and _resolve_helm_pull_target() manifest logic.
src/connectedk8s/azext_connectedk8s/clientproxyhelper/_binaryutils.py Updates caller to use new get_mcr_path(active_directory_endpoint) signature.
src/connectedk8s/azext_connectedk8s/_utils.py Refactors get_mcr_path, adds Helm 4 compatibility for helm list, and adds AGC endpoint overrides helper.
src/connectedk8s/azext_connectedk8s/_precheckutils.py Updates caller to use new get_mcr_path(active_directory_endpoint) signature.
src/connectedk8s/azext_connectedk8s/_params.py Removes deprecated --app-id/--app-secret arguments from enable-features.
src/connectedk8s/azext_connectedk8s/_constants.py Updates Helm version and bumps related artifact versions used by the extension.
src/connectedk8s/azext_connectedk8s/_client_factory.py Removes hardcoded ARM base_url to allow sovereign clouds to use the correct endpoint.
src/connectedk8s/azext_connectedk8s/_breaking_change.py Removes breaking-change registration for now-removed deprecated args.
src/connectedk8s/HISTORY.rst Adds release notes for 1.10.11 and 1.10.12.

Comment thread testing/Test.ps1
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
Copy link

Copilot AI Apr 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The install path assumes the wheel filename format connectedk8s-<version>-py2.py3-none-any.whl, but the committed sample wheel in testing/bin/ is named connectedk8s-1.0.0-py3-none-any.whl (and settings.template.json defaults to 1.0.0). This mismatch will cause local runs to fail to find the wheel. Consider either (1) updating the committed wheel name / template version to match the expected tag, or (2) making the script glob for connectedk8s-$connectedk8sVersion-*.whl instead of hardcoding the tag.

Suggested change
az extension add --source ./bin/connectedk8s-$connectedk8sVersion-py2.py3-none-any.whl
$wheelPattern = "connectedk8s-$connectedk8sVersion-*.whl"
$wheelFiles = Get-ChildItem -Path "$PSScriptRoot/bin" -Filter $wheelPattern -File
if ($wheelFiles.Count -eq 0) {
throw "Could not find a connectedk8s wheel matching '$wheelPattern' in '$PSScriptRoot/bin'."
}
if ($wheelFiles.Count -gt 1) {
throw "Found multiple connectedk8s wheels matching '$wheelPattern' in '$PSScriptRoot/bin': $($wheelFiles.Name -join ', ')."
}
az extension add --source $wheelFiles[0].FullName

Copilot uses AI. Check for mistakes.
Comment thread testing/Test.ps1
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'..."
Copy link

Copilot AI Apr 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the ParallelCI branch, the log message references $testFilePath, but only $testFilePaths is defined earlier. This will print an empty value and makes the script harder to debug; if later logic depends on it, it can also break. Use the correct variable (or set $testFilePath explicitly) before referencing it.

Suggested change
Write-Host "Invoking Pester to run tests from '$testFilePath'..."
Write-Host "Invoking Pester to run tests from '$testFilePaths'..."

Copilot uses AI. Check for mistakes.
Comment thread testing/Test.ps1
Comment on lines +58 to +71
$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
Copy link

Copilot AI Apr 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Several parameters use a Unicode en dash (e.g. –leaf, –Format, –AutoSize, –Seconds, –AutoRemoveJob, –Wait, –ForegroundColor) instead of the ASCII hyphen-minus -. PowerShell treats these as different characters, so these calls will fail at runtime. Replace them with standard -leaf, -Format, etc.

Copilot uses AI. Check for mistakes.
Comment on lines +1284 to +1326
# OCI media types required by MCR (HEAD/GET return 404 without Accept).
oci_accept = (
"application/vnd.oci.image.manifest.v1+json, "
"application/vnd.oci.image.index.v1+json"
)

# Check whether the arch-specific tag exists.
try:
response = http_client.head(
f"{base_api}/{arch_specific_tag}",
headers={"Accept": oci_accept},
timeout=30,
)
if response.status_code == 200:
return arch_specific_target
logger.debug(
"Arch-specific tag %s returned HTTP %d; trying manifest list.",
arch_specific_tag,
response.status_code,
)
except Exception as e: # pylint: disable=broad-except
logger.debug(
"Arch-specific tag check failed (%s); trying manifest list.",
e,
)

# Fall back to the manifest list tag and match via annotation title.
# Annotations live on each child manifest, not on the index entries,
# so we must fetch every child manifest to find the right one.
manifest_list_tag = f"helm-{helm_version}"
expected_title_prefix = f"helm-{helm_version}-{operating_system}-{arch}"
try:
response = http_client.get(
f"{base_api}/{manifest_list_tag}",
headers={"Accept": oci_accept},
timeout=30,
)
if response.status_code != 200:
raise CLIInternalError(
f"Could not resolve helm binary for {operating_system}/{arch}. "
f"Arch-specific tag '{arch_specific_tag}' check failed and "
f"manifest list '{manifest_list_tag}' returned HTTP {response.status_code}."
)
Copy link

Copilot AI Apr 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_resolve_helm_pull_target queries the registry HTTP API with requests but does not handle the common 401/WWW-Authenticate challenge flow (token retrieval) used by Docker/OCI registries. If MCR requires auth (or changes behavior), this function will always fail even though oras could have pulled successfully. Consider resolving the target by attempting oras pull for the arch-specific tag first (and only falling back on error), or implement the bearer-token flow for registry API calls.

Copilot uses AI. Check for mistakes.
Comment on lines +1284 to +1287
# OCI media types required by MCR (HEAD/GET return 404 without Accept).
oci_accept = (
"application/vnd.oci.image.manifest.v1+json, "
"application/vnd.oci.image.index.v1+json"
Copy link

Copilot AI Apr 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Accept header only advertises OCI media types. Some registries (and some MCR artifacts) return Docker v2 manifest list/media types, and will respond with 404/406 if those aren’t included. To make this more robust, include the Docker media types as well (e.g. application/vnd.docker.distribution.manifest.v2+json and application/vnd.docker.distribution.manifest.list.v2+json) or handle content negotiation based on the Content-Type response header.

Suggested change
# OCI media types required by MCR (HEAD/GET return 404 without Accept).
oci_accept = (
"application/vnd.oci.image.manifest.v1+json, "
"application/vnd.oci.image.index.v1+json"
# Registry responses may use either OCI or Docker schema 2 media types;
# advertise both so HEAD/GET requests work across MCR artifacts and
# compatible registries.
oci_accept = (
"application/vnd.oci.image.manifest.v1+json, "
"application/vnd.oci.image.index.v1+json, "
"application/vnd.docker.distribution.manifest.v2+json, "
"application/vnd.docker.distribution.manifest.list.v2+json"

Copilot uses AI. Check for mistakes.
Comment on lines +1879 to +1896
arm_metadata_endpoint_array = (
arm_metadata["authentication"]["loginEndpoint"].strip("/").split(".")
)
if len(arm_metadata_endpoint_array) < 4:
raise CLIInternalError("Unexpected loginEndpoint format for AGC")

cloud_suffix = arm_metadata_endpoint_array[3]
endpoint_suffix = (
arm_metadata_endpoint_array[2] + "." + arm_metadata_endpoint_array[3]
)
if cloud_name.lower() == "usnat":
cloud_suffix = (
arm_metadata_endpoint_array[2]
+ "."
+ arm_metadata_endpoint_array[3]
+ "."
+ arm_metadata_endpoint_array[4]
)
Copy link

Copilot AI Apr 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

add_agc_endpoint_overrides indexes arm_metadata_endpoint_array[4] in the usnat branch but only validates len(...) < 4. If loginEndpoint splits into exactly 4 segments for usnat, this will raise IndexError and break onboarding. Add an explicit length check for the usnat format (>= 5) and/or parse the URL host with urllib.parse.urlparse before splitting.

Copilot uses AI. Check for mistakes.
Comment on lines +43 to +50
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
}
Copy link

Copilot AI Apr 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the 'Disable proxy' retry loop, there is an unconditional break inside the $provisioningState -eq $SUCCEEDED block (line 49). This exits the loop even when isProxyEnabled is not yet false, so the test can pass without actually validating the update took effect. Remove the unconditional break (or move it into an else branch) so the loop only exits when the expected Helm value is observed or retries are exhausted.

Copilot uses AI. Check for mistakes.
Comment on lines +1254 to +1379
def _resolve_helm_pull_target(
mcr_url: str,
helm_mcr_repo: str,
helm_version: str,
operating_system: str,
arch: str,
) -> str:
"""Return the ORAS pull target for the helm binary.

Tries the arch-specific tag first (e.g. ``helm-v3.20.1-linux-arm64``).
If that tag does not exist, falls back to the manifest list tag
(``helm-v3.20.1``) and resolves the correct entry by matching the
``org.opencontainers.image.title`` annotation on each child manifest.

Uses the OCI Distribution v2 HTTP API directly so that the logic is
independent of the ``oras`` library version installed.

:param mcr_url: MCR hostname (e.g. ``mcr.microsoft.com``)
:param helm_mcr_repo: repository path within MCR (e.g. ``azurearck8s/helm``)
:param helm_version: helm version string including the leading ``v`` (e.g. ``v3.20.1``)
:param operating_system: lower-case OS name: ``linux``, ``darwin``, or ``windows``
:param arch: CPU architecture: ``amd64`` or ``arm64``
:returns: full ORAS pull target string (tag-based or digest-based)
"""
import requests as http_client # pylint: disable=import-outside-toplevel

arch_specific_tag = f"helm-{helm_version}-{operating_system}-{arch}"
arch_specific_target = f"{mcr_url}/{helm_mcr_repo}:{arch_specific_tag}"
base_api = f"https://{mcr_url}/v2/{helm_mcr_repo}/manifests"

# OCI media types required by MCR (HEAD/GET return 404 without Accept).
oci_accept = (
"application/vnd.oci.image.manifest.v1+json, "
"application/vnd.oci.image.index.v1+json"
)

# Check whether the arch-specific tag exists.
try:
response = http_client.head(
f"{base_api}/{arch_specific_tag}",
headers={"Accept": oci_accept},
timeout=30,
)
if response.status_code == 200:
return arch_specific_target
logger.debug(
"Arch-specific tag %s returned HTTP %d; trying manifest list.",
arch_specific_tag,
response.status_code,
)
except Exception as e: # pylint: disable=broad-except
logger.debug(
"Arch-specific tag check failed (%s); trying manifest list.",
e,
)

# Fall back to the manifest list tag and match via annotation title.
# Annotations live on each child manifest, not on the index entries,
# so we must fetch every child manifest to find the right one.
manifest_list_tag = f"helm-{helm_version}"
expected_title_prefix = f"helm-{helm_version}-{operating_system}-{arch}"
try:
response = http_client.get(
f"{base_api}/{manifest_list_tag}",
headers={"Accept": oci_accept},
timeout=30,
)
if response.status_code != 200:
raise CLIInternalError(
f"Could not resolve helm binary for {operating_system}/{arch}. "
f"Arch-specific tag '{arch_specific_tag}' check failed and "
f"manifest list '{manifest_list_tag}' returned HTTP {response.status_code}."
)

index = response.json()
for entry in index.get("manifests", []):
# Check platform fields if present (future-proof).
plat = entry.get("platform", {})
if plat.get("os") == operating_system and plat.get("architecture") == arch:
digest = entry["digest"]
logger.debug(
"Resolved %s/%s via platform field to digest %s.",
operating_system,
arch,
digest,
)
return f"{mcr_url}/{helm_mcr_repo}@{digest}"

# Annotations are on child manifests; fetch each one to match.
for entry in index.get("manifests", []):
digest = entry.get("digest", "")
try:
child_resp = http_client.get(
f"{base_api}/{digest}",
headers={"Accept": oci_accept},
timeout=30,
)
if child_resp.status_code != 200:
continue
child = child_resp.json()
title = child.get("annotations", {}).get(
"org.opencontainers.image.title", ""
)
if title.startswith(expected_title_prefix):
logger.debug(
"Resolved %s/%s via child annotation title '%s' to digest %s.",
operating_system,
arch,
title,
digest,
)
return f"{mcr_url}/{helm_mcr_repo}@{digest}"
except Exception: # pylint: disable=broad-except
continue

raise CLIInternalError(
f"Could not resolve helm binary for {operating_system}/{arch}. "
f"No matching entry found in manifest list '{manifest_list_tag}'."
)
except CLIInternalError:
raise
except Exception as e: # pylint: disable=broad-except
raise CLIInternalError(
f"Could not resolve helm binary for {operating_system}/{arch}. "
f"Manifest list resolution failed: {e}"
) from e
Copy link

Copilot AI Apr 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

New helper _resolve_helm_pull_target introduces non-trivial logic (HTTP probing, tag fallback, manifest/index parsing). There are existing unit tests under azext_connectedk8s/tests/unittests/, but this behavior is currently untested. Adding unit tests with mocked HTTP responses (arch tag exists / missing, manifest list with platform fields, manifest list requiring child annotation fetch, error paths) would help prevent regressions.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Auto-Assign Auto assign by bot Connected Kubernetes release-version-block Updates do not qualify release version rules. NOTE: please do not edit it manually.

Projects

None yet

Development

Successfully merging this pull request may close these issues.