@@ -304,6 +304,7 @@ def create_connectedk8s(
304304 try :
305305 kubectl_client_location = install_kubectl_client ()
306306 helm_client_location = install_helm_client (cmd )
307+ logger .debug ("Using helm binary: %s" , helm_client_location )
307308 except Exception as e :
308309 raise CLIInternalError (
309310 f"An exception has occured while trying to perform kubectl or helm install: { e } "
@@ -1251,6 +1252,134 @@ def check_kube_connection() -> str:
12511252 assert False
12521253
12531254
1255+ def _resolve_helm_pull_target (
1256+ mcr_url : str ,
1257+ helm_mcr_repo : str ,
1258+ helm_version : str ,
1259+ operating_system : str ,
1260+ arch : str ,
1261+ ) -> str :
1262+ """Return the ORAS pull target for the helm binary.
1263+
1264+ Tries the arch-specific tag first (e.g. ``helm-v3.20.1-linux-arm64``).
1265+ If that tag does not exist, falls back to the manifest list tag
1266+ (``helm-v3.20.1``) and resolves the correct entry by matching the
1267+ ``org.opencontainers.image.title`` annotation on each child manifest.
1268+
1269+ Uses the OCI Distribution v2 HTTP API directly so that the logic is
1270+ independent of the ``oras`` library version installed.
1271+
1272+ :param mcr_url: MCR hostname (e.g. ``mcr.microsoft.com``)
1273+ :param helm_mcr_repo: repository path within MCR (e.g. ``azurearck8s/helm``)
1274+ :param helm_version: helm version string including the leading ``v`` (e.g. ``v3.20.1``)
1275+ :param operating_system: lower-case OS name: ``linux``, ``darwin``, or ``windows``
1276+ :param arch: CPU architecture: ``amd64`` or ``arm64``
1277+ :returns: full ORAS pull target string (tag-based or digest-based)
1278+ """
1279+ import requests as http_client # pylint: disable=import-outside-toplevel
1280+
1281+ arch_specific_tag = f"helm-{ helm_version } -{ operating_system } -{ arch } "
1282+ arch_specific_target = f"{ mcr_url } /{ helm_mcr_repo } :{ arch_specific_tag } "
1283+ base_api = f"https://{ mcr_url } /v2/{ helm_mcr_repo } /manifests"
1284+
1285+ # OCI media types required by MCR (HEAD/GET return 404 without Accept).
1286+ oci_accept = (
1287+ "application/vnd.oci.image.manifest.v1+json, "
1288+ "application/vnd.oci.image.index.v1+json"
1289+ )
1290+
1291+ # Check whether the arch-specific tag exists.
1292+ try :
1293+ response = http_client .head (
1294+ f"{ base_api } /{ arch_specific_tag } " ,
1295+ headers = {"Accept" : oci_accept },
1296+ timeout = 30 ,
1297+ )
1298+ if response .status_code == 200 :
1299+ return arch_specific_target
1300+ logger .debug (
1301+ "Arch-specific tag %s returned HTTP %d; trying manifest list." ,
1302+ arch_specific_tag ,
1303+ response .status_code ,
1304+ )
1305+ except Exception as e : # pylint: disable=broad-except
1306+ logger .debug (
1307+ "Arch-specific tag check failed (%s); trying manifest list." ,
1308+ e ,
1309+ )
1310+
1311+ # Fall back to the manifest list tag and match via annotation title.
1312+ # Annotations live on each child manifest, not on the index entries,
1313+ # so we must fetch every child manifest to find the right one.
1314+ manifest_list_tag = f"helm-{ helm_version } "
1315+ expected_title_prefix = f"helm-{ helm_version } -{ operating_system } -{ arch } "
1316+ try :
1317+ response = http_client .get (
1318+ f"{ base_api } /{ manifest_list_tag } " ,
1319+ headers = {"Accept" : oci_accept },
1320+ timeout = 30 ,
1321+ )
1322+ if response .status_code != 200 :
1323+ raise CLIInternalError (
1324+ f"Could not resolve helm binary for { operating_system } /{ arch } . "
1325+ f"Arch-specific tag '{ arch_specific_tag } ' check failed and "
1326+ f"manifest list '{ manifest_list_tag } ' returned HTTP { response .status_code } ."
1327+ )
1328+
1329+ index = response .json ()
1330+ for entry in index .get ("manifests" , []):
1331+ # Check platform fields if present (future-proof).
1332+ plat = entry .get ("platform" , {})
1333+ if plat .get ("os" ) == operating_system and plat .get ("architecture" ) == arch :
1334+ digest = entry ["digest" ]
1335+ logger .debug (
1336+ "Resolved %s/%s via platform field to digest %s." ,
1337+ operating_system ,
1338+ arch ,
1339+ digest ,
1340+ )
1341+ return f"{ mcr_url } /{ helm_mcr_repo } @{ digest } "
1342+
1343+ # Annotations are on child manifests; fetch each one to match.
1344+ for entry in index .get ("manifests" , []):
1345+ digest = entry .get ("digest" , "" )
1346+ try :
1347+ child_resp = http_client .get (
1348+ f"{ base_api } /{ digest } " ,
1349+ headers = {"Accept" : oci_accept },
1350+ timeout = 30 ,
1351+ )
1352+ if child_resp .status_code != 200 :
1353+ continue
1354+ child = child_resp .json ()
1355+ title = child .get ("annotations" , {}).get (
1356+ "org.opencontainers.image.title" , ""
1357+ )
1358+ if title .startswith (expected_title_prefix ):
1359+ logger .debug (
1360+ "Resolved %s/%s via child annotation title '%s' to digest %s." ,
1361+ operating_system ,
1362+ arch ,
1363+ title ,
1364+ digest ,
1365+ )
1366+ return f"{ mcr_url } /{ helm_mcr_repo } @{ digest } "
1367+ except Exception : # pylint: disable=broad-except
1368+ continue
1369+
1370+ raise CLIInternalError (
1371+ f"Could not resolve helm binary for { operating_system } /{ arch } . "
1372+ f"No matching entry found in manifest list '{ manifest_list_tag } '."
1373+ )
1374+ except CLIInternalError :
1375+ raise
1376+ except Exception as e : # pylint: disable=broad-except
1377+ raise CLIInternalError (
1378+ f"Could not resolve helm binary for { operating_system } /{ arch } . "
1379+ f"Manifest list resolution failed: { e } "
1380+ ) from e
1381+
1382+
12541383def install_helm_client (cmd : CLICommand ) -> str :
12551384 print (
12561385 f"Step: { utils .get_utctimestring ()} : Install Helm client if it does not exist"
@@ -1263,6 +1392,7 @@ def install_helm_client(cmd: CLICommand) -> str:
12631392 # Fetch system related info
12641393 operating_system = platform .system ().lower ()
12651394 machine_type = platform .machine ()
1395+ arch = "arm64" if machine_type .lower () in ("aarch64" , "arm64" ) else "amd64"
12661396
12671397 # Send machine telemetry
12681398 telemetry .add_extension_event (
@@ -1271,20 +1401,18 @@ def install_helm_client(cmd: CLICommand) -> str:
12711401 # Set helm binary download & install locations
12721402 if operating_system == "windows" :
12731403 download_location_string = f".azure\\ helm\\ { consts .HELM_VERSION } "
1274- download_file_name = f"helm-{ consts .HELM_VERSION } -{ operating_system } -amd64 .zip"
1404+ download_file_name = f"helm-{ consts .HELM_VERSION } -{ operating_system } -{ arch } .zip"
12751405 install_location_string = (
1276- f".azure\\ helm\\ { consts .HELM_VERSION } \\ { operating_system } -amd64 \\ helm.exe"
1406+ f".azure\\ helm\\ { consts .HELM_VERSION } \\ { operating_system } -{ arch } \\ helm.exe"
12771407 )
1278- artifactTag = f"helm-{ consts .HELM_VERSION } -{ operating_system } -amd64"
12791408 elif operating_system == "linux" or operating_system == "darwin" :
12801409 download_location_string = f".azure/helm/{ consts .HELM_VERSION } "
12811410 download_file_name = (
1282- f"helm-{ consts .HELM_VERSION } -{ operating_system } -amd64 .tar.gz"
1411+ f"helm-{ consts .HELM_VERSION } -{ operating_system } -{ arch } .tar.gz"
12831412 )
12841413 install_location_string = (
1285- f".azure/helm/{ consts .HELM_VERSION } /{ operating_system } -amd64 /helm"
1414+ f".azure/helm/{ consts .HELM_VERSION } /{ operating_system } -{ arch } /helm"
12861415 )
1287- artifactTag = f"helm-{ consts .HELM_VERSION } -{ operating_system } -amd64"
12881416 else :
12891417 telemetry .set_exception (
12901418 exception = "Unsupported OS for installing helm client" ,
@@ -1296,15 +1424,15 @@ def install_helm_client(cmd: CLICommand) -> str:
12961424 )
12971425
12981426 download_location = os .path .expanduser (os .path .join ("~" , download_location_string ))
1299- download_dir = os .path .dirname (download_location )
13001427 install_location = os .path .expanduser (os .path .join ("~" , install_location_string ))
13011428
13021429 # Download compressed Helm binary if not already present
13031430 if not os .path .isfile (install_location ):
1304- # Creating the helm folder if it doesnt exist
1305- if not os .path .exists (download_dir ):
1431+ # The archive is downloaded to ~/.azure/helm/<version>/<archive-file>.
1432+ # Ensure the <version> directory exists first to avoid file-not-found errors.
1433+ if not os .path .exists (download_location ):
13061434 try :
1307- os .makedirs (download_dir )
1435+ os .makedirs (download_location )
13081436 except Exception as e :
13091437 telemetry .set_exception (
13101438 exception = e ,
@@ -1318,15 +1446,23 @@ def install_helm_client(cmd: CLICommand) -> str:
13181446 "Downloading helm client for first time. This can take few minutes..."
13191447 )
13201448
1449+ retry_count = 3
1450+ retry_delay = 5
1451+ # Helm binaries are downloaded from MCR artifacts for all architectures.
13211452 mcr_url = utils .get_mcr_path (cmd .cli_ctx .cloud .endpoints .active_directory )
13221453
13231454 client = oras .client .OrasClient (hostname = mcr_url )
1324- retry_count = 3
1325- retry_delay = 5
1455+ pull_target = _resolve_helm_pull_target (
1456+ mcr_url ,
1457+ consts .HELM_MCR_URL ,
1458+ consts .HELM_VERSION ,
1459+ operating_system ,
1460+ arch ,
1461+ )
13261462 for i in range (retry_count ):
13271463 try :
13281464 client .pull (
1329- target = f" { mcr_url } / { consts . HELM_MCR_URL } : { artifactTag } " ,
1465+ target = pull_target ,
13301466 outdir = download_location ,
13311467 )
13321468 break
0 commit comments