diff --git a/bonfire/bonfire.py b/bonfire/bonfire.py index be1e5679..5337a162 100755 --- a/bonfire/bonfire.py +++ b/bonfire/bonfire.py @@ -6,12 +6,6 @@ import sys import warnings -from importlib.metadata import version, PackageNotFoundError -try: - __version__ = version("crc-bonfire") -except PackageNotFoundError: - __version__ = "(unknown)" - from tabulate import tabulate from wait_for import TimedOutError @@ -31,7 +25,14 @@ oc, whoami, ) -from bonfire.utils import FatalError, split_equals, find_what_depends_on, validate_time_string +from bonfire.utils import ( + FatalError, + split_equals, + find_what_depends_on, + validate_time_string, + check_pypi, + get_version, +) from bonfire.local import get_local_apps from bonfire.processor import ( TemplateProcessor, @@ -64,6 +65,8 @@ def _error(msg): @click.group(context_settings=dict(help_option_names=["-h", "--help"])) @click.option("--debug", "-d", help="Enable debug logging", is_flag=True, default=False) def main(debug): + check_pypi() + logging.getLogger("sh").setLevel(logging.CRITICAL) # silence the 'sh' library logger logging.basicConfig( format="%(asctime)s [%(levelname)8s] [%(threadName)20s] %(message)s", @@ -1124,7 +1127,7 @@ def _err_handler(err): @main.command("version") def _cmd_version(): """Print bonfire version""" - click.echo("bonfire version " + __version__) + click.echo("bonfire version " + get_version()) @config.command("write-default") diff --git a/bonfire/config.py b/bonfire/config.py index d9379667..264a15b0 100644 --- a/bonfire/config.py +++ b/bonfire/config.py @@ -26,6 +26,9 @@ def _get_config_path(): DEFAULT_CONFIG_PATH = _get_config_path().joinpath("config.yaml") DEFAULT_ENV_PATH = _get_config_path().joinpath("env") DEFAULT_SECRETS_DIR = _get_config_path().joinpath("secrets") +VER_CHECK_PATH = _get_config_path().joinpath("lastvercheck") +VER_CHECK_TIME = 3600 # check every 1hr + DEFAULT_CLOWDENV_TEMPLATE = resource_filename( "bonfire", "resources/local-cluster-clowdenvironment.yaml" ) diff --git a/bonfire/utils.py b/bonfire/utils.py index 6e3382c1..33ce6d0d 100644 --- a/bonfire/utils.py +++ b/bonfire/utils.py @@ -3,14 +3,20 @@ import logging import os import re -import requests import shlex import subprocess import tempfile +import time import yaml +from distutils.version import StrictVersion +from pathlib import Path +import pkg_resources +import requests from cached_property import cached_property +import bonfire.config as conf + class FatalError(Exception): """An exception that will cause the CLI to exit""" @@ -18,6 +24,9 @@ class FatalError(Exception): pass +PKG_NAME = "crc-bonfire" +PYPI_URL = f"https://pypi.python.org/pypi/{PKG_NAME}/json" + GH_RAW_URL = "https://raw.githubusercontent.com/{org}/{repo}/{ref}{path}" GL_RAW_URL = "https://gitlab.cee.redhat.com/{group}/{project}/-/raw/{ref}{path}" GH_API_URL = os.getenv("GITHUB_API_URL", "https://api.github.com") @@ -393,3 +402,90 @@ def load_file(path): raise FatalError("File '{}' is empty!".format(path)) return content + + +def get_version(): + try: + return pkg_resources.get_distribution(PKG_NAME).version + except pkg_resources.DistributionNotFound: + return "0.0.0" + + +def _compare_version(pypi_version): + pypi_version = StrictVersion(pypi_version) + + local_version = get_version() + try: + my_version = StrictVersion(local_version) + except ValueError: + print(f"Version {local_version} seems to be a dev version, assuming up-to-date") + my_version = StrictVersion("999.999.999") + return + + if my_version < pypi_version: + print( + "\n" + "There is a new bonfire version available! (yours: {}, available: {})" + "\n" + "Upgrade with:\n" + f" pip install --upgrade {PKG_NAME}" + " ".format(my_version, pypi_version) + ) + else: + print("Up-to-date!") + + +def _update_ver_check_file(): + ver_check_file = Path(conf.VER_CHECK_PATH) + try: + with ver_check_file.open(mode="w") as fp: + fp.write(str(time.time())) + except OSError: + log.error("failed to update version check file at path: %s", ver_check_file.resolve()) + + +def _ver_check_needed(): + ver_check_file = Path(conf.VER_CHECK_PATH) + if not ver_check_file.exists(): + _update_ver_check_file() + return True + + last_check_time = 0 + try: + with ver_check_file.open() as fp: + last_check_time = float(fp.read().strip()) + except (OSError, ValueError): + log.exception("failed to read version check file at path: %s", ver_check_file.resolve()) + + if time.time() > last_check_time + conf.VER_CHECK_TIME: + _update_ver_check_file() + return True + + return False + + +def check_pypi(): + if not _ver_check_needed(): + return + + print("\nChecking pypi for latest release...") + + pkg_data = {} + try: + response = requests.get(PYPI_URL, timeout=5) + response.raise_for_status() + pkg_data = response.json() + except requests.exceptions.Timeout: + print("Unable to reach pypi quickly, giving up.") + except requests.exceptions.HTTPError as e: + print("Error response from pypi: ", e.errno, e.message) + except ValueError: + print("Response was not valid json, giving up.") + + try: + pypi_version = pkg_data["info"]["version"] + except KeyError: + print("Unable to parse version info from pypi") + else: + _compare_version(pypi_version) + print("\n")