From ed8437127887f172cba12849c5ab8d7caa9d890d Mon Sep 17 00:00:00 2001 From: Aleksandra Ovchinnikova Date: Mon, 7 Aug 2023 14:13:56 -0700 Subject: [PATCH] LITE-28324 Fix --version check for updates --- connect/cli/core/utils.py | 42 +++++- .../cli/plugins/project/extension/helpers.py | 3 +- .../cli/plugins/project/extension/utils.py | 18 ++- tests/core/test_utils.py | 129 +++++++++++++++++- .../plugins/project/test_extension_helpers.py | 28 +++- 5 files changed, 206 insertions(+), 14 deletions(-) diff --git a/connect/cli/core/utils.py b/connect/cli/core/utils.py index e56aec43..9de1b6e1 100644 --- a/connect/cli/core/utils.py +++ b/connect/cli/core/utils.py @@ -5,13 +5,14 @@ import os from collections import OrderedDict from importlib.metadata import entry_points +from distutils.version import StrictVersion import click import requests from packaging.version import InvalidVersion, Version from connect.cli import get_version -from connect.cli.core.constants import PYPI_JSON_API_URL +from connect.cli.core.constants import DEFAULT_ENDPOINT, PYPI_JSON_API_URL from connect.cli.core.terminal import console @@ -37,6 +38,18 @@ def sort_and_filter_tags(tags, desired_major): return sorted_tags +def get_last_version_by_major(tags, major): + major = int(major) + + while major >= 0: + result = sort_and_filter_tags(tags, str(major)) + if result: + return result.popitem()[0] + major -= 1 + + return None + + def get_last_cli_version(): try: res = requests.get(PYPI_JSON_API_URL) @@ -47,14 +60,37 @@ def get_last_cli_version(): pass +def get_connect_version(): + try: + response = requests.get(DEFAULT_ENDPOINT) + return response.headers.get('Connect-Version') + except requests.RequestException: + return + + def check_for_updates(*args): + connect_version = get_connect_version() + if not connect_version: + return + current = get_version() - last_version = get_last_cli_version() + last_version = None + try: + res = requests.get(PYPI_JSON_API_URL) + if res.status_code == 200: + data = res.json() + last_version = get_last_version_by_major( + data['releases'], + connect_version.split('.', 1)[0], + ) + except requests.RequestException: + return + if last_version and last_version != current: console.echo() console.secho( f'You are running CloudBlue Connect CLI version {current}. ' - f'A newer version is available: {last_version}', + f'Suggested latest version for used Connect version: {last_version}', fg='yellow', ) console.echo() diff --git a/connect/cli/plugins/project/extension/helpers.py b/connect/cli/plugins/project/extension/helpers.py index 953c5919..2d98837b 100644 --- a/connect/cli/plugins/project/extension/helpers.py +++ b/connect/cli/plugins/project/extension/helpers.py @@ -13,6 +13,7 @@ from connect.cli.plugins.project.extension.utils import ( get_event_definitions, get_pypi_runner_version, + get_pypi_runner_version_by_connect_version, initialize_git_repository, ) from connect.cli.plugins.project.extension.wizard import ( @@ -244,7 +245,7 @@ def bump_runner_extension_project(project_dir: str): # noqa: CCR001 console.secho(f'Bumping runner version on project {project_dir}...\n', fg='blue') updated_files = set() - latest_version = get_pypi_runner_version() + latest_version = get_pypi_runner_version_by_connect_version() latest_runner_version = f'cloudblueconnect/connect-extension-runner:{latest_version}' docker_compose_file = os.path.join(project_dir, 'docker-compose.yml') if not os.path.isfile(docker_compose_file): diff --git a/connect/cli/plugins/project/extension/utils.py b/connect/cli/plugins/project/extension/utils.py index 3d819068..7a582484 100644 --- a/connect/cli/plugins/project/extension/utils.py +++ b/connect/cli/plugins/project/extension/utils.py @@ -7,7 +7,11 @@ from connect.client import ClientError from connect.cli import get_version -from connect.cli.core.utils import sort_and_filter_tags +from connect.cli.core.utils import ( + get_connect_version, + get_last_version_by_major, + sort_and_filter_tags, +) from connect.cli.plugins.project.extension.constants import ( PRE_COMMIT_HOOK, PYPI_EXTENSION_RUNNER_URL, @@ -34,6 +38,18 @@ def get_pypi_runner_version(): return content['info']['version'] +def get_pypi_runner_version_by_connect_version(): + connect_version = get_connect_version() + res = requests.get(PYPI_EXTENSION_RUNNER_URL) + if res.status_code != 200 or not connect_version: + raise ClickException( + f'We can not retrieve the current connect-extension-runner version from {PYPI_EXTENSION_RUNNER_URL}.', + ) + content = res.json() + version = get_last_version_by_major(content['releases'], connect_version.split('.', 1)[0]) + return version or content['info']['version'] + + def get_extension_types(config): if config.active.is_provider(): extension_types = [('hub', 'Hub integration')] diff --git a/tests/core/test_utils.py b/tests/core/test_utils.py index d5c41e73..dc58cd83 100644 --- a/tests/core/test_utils.py +++ b/tests/core/test_utils.py @@ -6,36 +6,147 @@ from requests import RequestException from connect.cli.core import utils -from connect.cli.core.constants import PYPI_JSON_API_URL +from connect.cli.core.constants import DEFAULT_ENDPOINT, PYPI_JSON_API_URL from connect.cli.core.utils import iter_entry_points, sort_and_filter_tags def test_check_for_updates_ok(mocker, capsys, mocked_responses): mocker.patch('connect.cli.core.utils.get_version', return_value='1.0.0') - mocked_responses.add('GET', PYPI_JSON_API_URL, json={'info': {'version': '2.0.0'}}) + mocked_responses.add( + 'GET', + DEFAULT_ENDPOINT, + status=401, + headers={'Connect-Version': '2.0.1-abc'}, + ) + mocked_responses.add( + 'GET', + PYPI_JSON_API_URL, + json={ + 'releases': { + '1.6.0': ['release info'], + '1.7.1': ['release info'], + '2.0.0': ['release info'], + '2.1.0': ['release info'], + '2.1.1': ['release info'], + }, + }, + ) utils.check_for_updates() captured = capsys.readouterr() assert 'You are running CloudBlue Connect CLI version 1.0.0. ' in captured.out - assert 'A newer version is available: 2.0.0' in captured.out + assert 'Suggested latest version for used Connect version: 2.1.1' in captured.out def test_check_for_updates_is_latest(mocker, capsys, mocked_responses): mocker.patch('connect.cli.core.utils.get_version', return_value='2.0.0') - mocked_responses.add('GET', PYPI_JSON_API_URL, json={'info': {'version': '2.0.0'}}) + mocked_responses.add( + 'GET', + DEFAULT_ENDPOINT, + status=401, + headers={'Connect-Version': '2.0.1-abc'}, + ) + mocked_responses.add( + 'GET', + PYPI_JSON_API_URL, + json={ + 'releases': { + '2.0.0': ['release info'], + }, + }, + ) + + utils.check_for_updates() + + captured = capsys.readouterr() + + assert 'Suggested latest version' not in captured.out + + +def test_check_for_updates_no_update_needed(mocker, capsys, mocked_responses): + mocker.patch('connect.cli.core.utils.get_version', return_value='2.0.0') + mocked_responses.add( + 'GET', + DEFAULT_ENDPOINT, + status=401, + headers={'Connect-Version': '3.0.1-abc'}, + ) + mocked_responses.add( + 'GET', + PYPI_JSON_API_URL, + json={ + 'releases': { + '2.0.0': ['release info'], + }, + }, + ) + + utils.check_for_updates() + + captured = capsys.readouterr() + + assert 'Suggested latest version' not in captured.out + + +def test_check_for_updates_need_downgrade(mocker, capsys, mocked_responses): + mocker.patch('connect.cli.core.utils.get_version', return_value='4.0.0') + mocked_responses.add( + 'GET', + DEFAULT_ENDPOINT, + status=401, + headers={'Connect-Version': '2.2.1-abc'}, + ) + mocked_responses.add( + 'GET', + PYPI_JSON_API_URL, + json={ + 'releases': { + '4.1.0': ['release info'], + '3.0.0': ['release info'], + '2.1.1': ['release info'], + }, + }, + ) utils.check_for_updates() captured = capsys.readouterr() - assert 'You are running CloudBlue Connect CLI version 1.0.0. ' not in captured.out - assert 'A newer version is available: 2.0.0' not in captured.out + assert 'You are running CloudBlue Connect CLI version 4.0.0. ' in captured.out + assert 'Suggested latest version for used Connect version: 2.1.1' in captured.out + + +def test_check_for_updates_no_matching_version_downgrade(mocker, capsys, mocked_responses): + mocker.patch('connect.cli.core.utils.get_version', return_value='4.0.0') + mocked_responses.add( + 'GET', + DEFAULT_ENDPOINT, + status=401, + headers={'Connect-Version': '2.2.1-abc'}, + ) + mocked_responses.add( + 'GET', + PYPI_JSON_API_URL, + json={ + 'releases': { + '3.0.0': ['release info'], + '1.1.1': ['release info'], + }, + }, + ) + + utils.check_for_updates() + + captured = capsys.readouterr() + + assert 'Suggested latest version for used Connect version: 1.1.1' in captured.out def test_check_for_updates_exception(mocker, capsys, mocked_responses): mocker.patch('connect.cli.core.utils.get_version', return_value='1.0.0') + mocker.patch('connect.cli.core.utils.get_connect_version', return_value='1.0.0') mocker.patch('connect.cli.core.utils.requests.get', side_effect=RequestException()) utils.check_for_updates() @@ -46,6 +157,12 @@ def test_check_for_updates_exception(mocker, capsys, mocked_responses): def test_check_for_updates_invalid_response(mocker, capsys, mocked_responses): mocker.patch('connect.cli.core.utils.get_version', return_value='1.0.0') + mocked_responses.add( + 'GET', + DEFAULT_ENDPOINT, + status=401, + headers={'Connect-Version': '3.0.1-abc'}, + ) mocked_responses.add('GET', PYPI_JSON_API_URL, status=400) utils.check_for_updates() diff --git a/tests/plugins/project/test_extension_helpers.py b/tests/plugins/project/test_extension_helpers.py index 45b46455..04d5ea40 100644 --- a/tests/plugins/project/test_extension_helpers.py +++ b/tests/plugins/project/test_extension_helpers.py @@ -11,6 +11,8 @@ from click import ClickException from flake8.api import legacy as flake8 +from connect.cli.core.constants import DEFAULT_ENDPOINT +from connect.cli.plugins.project.extension.constants import PYPI_EXTENSION_RUNNER_URL from connect.cli.plugins.project.extension.helpers import ( bootstrap_extension_project, bump_runner_extension_project, @@ -890,8 +892,20 @@ def test_bootstrap_extension_project_if_destination_exists(mocker): assert 'Answers cannot be saved' in str(cv.value) -def test_bump_runner_version(mocker, capsys): - _mock_pypi_version(mocker) +def test_bump_runner_version(mocker, mocked_responses, capsys): + mocker.patch('connect.cli.plugins.project.extension.utils.get_version', return_value='0.5') + mocked_responses.add( + 'GET', + DEFAULT_ENDPOINT, + status=401, + headers={'Connect-Version': '1.0.1-abc'}, + ) + mocked_responses.add( + 'GET', + PYPI_EXTENSION_RUNNER_URL, + json={'releases': {'1.0': ['release info']}}, + ) + with tempfile.TemporaryDirectory() as tmp_data: project_dir = f'{tmp_data}/project' os.mkdir(project_dir) @@ -1008,9 +1022,17 @@ def test_bump_runner_docker_yaml_error(mocker): assert 'not properly formatted' in str(error.value) +def test_bump_runner_get_version_error(mocker, mocked_responses): + mocker.patch('connect.cli.plugins.project.extension.utils.get_connect_version') + mocked_responses.add('GET', PYPI_EXTENSION_RUNNER_URL, status=400) + with pytest.raises(ClickException) as error: + bump_runner_extension_project('project_dir') + assert 'We can not retrieve the current connect-extension-runner' in str(error.value) + + def _mock_pypi_version(mocker): mocker.patch( - 'connect.cli.plugins.project.extension.helpers.get_pypi_runner_version', + 'connect.cli.plugins.project.extension.helpers.get_pypi_runner_version_by_connect_version', return_value='1.0', )