Skip to content
10 changes: 10 additions & 0 deletions src/connectedk8s/azext_connectedk8s/_constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,16 @@
No_Param_Error = "No parameters were specified with update command. Please run az connectedk8s update --help to check parameters available for update"
Gateway_ArmId_Is_Invalid = "The provided Gateway ArmID in --gateway-resource-id {} is invalid. Please provide a valid Gateway ArmID."
EnableProxy_Conflict_Error = "Conflict detected: --disable-proxy can not be set with --https-proxy, --http-proxy, --proxy-skip-range and --proxy-cert at the same time. Please run az connectedk8s update --help for more information about the parameters"

# --proxy-skip-range keyword that expands to the Azure Arc private-link endpoints.
Comment thread
Tareesh21 marked this conversation as resolved.
Proxy_Skip_Range_Arc_Keyword = "arc"
# Arc private-link endpoint host suffixes the "arc" keyword expands to.
Arc_Private_Link_Endpoints = [
".his.arc.azure.{cloud_based_domain}",
".dp.kubernetesconfiguration.azure.{cloud_based_domain}",
".guestconfiguration.azure.{cloud_based_domain}",
]

Manual_Upgrade_Called_In_Auto_Update_Enabled = (
"Manual Upgrade was called while in auto_Update enabled mode"
)
Expand Down
4 changes: 2 additions & 2 deletions src/connectedk8s/azext_connectedk8s/_help.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
- name: Onboard a connected kubernetes cluster by specifying the kubeconfig and kubecontext.
text: az connectedk8s connect -g resourceGroupName -n connectedClusterName --kube-config /path/to/kubeconfig --kube-context kubeContextName
- name: Onboard a connected kubernetes cluster by specifying the https proxy, http proxy, no proxy settings.
text: az connectedk8s connect -g resourceGroupName -n connectedClusterName --proxy-https https://proxy-url --proxy-http http://proxy-url --proxy-skip-range excludedIP,excludedCIDR,exampleCIDRfollowed,10.0.0.0/24
text: az connectedk8s connect -g resourceGroupName -n connectedClusterName --proxy-https https://proxy-url --proxy-http http://proxy-url --proxy-skip-range excludedIP,excludedCIDR,exampleCIDRfollowed,10.0.0.0/24,Arc
- name: Onboard a connected kubernetes cluster by specifying the https proxy, http proxy, no proxy with cert settings.
text: az connectedk8s connect -g resourceGroupName -n connectedClusterName --proxy-cert /path/to/crt --proxy-https https://proxy-url --proxy-http http://proxy-url --proxy-skip-range excludedIP,excludedCIDR,exampleCIDRfollowed,10.0.0.0/24
- name: Onboard a connected kubernetes cluster with private link feature enabled by specifying private link parameters.
Expand All @@ -46,7 +46,7 @@
short-summary: Update properties of the arc onboarded kubernetes cluster.
examples:
- name: Update proxy values for the agents
text: az connectedk8s update -g resourceGroupName -n connectedClusterName --proxy-cert /path/to/crt --proxy-https https://proxy-url --proxy-http http://proxy-url --proxy-skip-range excludedIP,excludedCIDR,exampleCIDRfollowed,10.0.0.0/24
text: az connectedk8s update -g resourceGroupName -n connectedClusterName --proxy-cert /path/to/crt --proxy-https https://proxy-url --proxy-http http://proxy-url --proxy-skip-range excludedIP,excludedCIDR,exampleCIDRfollowed,10.0.0.0/24,Arc
- name: Disable proxy settings for agents
Comment thread
Tareesh21 marked this conversation as resolved.
text: az connectedk8s update -g resourceGroupName -n connectedClusterName --disable-proxy
- name: Disable auto-upgrade of agents
Expand Down
6 changes: 4 additions & 2 deletions src/connectedk8s/azext_connectedk8s/_params.py
Comment thread
Tareesh21 marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,8 @@ def load_arguments(self: Connectedk8sCommandsLoader, _: CLICommand) -> None: #
"no_proxy",
options_list=["--proxy-skip-range"],
arg_group="Proxy",
help="List of URLs/CIDRs for which proxy should not to be used.",
help="List of URLs/CIDRs for which proxy should not be used. Pass the "
"keyword 'Arc' to bypass the proxy for the linked Azure Arc private-link endpoints.",
)
c.argument(
"proxy_cert",
Expand Down Expand Up @@ -260,7 +261,8 @@ def load_arguments(self: Connectedk8sCommandsLoader, _: CLICommand) -> None: #
"no_proxy",
options_list=["--proxy-skip-range"],
arg_group="Proxy",
help="List of URLs/CIDRs for which proxy should not to be used.",
help="List of URLs/CIDRs for which proxy should not be used. Pass the "
"keyword 'Arc' to bypass the proxy for the linked Azure Arc private-link endpoints.",
)
c.argument(
"distribution",
Expand Down
51 changes: 50 additions & 1 deletion src/connectedk8s/azext_connectedk8s/custom.py
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,9 @@ def create_connectedk8s(

print(f"Step: {utils.get_utctimestring()}: Escape Proxy Settings, if passed in")

# Expand --proxy-skip-range service keywords (e.g. "Arc") before escaping.
no_proxy = expand_proxy_skip_range_keywords(cmd, no_proxy)

# Escaping comma, forward slash present in https proxy urls, needed for helm params.
https_proxy = escape_proxy_settings(https_proxy)

Expand Down Expand Up @@ -1321,6 +1324,45 @@ def escape_proxy_settings(proxy_setting: str | None) -> str:
return proxy_setting


def get_arc_proxy_skip_range_endpoints(cmd: CLICommand) -> list[str]:
Comment thread
jianyunt marked this conversation as resolved.
# Arc private-link data-plane hosts to bypass the proxy. Leading-dot form matches
# every region; suffix is derived so it works across public and sovereign clouds.
cloud_based_domain = get_cloud_based_domain(cmd)
return [
endpoint.format(cloud_based_domain=cloud_based_domain)
for endpoint in consts.Arc_Private_Link_Endpoints
]
Comment thread
Tareesh21 marked this conversation as resolved.


def expand_proxy_skip_range_keywords(cmd: CLICommand, no_proxy: str) -> str:
# Replace the "Arc" keyword (case-insensitive) with the Arc private-link endpoints,
# keeping all other entries in order; returns the value unchanged if no keyword.
if not no_proxy:
return no_proxy

entries = no_proxy.split(",")
if not any(
entry.strip().lower() == consts.Proxy_Skip_Range_Arc_Keyword
for entry in entries
):
return no_proxy

expanded: list[str] = []
seen: set[str] = set()
for entry in entries:
stripped = entry.strip()
if stripped.lower() == consts.Proxy_Skip_Range_Arc_Keyword:
for endpoint in get_arc_proxy_skip_range_endpoints(cmd):
if endpoint.lower() not in seen:
seen.add(endpoint.lower())
expanded.append(endpoint)
elif stripped and stripped.lower() not in seen:
seen.add(stripped.lower())
expanded.append(stripped)

return ",".join(expanded)


def check_kube_connection() -> str:
print(f"Step: {utils.get_utctimestring()}: Checking Connectivity to Cluster")
api_instance = kube_client.VersionApi()
Expand Down Expand Up @@ -1627,7 +1669,7 @@ def connected_cluster_exists(
return True


def get_default_config_dp_endpoint(cmd: CLICommand, location: str) -> str:
def get_cloud_based_domain(cmd: CLICommand) -> 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]
Expand All @@ -1642,7 +1684,11 @@ def get_default_config_dp_endpoint(cmd: CLICommand, location: str) -> str:
+ "."
+ active_directory_array[4]
)
return cloud_based_domain


def get_default_config_dp_endpoint(cmd: CLICommand, location: str) -> str:
cloud_based_domain = get_cloud_based_domain(cmd)
config_dp_endpoint = (
f"https://{location}.dp.kubernetesconfiguration.azure.{cloud_based_domain}"
)
Expand Down Expand Up @@ -2439,6 +2485,9 @@ def update_connected_cluster(
# Setting kubeconfig
kube_config = set_kube_config(kube_config)

# Expand --proxy-skip-range service keywords (e.g. "Arc") before escaping.
no_proxy = expand_proxy_skip_range_keywords(cmd, no_proxy)

# Escaping comma, forward slash present in https proxy urls, needed for helm params.
https_proxy = escape_proxy_settings(https_proxy)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,17 @@
import os
Comment thread
Tareesh21 marked this conversation as resolved.
import sys
from typing import Dict, Optional
from unittest.mock import MagicMock

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
from azext_connectedk8s.custom import (
expand_proxy_skip_range_keywords,
get_kubernetes_distro,
get_kubernetes_infra,
)


def create_node(
Expand Down Expand Up @@ -86,3 +91,91 @@ 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"


# --------------------- Tests for expand_proxy_skip_range_keywords ---------------------
def _proxy_cmd(active_directory="https://login.microsoftonline.com"):
cmd = MagicMock()
cmd.cli_ctx.cloud.endpoints.active_directory = active_directory
return cmd


ARC_PUBLIC = (
".his.arc.azure.com,"
".dp.kubernetesconfiguration.azure.com,"
".guestconfiguration.azure.com"
)


def test_expand_arc_keyword_public_cloud():
assert expand_proxy_skip_range_keywords(_proxy_cmd(), "Arc") == ARC_PUBLIC


@pytest.mark.parametrize("keyword", ["Arc", "arc", "ARC", " aRc "])
def test_expand_arc_keyword_is_case_and_space_insensitive(keyword):
assert expand_proxy_skip_range_keywords(_proxy_cmd(), keyword) == ARC_PUBLIC


def test_expand_arc_keyword_preserves_other_entries():
out = expand_proxy_skip_range_keywords(_proxy_cmd(), "Arc,10.0.0.0/16,.svc")
assert out == ARC_PUBLIC + ",10.0.0.0/16,.svc"


def test_expand_arc_keyword_china_cloud():
cmd = _proxy_cmd("https://login.chinacloudapi.cn")
out = expand_proxy_skip_range_keywords(cmd, "Arc")
assert out == (
".his.arc.azure.cn,"
".dp.kubernetesconfiguration.azure.cn,"
".guestconfiguration.azure.cn"
)


def test_expand_arc_keyword_usgov_cloud():
Comment thread
Tareesh21 marked this conversation as resolved.
cmd = _proxy_cmd("https://login.microsoftonline.us")
out = expand_proxy_skip_range_keywords(cmd, "Arc")

@jianyunt jianyunt Jul 31, 2026 •

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

"Arc")

Also add cases like

"Arc, arc", ==> "arc"

"Arc, .his.ARC.azure.com" ===> "arc, .his.arc.azure.com"

de-dup is needed

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Updated.
Now for "Arc, .his.ARC.azure.com" ===> "arc", because his.ARC.azure.com is one of the endpoints in "arc"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

ok

assert out == (
".his.arc.azure.us,"
".dp.kubernetesconfiguration.azure.us,"
".guestconfiguration.azure.us"
)


def test_expand_arc_keyword_ussec_cloud():
cmd = _proxy_cmd("https://login.microsoftonline.microsoft.scloud")
out = expand_proxy_skip_range_keywords(cmd, "Arc")
assert out == (
".his.arc.azure.microsoft.scloud,"
".dp.kubernetesconfiguration.azure.microsoft.scloud,"
".guestconfiguration.azure.microsoft.scloud"
)


def test_expand_arc_keyword_usnat_cloud():
cmd = _proxy_cmd("https://login.microsoftonline.eaglex.ic.gov")
out = expand_proxy_skip_range_keywords(cmd, "Arc")
assert out == (
".his.arc.azure.eaglex.ic.gov,"
".dp.kubernetesconfiguration.azure.eaglex.ic.gov,"
".guestconfiguration.azure.eaglex.ic.gov"
)


def test_expand_no_keyword_returns_unchanged():
val = "10.0.0.0/16,.svc,localhost"
assert expand_proxy_skip_range_keywords(_proxy_cmd(), val) == val


def test_expand_empty_returns_unchanged():
assert expand_proxy_skip_range_keywords(_proxy_cmd(), "") == ""


def test_expand_arc_keyword_deduplicates():
out = expand_proxy_skip_range_keywords(_proxy_cmd(), "Arc,Arc")
assert out == ARC_PUBLIC


def test_expand_arc_keyword_dedups_case_insensitive_endpoint():
# A user endpoint differing only in case is not duplicated in NO_PROXY.
out = expand_proxy_skip_range_keywords(_proxy_cmd(), "Arc, .his.ARC.azure.com")
assert out == ARC_PUBLIC
30 changes: 30 additions & 0 deletions testing/test/configurations/Proxy.Tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,36 @@ Describe 'Proxy Scenario' {
$n | Should -BeLessOrEqual $MAX_RETRY_ATTEMPTS
}

It 'Expands the Arc keyword in --proxy-skip-range to the Azure Arc private-link endpoints' {
az connectedk8s update -n $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --proxy-skip-range Arc

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

update

do we want to connect scenario too?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Connect and update both call the same function. Since we've already added the update scenario with Arc, adding a connect scenario here would just duplicate the same logic.

$? | Should -BeTrue
Start-Sleep -Seconds 10

# Loop and retry until the Arc keyword expands into the no-proxy list
$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) {
$noProxy = helm get values -n azure-arc-release azure-arc -o yaml | grep noProxy
Write-Host "$noProxy"
if ($noProxy -match "\.his\.arc\.azure") {
break
}
}
Start-Sleep -Seconds 10
$n += 1
} while ($n -le $MAX_RETRY_ATTEMPTS)
$n | Should -BeLessOrEqual $MAX_RETRY_ATTEMPTS

$noProxy = helm get values -n azure-arc-release azure-arc -o yaml | grep noProxy
$noProxy | Should -Match "\.his\.arc\.azure"
$noProxy | Should -Match "\.dp\.kubernetesconfiguration\.azure"
$noProxy | Should -Match "\.guestconfiguration\.azure"
}

It 'Disable proxy' {
az connectedk8s update -n $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --disable-proxy
$? | Should -BeTrue
Expand Down
Loading