From 06c4a77afe79b97382b14843e00d36574bbcf5a9 Mon Sep 17 00:00:00 2001 From: David Boreham Date: Wed, 27 Sep 2023 14:24:41 -0600 Subject: [PATCH] Simple self-update feature (#545) * Simple self-update feature * Update quick install script for self-update --- app/update.py | 79 ++++++++++++++++++++++++++++++++++ cli.py | 2 + requirements.txt | 1 + scripts/quick-install-linux.sh | 12 +++++- 4 files changed, 93 insertions(+), 1 deletion(-) create mode 100644 app/update.py diff --git a/app/update.py b/app/update.py new file mode 100644 index 00000000..ed1a1d6a --- /dev/null +++ b/app/update.py @@ -0,0 +1,79 @@ +# Copyright © 2023 Vulcanize + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. + +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +import click +import datetime +import filecmp +import os +from pathlib import Path +import requests +import sys +import stat +import shutil +import validators +from app.util import get_yaml + + +def _download_url(url: str, file_path: Path): + r = requests.get(url, stream=True) + r.raw.decode_content = True + with open(file_path, 'wb') as f: + shutil.copyfileobj(r.raw, f) + + +def _error_exit(s: str): + print(s) + sys.exit(1) + + +# Note at present this probably won't work on non-Unix based OSes like Windows +@click.command() +@click.option("--check-only", is_flag=True, default=False, help="only check, don't update") +@click.pass_context +def command(ctx, check_only): + '''update shiv binary from a distribution url''' + # Get the distribution URL from config + config_key = 'distribution-url' + config_file_path = Path(os.path.expanduser("~/.laconic-so/config.yml")) + if not config_file_path.exists(): + _error_exit(f"Error: Config file: {config_file_path} not found") + yaml = get_yaml() + config = yaml.load(open(config_file_path, "r")) + if "distribution-url" not in config: + _error_exit(f"Error: {config_key} not defined in {config_file_path}") + distribution_url = config[config_key] + # Sanity check the URL + if not validators.url(distribution_url): + _error_exit(f"ERROR: distribution url: {distribution_url} is not valid") + # Figure out the filename for ourselves + shiv_binary_path = Path(sys.argv[0]) + timestamp_filename = f"laconic-so-download-{datetime.datetime.now().strftime('%y%m%d-%H%M%S')}" + temp_download_path = shiv_binary_path.parent.joinpath(timestamp_filename) + # Download the file to a temp filename + if ctx.obj.verbose: + print(f"Downloading from: {distribution_url} to {temp_download_path}") + _download_url(distribution_url, temp_download_path) + # Set the executable bit + existing_mode = os.stat(temp_download_path) + os.chmod(temp_download_path, existing_mode.st_mode | stat.S_IXUSR) + # Switch the new file for the path we ran from + # Check if the downloaded file is identical to the existing one + same = filecmp.cmp(temp_download_path, shiv_binary_path) + if same: + if not ctx.obj.quiet: + print("Up update available, latest version already installed") + if ctx.obj.verbose: + print(f"Replacing: {shiv_binary_path} with {temp_download_path}") + os.replace(temp_download_path, shiv_binary_path) diff --git a/cli.py b/cli.py index 21f8a8b5..63823715 100644 --- a/cli.py +++ b/cli.py @@ -22,6 +22,7 @@ from app import build_npms from app import deploy from app import version from app import deployment +from app import update CONTEXT_SETTINGS = dict(help_option_names=['-h', '--help']) @@ -48,3 +49,4 @@ cli.add_command(deploy.command, "deploy") # deploy is an alias for deploy-syste cli.add_command(deploy.command, "deploy-system") cli.add_command(deployment.command, "deployment") cli.add_command(version.command, "version") +cli.add_command(update.command, "update") diff --git a/requirements.txt b/requirements.txt index 538121aa..7f60f9dd 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,3 +7,4 @@ PyYAML>=6.0.1 ruamel.yaml>=0.17.32 pydantic==1.10.9 tomli==2.0.1 +validators==0.22.0 diff --git a/scripts/quick-install-linux.sh b/scripts/quick-install-linux.sh index 007102f6..5670a403 100755 --- a/scripts/quick-install-linux.sh +++ b/scripts/quick-install-linux.sh @@ -5,6 +5,9 @@ fi install_dir=~/bin +# Skip the package install stuff if so directed +if ! [[ -n "$CERC_SO_INSTALL_SKIP_PACKAGES" ]]; then + # First display a reasonable warning to the user unless run with -y if ! [[ $# -eq 1 && $1 == "-y" ]]; then echo "**************************************************************************************" @@ -128,13 +131,20 @@ sudo apt -y install docker-ce docker-ce-cli containerd.io docker-buildx-plugin d # Allow the current user to use Docker sudo usermod -aG docker $USER +# End of long if block: Skip the package install stuff if so directed +fi + echo "**************************************************************************************" echo "Installing laconic-so" # install latest `laconic-so` +distribution_url=https://github.com/cerc-io/stack-orchestrator/releases/latest/download/laconic-so install_filename=${install_dir}/laconic-so mkdir -p ${install_dir} -curl -L -o ${install_filename} https://github.com/cerc-io/stack-orchestrator/releases/latest/download/laconic-so +curl -L -o ${install_filename} ${distribution_url} chmod +x ${install_filename} +# Set up config file for self-update feature +mkdir ~/.laconic-so +echo "distribution-url: ${distribution_url}" > ~/.laconic-so/config.yml echo "**************************************************************************************" # Check if our PATH line is already there