Skip to content

Commit 98e4807

Browse files
committed
[connectedk8s] Update extension CLI to v1.11.1
- Added ARM64 support for Helm binary installation - Handle removal of '--all' flag in Helm 4 for compatibility - Added Helm overrides support for Application Gateway for Containers (AGC) - Updated CSP version - Updated CLIENT_PROXY_VERSION to 1.3.033892 - Updated pre-diagnostics version - Added debug log for helm binary location
1 parent faf418d commit 98e4807

5 files changed

Lines changed: 187 additions & 20 deletions

File tree

src/connectedk8s/HISTORY.rst

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,15 @@
22
33
Release History
44
===============
5+
1.11.1
6+
+++++++
7+
* Added ARM64 support for Helm binary installation.
8+
* Handle removal of '--all' flag in Helm 4 to ensure compatibility.
9+
* Added Helm overrides support for Application Gateway for Containers (AGC).
10+
* Updated CSP version.
11+
* Updated CLIENT_PROXY_VERSION to 1.3.033892.
12+
* Updated pre-diagnostics version.
13+
514
1.11.0
615
+++++
716
* [Breaking Change] Removed deprecated '--app-id' and '--app-secret' RBAC parameters from the extension.

src/connectedk8s/azext_connectedk8s/_constants.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -476,7 +476,7 @@
476476
)
477477
DNS_Check_Result_String = "DNS Result:"
478478
AZ_CLI_ADAL_TO_MSAL_MIGRATE_VERSION = "2.30.0"
479-
CLIENT_PROXY_VERSION = "1.3.032281"
479+
CLIENT_PROXY_VERSION = "1.3.033892"
480480
CLIENT_PROXY_FOLDER = ".clientproxy"
481481
API_SERVER_PORT = 47011
482482
CLIENT_PROXY_PORT = 47010
@@ -491,7 +491,7 @@
491491
# URL constants
492492
CLIENT_PROXY_MCR_TARGET = "azureconnectivity/proxy"
493493
HELM_MCR_URL = "azurearck8s/helm"
494-
HELM_VERSION = "v3.12.2"
494+
HELM_VERSION = "v3.20.1"
495495
Download_And_Install_Kubectl_Fault_Type = "Failed to download and install kubectl"
496496
Azure_Access_Token_Variable = "AZURE_ACCESS_TOKEN"
497497
Provisioned_Cluster_Kind = "provisionedcluster"

src/connectedk8s/azext_connectedk8s/_utils.py

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -104,11 +104,11 @@ def validate_connect_rp_location(cmd: CLICommand, location: str) -> None:
104104
"Failed to fetch resource provider details",
105105
)
106106

107-
for resourceTypes in providerDetails.resource_types: # type: ignore[attr-defined]
107+
for resourceTypes in providerDetails.resource_types: # type: ignore[union-attr]
108108
if resourceTypes.resource_type == "connectedClusters":
109109
rp_locations = [
110110
location.replace(" ", "").lower()
111-
for location in resourceTypes.locations
111+
for location in resourceTypes.locations # type: ignore[union-attr]
112112
]
113113
if location.lower() not in rp_locations:
114114
telemetry.set_exception(
@@ -1315,7 +1315,7 @@ def helm_install_release(
13151315
]
13161316

13171317
# Special configurations from 2022-09-01 ARM metadata.
1318-
# "dataplaneEndpoints" property does not appear in arm_metadata structure for public and AGC clouds.
1318+
# "dataplaneEndpoints" does not appear in arm_metadata for public and AGC
13191319
if "dataplaneEndpoints" in arm_metadata:
13201320
if "arcConfigEndpoint" in arm_metadata["dataplaneEndpoints"]:
13211321
notification_endpoint = arm_metadata["dataplaneEndpoints"][
@@ -1484,6 +1484,25 @@ def redact_sensitive_fields_from_string(input_text: str) -> str:
14841484
return input_text
14851485

14861486

1487+
def get_helm_major_version(helm_client_location: str) -> int:
1488+
"""Returns the major version of the helm client (e.g. 3 or 4)."""
1489+
try:
1490+
result = Popen(
1491+
[helm_client_location, "version", "--short"],
1492+
stdout=PIPE,
1493+
stderr=PIPE,
1494+
)
1495+
out, _ = result.communicate()
1496+
version_str = out.decode("ascii").strip()
1497+
# version_str is like "v3.17.0+gabcdef" or "v4.1.3+gabcdef"
1498+
match = re.match(r"v(\d+)\.", version_str)
1499+
if match:
1500+
return int(match.group(1))
1501+
except (OSError, ValueError):
1502+
pass
1503+
return 3 # assume Helm 3 if we cannot determine version
1504+
1505+
14871506
def get_release_namespace(
14881507
kube_config: str | None,
14891508
kube_context: str | None,
@@ -1494,11 +1513,14 @@ def get_release_namespace(
14941513
cmd_helm_release = [
14951514
helm_client_location,
14961515
"list",
1497-
"-a",
14981516
"--all-namespaces",
14991517
"--output",
15001518
"json",
15011519
]
1520+
# Helm 4 removed the --all flag (all releases are shown by default).
1521+
# Helm 3 requires --all to include non-deployed releases.
1522+
if get_helm_major_version(helm_client_location) < 4:
1523+
cmd_helm_release.insert(2, "--all")
15021524
if kube_config:
15031525
cmd_helm_release.extend(["--kubeconfig", kube_config])
15041526
if kube_context:

src/connectedk8s/azext_connectedk8s/custom.py

Lines changed: 149 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
12541383
def 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

src/connectedk8s/setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
# TODO: Confirm this is the right version number you want and it matches your
1414
# HISTORY.rst entry.
1515

16-
VERSION = "1.11.0"
16+
VERSION = "1.11.1"
1717

1818
# The full list of classifiers is available at
1919
# https://pypi.python.org/pypi?%3Aaction=list_classifiers

0 commit comments

Comments
 (0)