From a54a3f06ab0f3d75c6e12e8166119bbc3757d495 Mon Sep 17 00:00:00 2001 From: Jordan Mance Date: Sun, 19 Sep 2021 13:34:00 -0400 Subject: [PATCH] Adding figgy cloud version checks before auto-upgrade --- REQUIRES_CLOUD_VERSION | 0 src/figcli/commands/command_factory.py | 2 +- src/figcli/commands/help/upgrade.py | 23 ++++--- src/figcli/commands/help/version.py | 5 +- src/figcli/commands/help_factory.py | 9 +-- src/figcli/config/constants.py | 3 +- .../svcs/observability/version_tracker.py | 66 +++++++++++++++---- 7 files changed, 80 insertions(+), 28 deletions(-) create mode 100644 REQUIRES_CLOUD_VERSION diff --git a/REQUIRES_CLOUD_VERSION b/REQUIRES_CLOUD_VERSION new file mode 100644 index 0000000..e69de29 diff --git a/src/figcli/commands/command_factory.py b/src/figcli/commands/command_factory.py index 5c1086f..42278a9 100644 --- a/src/figcli/commands/command_factory.py +++ b/src/figcli/commands/command_factory.py @@ -260,7 +260,7 @@ def instance(self): optional_args = self._context.find_matching_optional_arguments(help_commands) context = HelpContext(self._context.resource, self._context.command, optional_args, self._context.run_env, defaults=self._cli_defaults, role=self._context.role) - factory = HelpFactory(self._context.command, context, self._context) + factory = HelpFactory(self._context.command, context, self._context, self.__config_service()) elif self._context.command in ui_commands or self._context.resource == ui: context = CommandContext(self._context.run_env, self._context.command, defaults=self._cli_defaults) diff --git a/src/figcli/commands/help/upgrade.py b/src/figcli/commands/help/upgrade.py index a0034d6..b00dcd7 100644 --- a/src/figcli/commands/help/upgrade.py +++ b/src/figcli/commands/help/upgrade.py @@ -4,6 +4,7 @@ from figcli.config import * from figcli.io.input import Input from figcli.io.output import Output +from figcli.svcs.config import ConfigService from figcli.svcs.observability.anonymous_usage_tracker import AnonymousUsageTracker from figcli.svcs.observability.version_tracker import VersionTracker, FiggyVersionDetails from figcli.svcs.upgrade_manager import UpgradeManager @@ -15,9 +16,9 @@ class Upgrade(HelpCommand): Drives the --version command """ - def __init__(self, help_context: HelpContext): + def __init__(self, help_context: HelpContext, config_service: ConfigService): super().__init__(version, help_context.defaults.colors_enabled, help_context) - self.tracker = VersionTracker(self.context.defaults) + self.tracker = VersionTracker(self.context.defaults, config_service) self.upgrade_mgr = UpgradeManager(help_context.defaults.colors_enabled) self._utils = Utils(colors_enabled=help_context.defaults.colors_enabled) self._out = Output(colors_enabled=help_context.defaults.colors_enabled) @@ -49,9 +50,15 @@ def upgrade(self): if latest_version.version == VERSION: self._out.success(f'You are currently using the latest version of [[{CLI_NAME}]]: [[{VERSION}]]') upgrade_it = False - - elif self.tracker.upgrade_available(VERSION, latest_version.version): + elif self.tracker.upgrade_available(): self._out.notify_h2(f"New version: [[{latest_version.version}]] is more recent than your version: [[{VERSION}]]") + upgrade_it = True + elif not self.tracker.cloud_version_compatible_with_upgrade(): + self._out.notify_h2(f"Version [[{self.tracker.get_version().version}]] of the Figgy CLI is available but your " + f"current version of Figgy Cloud ([[{self.tracker.current_cloud_version()}]]) is not compatible." + f" Your administrator must first update FiggyCloud to at least version: " + f"[[{self.tracker.required_cloud_version()}]] before you can upgrade Figgy.") + upgrade_it = False else: self._out.notify_h2(f"Your version: [[{VERSION}]] is more recent then the current recommended version " f"of {CLI_NAME}: [[{latest_version.version}]]") @@ -71,11 +78,11 @@ def upgrade(self): if install_success: self._out.success(f"Installation successful! Exiting. Rerun `[[{CLI_NAME}]]` " - f"to use the latest version!") + f"to use the latest version!") else: self._out.warn(f"\nUpgrade may not have been successful. Check by re-running " - f"[[`{CLI_NAME}` --version]] to see if it was. If it wasn't, please reinstall [[`{CLI_NAME}`]]. " - f"See {INSTALL_URL}.") + f"[[`{CLI_NAME}` --version]] to see if it was. If it wasn't, please reinstall [[`{CLI_NAME}`]]. " + f"See {INSTALL_URL}.") def install_mac(self, latest_version: FiggyVersionDetails) -> bool: install_path = '/usr/local/bin/figgy' @@ -95,7 +102,7 @@ def install_mac(self, latest_version: FiggyVersionDetails) -> bool: return True else: self._out.print(f'\n[[Auto-upgrade aborted. To upgrade through brew run:]] \n' - f'-> brew upgrade figtools/figgy/figgy') + f'-> brew upgrade figtools/figgy/figgy') self._out.warn(f"\n\nYou may continue to manage [[{CLI_NAME}]] through Homebrew, but doing so will " f"limit some upcoming functionality around canary releases, rollbacks, and dynamic " f"version-swapping.") diff --git a/src/figcli/commands/help/version.py b/src/figcli/commands/help/version.py index 455af39..06be170 100644 --- a/src/figcli/commands/help/version.py +++ b/src/figcli/commands/help/version.py @@ -1,6 +1,7 @@ from figcli.commands.help_context import HelpContext from figcli.commands.types.help import HelpCommand from figcli.config import * +from figcli.svcs.config import ConfigService from figcli.svcs.observability.anonymous_usage_tracker import AnonymousUsageTracker from figcli.svcs.observability.version_tracker import VersionTracker @@ -10,9 +11,9 @@ class Version(HelpCommand): Drives the --version command """ - def __init__(self, help_context: HelpContext): + def __init__(self, help_context: HelpContext, config_service: ConfigService): super().__init__(version, help_context.defaults.colors_enabled, help_context) - self.tracker = VersionTracker(self.context.defaults) + self.tracker = VersionTracker(self.context.defaults, config_service) def version(self): self.tracker.check_version(self.c) diff --git a/src/figcli/commands/help_factory.py b/src/figcli/commands/help_factory.py index 0fb18b5..25cada6 100644 --- a/src/figcli/commands/help_factory.py +++ b/src/figcli/commands/help_factory.py @@ -6,19 +6,20 @@ from figcli.commands.help_context import HelpContext from figcli.commands.help.configure import Configure from figcli.config import * +from figcli.svcs.config import ConfigService from figcli.svcs.setup import FiggySetup -from figcli.svcs.auth.session_manager import SessionManager from figcli.utils.utils import Utils, CollectionUtils class HelpFactory(Factory): - def __init__(self, command: CliCommand, context: HelpContext, figgy_context: FiggyContext): + def __init__(self, command: CliCommand, context: HelpContext, figgy_context: FiggyContext, config: ConfigService): self._command = command self._context = context self._figgy_context = figgy_context self._options = context.options self._utils = Utils(False) self._setup: FiggySetup = FiggySetup(self._figgy_context) + self._config: ConfigService = config def instance(self): return self.get(self._command) @@ -27,11 +28,11 @@ def get(self, command: CliCommand): if configure in self._options: return Configure(self._context, self._setup) elif version in self._options: - return Version(self._context) + return Version(self._context, self._config) elif command == login or command == sandbox: return Login(self._context, self._setup, self._figgy_context) elif upgrade in self._options: - return Upgrade(self._context) + return Upgrade(self._context, self._config) else: self._utils.error_exit(f"{command.name} is not a valid command. You must select from: " f"[{CollectionUtils.printable_set(help_commands)}]. Try using --help for more info.") diff --git a/src/figcli/config/constants.py b/src/figcli/config/constants.py index 3c250f1..d7fe193 100644 --- a/src/figcli/config/constants.py +++ b/src/figcli/config/constants.py @@ -1,7 +1,7 @@ from pathlib import Path # If cloud has been updated with required changes, please updated REQUIRES_CLI_VERSION in figtools/figgy -VERSION = '1.2.0' +VERSION = '1.1.0' CLI_NAME = 'figgy' PROJECT_NAME = 'figgy' @@ -90,6 +90,7 @@ PS_FIGGY_OTS_KEY_ID = '/figgy/kms/figgy-ots-key-id' PS_FIGGY_ENV_ALIAS = '/figgy/env_alias' PS_FIGGY_CURRENT_ACCOUNT_ID = '/figgy/account_id' +PS_CLOUD_VERSION_PATH = '/figgy/cloud/version' # Replication Types: repl_types = [REPL_TYPE_APP, REPL_TYPE_MERGE] diff --git a/src/figcli/svcs/observability/version_tracker.py b/src/figcli/svcs/observability/version_tracker.py index 99e8660..994dd00 100644 --- a/src/figcli/svcs/observability/version_tracker.py +++ b/src/figcli/svcs/observability/version_tracker.py @@ -1,4 +1,6 @@ from typing import Optional, Tuple, Dict + +import cachetools.func import requests import logging import random @@ -10,7 +12,9 @@ from figcli.config.constants import * from figcli.config.style.color import Color from figcli.config.style.terminal_factory import TerminalFactory +from figcli.io.output import Output from figcli.models.defaults.defaults import CLIDefaults +from figcli.svcs.config import ConfigService log = logging.getLogger(__name__) @@ -19,6 +23,7 @@ class FiggyVersionDetails(BaseModel): version: str notify_chance: int changelog: str + cloud_version_requirement: str def changes_from(self, old_version: str): regex = f'.*(##+\s+{self.version}.*)##+\s+{old_version}.*' @@ -44,7 +49,8 @@ def from_api_response(response: Dict) -> "FiggyVersionDetails": return FiggyVersionDetails( version=version, notify_chance=int(notify_chance), - changelog=response.get('changelog') + changelog=response.get('changelog'), + cloud_version_requirement=response.get('cloud_version_requirement') ) @@ -52,11 +58,14 @@ class VersionTracker: _UPGRADE_CHECK_PERCENTAGE = 5 # % chance any decorated method execution will check for an upgrade _DISABLE_CHECK_ENV_VAR = "FIGGY_DISABLE_VERSION_CHECK" - def __init__(self, cli_defaults: CLIDefaults): + def __init__(self, cli_defaults: CLIDefaults, config_service: ConfigService): self._cli_defaults = cli_defaults self.c = TerminalFactory(self._cli_defaults.colors_enabled).instance().get_colors() + self._config = config_service + self._out = Output(colors_enabled=cli_defaults.colors_enabled) @staticmethod + @cachetools.func.ttl_cache(maxsize=2, ttl=100) def get_version() -> FiggyVersionDetails: result = requests.get(FIGGY_GET_VERSION_URL) if result.status_code == 200: @@ -129,9 +138,39 @@ def is_rollback(current_version: str, new_version: str): except IndentationError: pass - @staticmethod - def upgrade_available(current_version: str, new_version: str): + def current_version(self): + return VERSION + + @cachetools.func.ttl_cache(maxsize=2, ttl=100) + def current_cloud_version(self): + return self._config.get_fig_simple(PS_CLOUD_VERSION_PATH).value + + @cachetools.func.ttl_cache(maxsize=2, ttl=100) + def required_cloud_version(self): + details: FiggyVersionDetails = self.get_version() + return details.cloud_version_requirement + + def cloud_version_compatible_with_upgrade(self): + req_cloud_version = self.required_cloud_version() + current_cloud_version: str = self.current_cloud_version() + + cl_req_major = req_cloud_version.split('.')[0] + cl_req_minor = req_cloud_version.split('.')[1] + cl_req_patch = req_cloud_version.split('.')[2].strip('ab') + cl_cur_major = current_cloud_version.split('.')[0] + cl_cur_minor = current_cloud_version.split('.')[1] + cl_cur_patch = current_cloud_version.split('.')[2].strip('ab') + + return cl_cur_major > cl_req_major or \ + (cl_cur_major == cl_req_major and cl_cur_minor >= cl_req_minor) or \ + (cl_cur_major == cl_req_major and cl_cur_minor == cl_req_minor and cl_cur_patch >= cl_req_patch) + + def upgrade_available(self): try: + current_version = VERSION + details: FiggyVersionDetails = self.get_version() + new_version = details.version + cu_major = current_version.split('.')[0] cu_minor = current_version.split('.')[1] cu_patch = current_version.split('.')[2].strip('ab') @@ -139,14 +178,17 @@ def upgrade_available(current_version: str, new_version: str): new_minor = new_version.split('.')[1] new_patch = new_version.split('.')[2].strip('ab') - if new_major > cu_major: - return True - elif new_major >= cu_major and new_minor > cu_minor: - return True - elif new_major >= cu_major and new_minor >= cu_minor and new_patch > cu_patch: - return True - else: - return False + if self.cloud_version_compatible_with_upgrade(): + if new_major > cu_major: + return True + elif new_major >= cu_major and new_minor > cu_minor: + return True + elif new_major >= cu_major and new_minor >= cu_minor and new_patch > cu_patch: + return True + else: + return False + + return False except IndentationError: pass