Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 38 additions & 7 deletions src/azure-cli/azure/cli/command_modules/acs/custom.py
Original file line number Diff line number Diff line change
Expand Up @@ -2573,6 +2573,43 @@ def k8s_install_kubectl(cmd, client_version='latest', install_location=None, sou
install_dir, cli)


# get the latest version of kubelogin
def _get_latest_kubelogin_version(cloud_name, gh_token=None):
if cloud_name.lower() == 'azurechinacloud':
latest_release_url = 'https://mirror.azure.cn/kubernetes/kubelogin/latest'
logger.warning(
'No version specified, will get the latest version of kubelogin from "%s"', latest_release_url)
latest_release = _urlopen_read(latest_release_url, gh_token=gh_token)
return json.loads(latest_release)['tag_name'].strip()

latest_release_url = 'https://api.github.com/repos/Azure/kubelogin/releases/latest'
fallback_url = 'https://github.com/Azure/kubelogin/releases/latest/download/kubelogin-version.txt'
logger.warning(
'No version specified, will get the latest version of kubelogin from "%s"', latest_release_url)
try:
latest_release = _urlopen_read(latest_release_url, gh_token=gh_token)
return json.loads(latest_release)['tag_name'].strip()
except URLError as ex:
# the GitHub api answers with 403 or 429 when the rate limit is exceeded
if getattr(ex, 'code', None) not in (403, 429):
raise
logger.warning(
'The GitHub api rate limit was exceeded (%s), getting the latest version of kubelogin from "%s"',
ex, fallback_url)
try:
latest_version = _urlopen_read(fallback_url).decode('UTF-8', errors='replace').strip()
except OSError as fallback_ex:
raise ClientRequestError(
'Failed to get the latest version of kubelogin from "{}" ({}) and "{}" ({}).'.format(
latest_release_url, ex, fallback_url, fallback_ex),
recommendation='Please retry later, or specify a version with --kubelogin-version.')
if not re.fullmatch(r'v?\d+\.\d+\.\d+', latest_version):
raise ClientRequestError(
'Unexpected version "{}" returned by "{}".'.format(latest_version[:50], fallback_url),
recommendation='Please retry later, or specify a version with --kubelogin-version.')
return latest_version if latest_version.startswith('v') else 'v' + latest_version


# install kubelogin
def k8s_install_kubelogin(cmd, client_version='latest', install_location=None, source_url=None, arch=None, gh_token=None):
"""
Expand All @@ -2587,13 +2624,7 @@ def k8s_install_kubelogin(cmd, client_version='latest', install_location=None, s
source_url = 'https://mirror.azure.cn/kubernetes/kubelogin'

if client_version == 'latest':
latest_release_url = 'https://api.github.com/repos/Azure/kubelogin/releases/latest'
if cloud_name.lower() == 'azurechinacloud':
latest_release_url = 'https://mirror.azure.cn/kubernetes/kubelogin/latest'
logger.warning(
'No version specified, will get the latest version of kubelogin from "%s"', latest_release_url)
latest_release = _urlopen_read(latest_release_url, gh_token=gh_token)
client_version = json.loads(latest_release)['tag_name'].strip()
client_version = _get_latest_kubelogin_version(cloud_name, gh_token=gh_token)
else:
client_version = "v%s" % client_version

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import tempfile
import unittest
from unittest import mock
from urllib.error import HTTPError, URLError
import datetime
from dateutil.parser import parse

Expand All @@ -25,6 +26,7 @@
)
from azure.cli.command_modules.acs.custom import (
_get_command_context,
_get_latest_kubelogin_version,
_update_addons,
aks_agentpool_auto_scale_add,
aks_agentpool_auto_scale_delete,
Expand Down Expand Up @@ -848,6 +850,104 @@ def test_k8s_install_kubelogin_with_gh_token(self, logger_mock, mock_url_retriev
finally:
shutil.rmtree(temp_dir)

@mock.patch('azure.cli.command_modules.acs.custom._urlopen_read')
@mock.patch('azure.cli.command_modules.acs.custom._urlretrieve')
@mock.patch('azure.cli.command_modules.acs.custom.logger')
def test_k8s_install_kubelogin_latest_version_fallback(self, logger_mock, mock_url_retrieve, mock_urlopen_read):
"""Test that the version file is used to install kubelogin when the GitHub API is rate limited."""
mock_urlopen_read.side_effect = [
HTTPError('https://api.github.com/repos/Azure/kubelogin/releases/latest', 403, 'rate limited', None, None),
b'v0.0.30',
]
mock_url_retrieve.side_effect = create_kubelogin_zip

try:
temp_dir = tempfile.mkdtemp()
test_location = os.path.join(temp_dir, 'foo', 'kubelogin')

k8s_install_kubelogin(
mock.MagicMock(), client_version='latest', install_location=test_location,
arch="amd64", gh_token='ghp_test_token_123')

fallback_call = mock_urlopen_read.call_args_list[1]
self.assertEqual(
fallback_call[0][0],
'https://github.com/Azure/kubelogin/releases/latest/download/kubelogin-version.txt')
self.assertIsNone(fallback_call.kwargs.get('gh_token'))
mock_url_retrieve.assert_called_with(
MockUrlretrieveUrlValidator('https://github.com/Azure/kubelogin/releases/download', 'v0.0.30'),
mock.ANY)
self.assertTrue(
any('rate limit was exceeded' in str(call) for call in logger_mock.warning.call_args_list))
finally:
shutil.rmtree(temp_dir)

@mock.patch('azure.cli.command_modules.acs.custom._urlopen_read')
@mock.patch('azure.cli.command_modules.acs.custom.logger')
def test_get_latest_kubelogin_version_fallback_only_on_rate_limit(self, logger_mock, mock_urlopen_read):
"""Test that the version file is only used for a rate limit, other failures are surfaced as they are."""
api_url = 'https://api.github.com/repos/Azure/kubelogin/releases/latest'
cases = [
(HTTPError(api_url, 429, 'too many requests', None, None), True),
(HTTPError(api_url, 500, 'internal server error', None, None), False),
(URLError('[Errno -2] Name or service not known'), False),
]
for error, expect_fallback in cases:
with self.subTest(error=error):
mock_urlopen_read.reset_mock()
mock_urlopen_read.side_effect = [error, b'v0.0.30']

if expect_fallback:
self.assertEqual(_get_latest_kubelogin_version('azurecloud'), 'v0.0.30')
self.assertEqual(mock_urlopen_read.call_count, 2)
else:
with self.assertRaises(type(error)) as cm:
_get_latest_kubelogin_version('azurecloud')
self.assertIs(cm.exception, error)
mock_urlopen_read.assert_called_once()

@mock.patch('azure.cli.command_modules.acs.custom._urlopen_read')
@mock.patch('azure.cli.command_modules.acs.custom.logger')
def test_get_latest_kubelogin_version_fallback_without_tag_prefix(self, logger_mock, mock_urlopen_read):
"""Test that a bare version is normalized to the release tag used to build the download url."""
mock_urlopen_read.side_effect = [
HTTPError('https://api.github.com/repos/Azure/kubelogin/releases/latest', 403, 'rate limited', None, None),
b'0.0.30\n',
]

self.assertEqual(_get_latest_kubelogin_version('azurecloud'), 'v0.0.30')

@mock.patch('azure.cli.command_modules.acs.custom._urlopen_read')
@mock.patch('azure.cli.command_modules.acs.custom.logger')
def test_get_latest_kubelogin_version_all_sources_fail(self, logger_mock, mock_urlopen_read):
"""Test that both failures are reported when the GitHub API and the version file are unavailable."""
mock_urlopen_read.side_effect = [
HTTPError('https://api.github.com/repos/Azure/kubelogin/releases/latest', 403, 'rate limited', None, None),
HTTPError('https://github.com/Azure/kubelogin/releases/latest/download/kubelogin-version.txt',
500, 'internal server error', None, None),
]

with self.assertRaises(ClientRequestError) as cm:
_get_latest_kubelogin_version('azurecloud')
self.assertIn('403', str(cm.exception))
self.assertIn('500', str(cm.exception))

@mock.patch('azure.cli.command_modules.acs.custom._urlopen_read')
@mock.patch('azure.cli.command_modules.acs.custom.logger')
def test_get_latest_kubelogin_version_unexpected_fallback_content(self, logger_mock, mock_urlopen_read):
"""Test that content which is not exactly a version is rejected, not used to build the download url."""
for content in (b'<html>not found</html>', b'v0.0.30/../../evil', b'\xff\xfe\x00binary'):
with self.subTest(content=content):
mock_urlopen_read.reset_mock()
mock_urlopen_read.side_effect = [
HTTPError('https://api.github.com/repos/Azure/kubelogin/releases/latest',
403, 'rate limited', None, None),
content,
]

with self.assertRaises(ClientRequestError):
_get_latest_kubelogin_version('azurecloud')

@mock.patch('azure.cli.command_modules.acs.addonconfiguration.get_rg_location', return_value='eastus')
@mock.patch('azure.cli.command_modules.acs.addonconfiguration.get_resource_groups_client', autospec=True)
@mock.patch('azure.cli.command_modules.acs.addonconfiguration.get_resources_client', autospec=True)
Expand Down
Loading