diff --git a/src/aks-preview/HISTORY.rst b/src/aks-preview/HISTORY.rst index b0cc448c1b7..c59d7a48aed 100644 --- a/src/aks-preview/HISTORY.rst +++ b/src/aks-preview/HISTORY.rst @@ -11,6 +11,7 @@ To release a new version, please select a new version number (usually plus 1 to Pending +++++++ +* `az aks get-credentials`: Convert device code mode kubeconfig to Azure CLI token format to bypass conditional access login blocks. 19.0.0b4 +++++++ diff --git a/src/aks-preview/azext_aks_preview/_helpers.py b/src/aks-preview/azext_aks_preview/_helpers.py index 3f6fa436665..1f035643969 100644 --- a/src/aks-preview/azext_aks_preview/_helpers.py +++ b/src/aks-preview/azext_aks_preview/_helpers.py @@ -97,6 +97,37 @@ def print_or_merge_credentials(path, kubeconfig, overwrite_existing, context_nam os.remove(temp_path) +def uses_kubelogin_devicecode(kubeconfig: str) -> bool: + try: + config = yaml.safe_load(kubeconfig) + + # Check if users section exists and has at least one user + if not config or not config.get('users') or len(config['users']) == 0: + return False + + first_user = config['users'][0] + user_info = first_user.get('user', {}) + exec_info = user_info.get('exec', {}) + + # Check if command is kubelogin + command = exec_info.get('command', '') + if 'kubelogin' not in command: + return False + + # Check if args contains --login and devicecode + args = exec_info.get('args', []) + # Join args into a string for easier pattern matching + args_str = ' '.join(args) + # Check for '--login devicecode' or '-l devicecode' + if '--login devicecode' in args_str or '-l devicecode' in args_str: + return True + return False + except (yaml.YAMLError, KeyError, TypeError, AttributeError) as e: + # If there's any error parsing the kubeconfig, assume it doesn't require kubelogin + logger.debug("Error parsing kubeconfig: %s", str(e)) + return False + + def _merge_kubernetes_configurations(existing_file, addition_file, replace, context_name=None): existing = _load_kubernetes_configuration(existing_file) addition = _load_kubernetes_configuration(addition_file) diff --git a/src/aks-preview/azext_aks_preview/custom.py b/src/aks-preview/azext_aks_preview/custom.py index d275c080cae..4d8e3e5ef3b 100644 --- a/src/aks-preview/azext_aks_preview/custom.py +++ b/src/aks-preview/azext_aks_preview/custom.py @@ -14,6 +14,7 @@ import threading import time import webbrowser +import subprocess from azext_aks_preview._client_factory import ( CUSTOM_MGMT_AKS_PREVIEW, @@ -72,6 +73,8 @@ get_all_extensions_in_allow_list, raise_validation_error_if_extension_type_not_in_allow_list, get_extension_in_allow_list, + uses_kubelogin_devicecode, + which, ) from azext_aks_preview._podidentity import ( _ensure_managed_identity_operator_permission, @@ -1526,6 +1529,29 @@ def aks_get_credentials( encoding='UTF-8') print_or_merge_credentials( path, kubeconfig, overwrite_existing, context_name) + # Check if kubeconfig requires kubelogin with devicecode and convert it + if uses_kubelogin_devicecode(kubeconfig): + if which("kubelogin"): + try: + # Run kubelogin convert-kubeconfig -l azurecli + subprocess.run( + ["kubelogin", "convert-kubeconfig", "-l", "azurecli"], + cwd=os.path.dirname(path), + check=True, + ) + logger.warning("Converted kubeconfig to use Azure CLI authentication.") + except subprocess.CalledProcessError as e: + logger.warning("Failed to convert kubeconfig with kubelogin: %s", str(e)) + except Exception as e: # pylint: disable=broad-except + logger.warning("Error running kubelogin: %s", str(e)) + else: + logger.warning( + "The kubeconfig uses devicecode authentication which requires kubelogin. " + "Please install kubelogin from https://github.com/Azure/kubelogin or run " + "'az aks install-cli' to install both kubectl and kubelogin. " + "If devicecode login fails, try running " + "'kubelogin convert-kubeconfig -l azurecli' to unblock yourself." + ) except (IndexError, ValueError) as exc: raise CLIError("Fail to find kubeconfig file.") from exc diff --git a/src/aks-preview/azext_aks_preview/tests/latest/test_aks_commands.py b/src/aks-preview/azext_aks_preview/tests/latest/test_aks_commands.py index 631d4d41899..e88a890960e 100644 --- a/src/aks-preview/azext_aks_preview/tests/latest/test_aks_commands.py +++ b/src/aks-preview/azext_aks_preview/tests/latest/test_aks_commands.py @@ -5321,6 +5321,17 @@ def test_aks_automatic_sku(self, resource_group, resource_group_location): ], ) + # get-credentials + fd, temp_path = tempfile.mkstemp() + self.kwargs.update({'file': temp_path}) + try: + self.cmd( + 'aks get-credentials -g {resource_group} -n {name} --file "{file}"') + self.assertGreater(os.path.getsize(temp_path), 0) + finally: + os.close(fd) + os.remove(temp_path) + # scale the cluster scale_cluster_cmd = ( "aks scale --resource-group={resource_group} --name={name} "