From a4140828551bdae203a539f2525efbc261c66dc8 Mon Sep 17 00:00:00 2001 From: Grant Date: Tue, 17 Sep 2024 21:59:24 -0500 Subject: [PATCH 01/10] add constants related to the helm binary --- src/warnet/constants.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/warnet/constants.py b/src/warnet/constants.py index c01e8c2b4..4f6848c87 100644 --- a/src/warnet/constants.py +++ b/src/warnet/constants.py @@ -107,3 +107,8 @@ "helm repo update", f"helm upgrade --install ingress-nginx ingress-nginx/ingress-nginx --namespace {INGRESS_NAMESPACE} --create-namespace", ] + +# Helm binary +HELM_LATEST_URL = "https://get.helm.sh/helm-latest-version" +HELM_DOWNLOAD_URL_STUB = "https://get.helm.sh/" +HELM_BINARY_NAME = "helm" From 1d49f06845eb22ec010ef4087eb8710df569f559 Mon Sep 17 00:00:00 2001 From: Grant Date: Tue, 17 Sep 2024 21:59:44 -0500 Subject: [PATCH 02/10] `setup` shoehorn in an option to download helm --- src/warnet/project.py | 159 ++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 153 insertions(+), 6 deletions(-) diff --git a/src/warnet/project.py b/src/warnet/project.py index 0ac431015..6dd98ae26 100644 --- a/src/warnet/project.py +++ b/src/warnet/project.py @@ -1,15 +1,21 @@ +import hashlib import os import platform +import shutil import subprocess import sys +import tarfile +import tempfile from dataclasses import dataclass from enum import Enum, auto from pathlib import Path -from typing import Callable +from typing import Callable, Optional import click import inquirer +import requests +from .constants import HELM_BINARY_NAME, HELM_DOWNLOAD_URL_STUB, HELM_LATEST_URL from .graph import inquirer_create_network from .network import copy_network_defaults, copy_scenario_defaults @@ -19,6 +25,7 @@ def setup(): """Setup warnet""" class ToolStatus(Enum): + NeedsHelm = auto() Satisfied = auto() Unsatisfied = auto() @@ -155,7 +162,7 @@ def is_kubectl_installed() -> tuple[bool, str]: except FileNotFoundError as err: return False, str(err) - def is_helm_installed() -> tuple[bool, str]: + def is_helm_installed_and_offer_if_not() -> tuple[bool, str]: try: version_result = subprocess.run(["helm", "version"], capture_output=True, text=True) location_result = subprocess.run( @@ -167,8 +174,31 @@ def is_helm_installed() -> tuple[bool, str]: return version_result.returncode == 0, location_result.stdout.strip() else: return False, "" - except FileNotFoundError as err: - return False, str(err) + + except FileNotFoundError: + print() + helm_answer = inquirer.prompt( + [ + inquirer.Confirm( + "install_helm", + message=click.style( + "Would you like to use Warnet's downloader to install Helm into your virtual environment?", + fg="blue", + bold=True, + ), + default=True, + ), + ] + ) + if helm_answer is None: + msg = "Setup cancelled by user." + click.secho(msg, fg="yellow") + return False, msg + if helm_answer["install_helm"]: + click.secho(" Installing Helm...", fg="yellow", bold=True) + install_helm_rootlessly_to_venv() + return is_helm_installed_and_offer_if_not() + return False, "Please install Helm." def check_installation(tool_info: ToolInfo) -> ToolStatus: has_good_version, location = tool_info.is_installed_func() @@ -218,8 +248,8 @@ def check_installation(tool_info: ToolInfo) -> ToolStatus: ) helm_info = ToolInfo( tool_name="Helm", - is_installed_func=is_helm_installed, - install_instruction="Install Helm from Helm's official site.", + is_installed_func=is_helm_installed_and_offer_if_not, + install_instruction="Install Helm from Helm's official site, or rootlessly install Helm using Warnet's downloader when prompted.", install_url="https://helm.sh/docs/intro/install/", ) minikube_info = ToolInfo( @@ -361,3 +391,120 @@ def init(): """Initialize a warnet project in the current directory""" current_dir = Path.cwd() new_internal(directory=current_dir, from_init=True) + + +def get_os_name_for_helm() -> Optional[str]: + """Return a short operating system name suitable for downloading a helm binary.""" + uname_sys = platform.system().lower() + if "linux" in uname_sys: + return "linux" + elif uname_sys == "darwin": + return "darwin" + elif "win" in uname_sys: + return "windows" + return None + + +def is_in_virtualenv() -> bool: + """Check if the user is in a virtual environment.""" + return hasattr(sys, "real_prefix") or ( + hasattr(sys, "base_prefix") and sys.base_prefix != sys.prefix + ) + + +def download_file(url, destination): + click.secho(f" Downloading {url}", fg="blue") + response = requests.get(url, stream=True) + if response.status_code == 200: + with open(destination, "wb") as f: + for chunk in response.iter_content(1024): + f.write(chunk) + else: + raise Exception(f"Failed to download {url} (status code {response.status_code})") + + +def get_latest_version_of_helm() -> Optional[str]: + response = requests.get(HELM_LATEST_URL) + if response.status_code == 200: + return response.text.strip() + else: + return None + + +def verify_checksum(file_path, checksum_path): + click.secho(" Verifying checksum...", fg="blue") + sha256_hash = hashlib.sha256() + with open(file_path, "rb") as f: + for byte_block in iter(lambda: f.read(4096), b""): + sha256_hash.update(byte_block) + + with open(checksum_path) as f: + expected_checksum = f.read().strip() + + if sha256_hash.hexdigest() != expected_checksum: + raise Exception("Checksum verification failed!") + click.secho(" Checksum verified.", fg="blue") + + +def install_helm_to_venv(helm_bin_path): + venv_bin_dir = os.path.join(sys.prefix, "bin") + helm_dst_path = os.path.join(venv_bin_dir, HELM_BINARY_NAME) + shutil.move(helm_bin_path, helm_dst_path) + os.chmod(helm_dst_path, 0o755) + click.secho(f" {HELM_BINARY_NAME} installed into {helm_dst_path}", fg="blue") + + +def install_helm_rootlessly_to_venv(): + if not is_in_virtualenv(): + click.secho( + "Error: You are not in a virtual environment. Please activate a virtual environment and try again.", + fg="yellow", + ) + sys.exit(1) + + version = get_latest_version_of_helm() + if version is None: + click.secho( + "Error: Could not fetch the latest version of Helm. Please check your internet connection.", + fg="yellow", + ) + sys.exit(1) + + os_name = get_os_name_for_helm() + if os_name is None: + click.secho( + "Error: Could not determine the operating system of this computer.", fg="yellow" + ) + sys.exit(1) + + arch = os.uname().machine + arch_map = {"x86_64": "amd64", "i386": "386", "aarch64": "arm64", "armv7l": "arm"} + arch = arch_map.get(arch, arch) + + helm_filename = f"{HELM_BINARY_NAME}-{version}-{os_name}-{arch}.tar.gz" + helm_url = f"{HELM_DOWNLOAD_URL_STUB}{helm_filename}" + checksum_url = f"{helm_url}.sha256" + + try: + with tempfile.TemporaryDirectory() as temp_dir: + helm_archive_path = os.path.join(temp_dir, helm_filename) + checksum_path = os.path.join(temp_dir, f"{helm_filename}.sha256") + + download_file(helm_url, helm_archive_path) + download_file(checksum_url, checksum_path) + verify_checksum(helm_archive_path, checksum_path) + + # Extract Helm and install it in the virtual environment's bin folder + with tarfile.open(helm_archive_path, "r:gz") as tar: + tar.extractall(path=temp_dir) + helm_bin_path = os.path.join(temp_dir, os_name + "-" + arch, HELM_BINARY_NAME) + install_helm_to_venv(helm_bin_path) + + click.secho( + f" {HELM_BINARY_NAME} {version} installed successfully to your virtual environment!\n", + fg="blue", + ) + + except Exception as e: + click.secho(f"Error: {e}\nCould not install helm.", fg="yellow") + sys.exit(1) From 5ba32a073b493bcbae024d81e4f7532631577287 Mon Sep 17 00:00:00 2001 From: Grant Date: Fri, 20 Sep 2024 22:34:24 -0500 Subject: [PATCH 03/10] `project`: add a better arch_map --- src/warnet/project.py | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/src/warnet/project.py b/src/warnet/project.py index 6dd98ae26..8aeaf728b 100644 --- a/src/warnet/project.py +++ b/src/warnet/project.py @@ -478,8 +478,22 @@ def install_helm_rootlessly_to_venv(): sys.exit(1) arch = os.uname().machine - arch_map = {"x86_64": "amd64", "i386": "386", "aarch64": "arm64", "armv7l": "arm"} - arch = arch_map.get(arch, arch) + arch_map = { + "x86_64": "amd64", # Maps 'x86_64' to 'amd64' + "i686": "i386", # Maps 'i686' to 'i386' + "i386": "i386", # Maps 'i386' to 'i386' + "aarch64": "arm64", # Maps 'aarch64' (common on newer ARM) to 'arm64' + "armv7l": "arm", # Maps 'armv7l' to 'arm' (32-bit ARM) + "armv6l": "arm", # Maps 'armv6l' to 'arm' (32-bit ARM) + "ppc64le": "ppc64le", # PowerPC Little Endian + "s390x": "s390x", # IBM s390x architecture + "riscv64": "riscv64", # RISC-V 64-bit + } + if arch in arch_map: + arch = arch_map[arch] + else: + click.secho(f"No Helm binary candidate for arch: {arch}", fg="red") + sys.exit(1) helm_filename = f"{HELM_BINARY_NAME}-{version}-{os_name}-{arch}.tar.gz" helm_url = f"{HELM_DOWNLOAD_URL_STUB}{helm_filename}" From 3d8bb99abd3c821fbdb155dc18b0f57a5f4ef903 Mon Sep 17 00:00:00 2001 From: Grant Date: Fri, 20 Sep 2024 23:28:48 -0500 Subject: [PATCH 04/10] `constants`: add blessed helm metadata --- src/warnet/constants.py | 47 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/src/warnet/constants.py b/src/warnet/constants.py index 4f6848c87..722fc94a1 100644 --- a/src/warnet/constants.py +++ b/src/warnet/constants.py @@ -112,3 +112,50 @@ HELM_LATEST_URL = "https://get.helm.sh/helm-latest-version" HELM_DOWNLOAD_URL_STUB = "https://get.helm.sh/" HELM_BINARY_NAME = "helm" +HELM_BLESSED_VERSION = "v3.16.1" +HELM_BLESSED_NAME_AND_CHECKSUMS = [ + { + "name": "helm-v3.16.1-darwin-amd64.tar.gz", + "checksum": "1b194824e36da3e3889920960a93868b541c7888c905a06757e88666cfb562c9", + }, + { + "name": "helm-v3.16.1-darwin-arm64.tar.gz", + "checksum": "405a3b13f0e194180f7b84010dfe86689d7703e80612729882ad71e2a4ef3504", + }, + { + "name": "helm-v3.16.1-linux-amd64.tar.gz", + "checksum": "e57e826410269d72be3113333dbfaac0d8dfdd1b0cc4e9cb08bdf97722731ca9", + }, + { + "name": "helm-v3.16.1-linux-arm.tar.gz", + "checksum": "a15a8ddfc373628b13cd2a987206756004091a1f6a91c3b9ee8de6f0b1e2ce90", + }, + { + "name": "helm-v3.16.1-linux-arm64.tar.gz", + "checksum": "780b5b86f0db5546769b3e9f0204713bbdd2f6696dfdaac122fbe7f2f31541d2", + }, + { + "name": "helm-v3.16.1-linux-386.tar.gz", + "checksum": "92d7a47a90734b50528ffffc99cd1b2d4b9fc0f4291bac92c87ef03406a5a7b2", + }, + { + "name": "helm-v3.16.1-linux-ppc64le.tar.gz", + "checksum": "9f0178957c94516eff9a3897778edb93d78fab1f76751bd282883f584ea81c23", + }, + { + "name": "helm-v3.16.1-linux-s390x.tar.gz", + "checksum": "357f8b441cc535240f1b0ba30a42b44571d4c303dab004c9e013697b97160360", + }, + { + "name": "helm-v3.16.1-linux-riscv64.tar.gz", + "checksum": "9a2cab45b7d9282e9be7b42f86d8034dcaa2e81ab338642884843676c2f6929f", + }, + { + "name": "helm-v3.16.1-windows-amd64.zip", + "checksum": "89952ea1bace0a9498053606296ea03cf743c48294969dfc731e7f78d1dc809a", + }, + { + "name": "helm-v3.16.1-windows-arm64.zip", + "checksum": "fc370a291ed926da5e77acf42006de48e7fd5ff94d20c3f6aa10c04fea66e53c", + }, +] From 07104ee988914e568655dde1e324f752ca6887f8 Mon Sep 17 00:00:00 2001 From: Grant Date: Fri, 20 Sep 2024 23:29:16 -0500 Subject: [PATCH 05/10] `project`: get blessed helm version Get the blessed version instead of the latest version. --- src/warnet/project.py | 30 ++++++++++++++++++++---------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/src/warnet/project.py b/src/warnet/project.py index 8aeaf728b..22197573b 100644 --- a/src/warnet/project.py +++ b/src/warnet/project.py @@ -15,7 +15,13 @@ import inquirer import requests -from .constants import HELM_BINARY_NAME, HELM_DOWNLOAD_URL_STUB, HELM_LATEST_URL +from .constants import ( + HELM_BINARY_NAME, + HELM_BLESSED_NAME_AND_CHECKSUMS, + HELM_BLESSED_VERSION, + HELM_DOWNLOAD_URL_STUB, + HELM_LATEST_URL, +) from .graph import inquirer_create_network from .network import copy_network_defaults, copy_scenario_defaults @@ -431,6 +437,17 @@ def get_latest_version_of_helm() -> Optional[str]: return None +def write_blessed_checksum(helm_filename: str, dest_path: str): + checksum = next( + (b["checksum"] for b in HELM_BLESSED_NAME_AND_CHECKSUMS if b["name"] == helm_filename), None + ) + if checksum: + with open(dest_path, "w") as f: + f.write(checksum) + else: + click.secho("Could not find a matching helm binary and checksum", fg="red") + + def verify_checksum(file_path, checksum_path): click.secho(" Verifying checksum...", fg="blue") sha256_hash = hashlib.sha256() @@ -462,13 +479,7 @@ def install_helm_rootlessly_to_venv(): ) sys.exit(1) - version = get_latest_version_of_helm() - if version is None: - click.secho( - "Error: Could not fetch the latest version of Helm. Please check your internet connection.", - fg="yellow", - ) - sys.exit(1) + version = HELM_BLESSED_VERSION os_name = get_os_name_for_helm() if os_name is None: @@ -497,7 +508,6 @@ def install_helm_rootlessly_to_venv(): helm_filename = f"{HELM_BINARY_NAME}-{version}-{os_name}-{arch}.tar.gz" helm_url = f"{HELM_DOWNLOAD_URL_STUB}{helm_filename}" - checksum_url = f"{helm_url}.sha256" try: with tempfile.TemporaryDirectory() as temp_dir: @@ -505,7 +515,7 @@ def install_helm_rootlessly_to_venv(): checksum_path = os.path.join(temp_dir, f"{helm_filename}.sha256") download_file(helm_url, helm_archive_path) - download_file(checksum_url, checksum_path) + write_blessed_checksum(helm_filename, checksum_path) verify_checksum(helm_archive_path, checksum_path) # Extract Helm and install it in the virtual environment's bin folder From ec33acea9a40894fbd792bce6a71241132fa2c76 Mon Sep 17 00:00:00 2001 From: Grant Date: Mon, 23 Sep 2024 10:28:37 -0500 Subject: [PATCH 06/10] `project`: fix arch<->uname mapping --- src/warnet/project.py | 37 ++++++++++++++++++++++--------------- 1 file changed, 22 insertions(+), 15 deletions(-) diff --git a/src/warnet/project.py b/src/warnet/project.py index 22197573b..34b865c7a 100644 --- a/src/warnet/project.py +++ b/src/warnet/project.py @@ -437,6 +437,25 @@ def get_latest_version_of_helm() -> Optional[str]: return None +def query_arch_from_uname(arch: str) -> Optional[str]: + if arch.startswith("armv5"): + return "armv5" + elif arch.startswith("armv6"): + return "armv6" + elif arch.startswith("armv7"): + return "arm" + elif arch == "aarch64": + return "arm64" + elif arch == "x86": + return "386" + elif arch == "x86_64": + return "amd64" + elif arch == "i686" or arch == "i386": + return "386" + else: + return None + + def write_blessed_checksum(helm_filename: str, dest_path: str): checksum = next( (b["checksum"] for b in HELM_BLESSED_NAME_AND_CHECKSUMS if b["name"] == helm_filename), None @@ -488,21 +507,9 @@ def install_helm_rootlessly_to_venv(): ) sys.exit(1) - arch = os.uname().machine - arch_map = { - "x86_64": "amd64", # Maps 'x86_64' to 'amd64' - "i686": "i386", # Maps 'i686' to 'i386' - "i386": "i386", # Maps 'i386' to 'i386' - "aarch64": "arm64", # Maps 'aarch64' (common on newer ARM) to 'arm64' - "armv7l": "arm", # Maps 'armv7l' to 'arm' (32-bit ARM) - "armv6l": "arm", # Maps 'armv6l' to 'arm' (32-bit ARM) - "ppc64le": "ppc64le", # PowerPC Little Endian - "s390x": "s390x", # IBM s390x architecture - "riscv64": "riscv64", # RISC-V 64-bit - } - if arch in arch_map: - arch = arch_map[arch] - else: + uname_arch = os.uname().machine + arch = query_arch_from_uname(uname_arch) + if not arch: click.secho(f"No Helm binary candidate for arch: {arch}", fg="red") sys.exit(1) From 53f017194fcb781d7d9478c8ec48273e1b743062 Mon Sep 17 00:00:00 2001 From: Grant Date: Tue, 24 Sep 2024 09:22:22 -0500 Subject: [PATCH 07/10] remove dead code We no longer need the logic for the latest version of helm. Also we don't need the NeedsHelm option in ToolStatus. --- src/warnet/constants.py | 1 - src/warnet/project.py | 10 ---------- 2 files changed, 11 deletions(-) diff --git a/src/warnet/constants.py b/src/warnet/constants.py index 722fc94a1..99bdf2c5c 100644 --- a/src/warnet/constants.py +++ b/src/warnet/constants.py @@ -109,7 +109,6 @@ ] # Helm binary -HELM_LATEST_URL = "https://get.helm.sh/helm-latest-version" HELM_DOWNLOAD_URL_STUB = "https://get.helm.sh/" HELM_BINARY_NAME = "helm" HELM_BLESSED_VERSION = "v3.16.1" diff --git a/src/warnet/project.py b/src/warnet/project.py index 34b865c7a..014e81ba5 100644 --- a/src/warnet/project.py +++ b/src/warnet/project.py @@ -20,7 +20,6 @@ HELM_BLESSED_NAME_AND_CHECKSUMS, HELM_BLESSED_VERSION, HELM_DOWNLOAD_URL_STUB, - HELM_LATEST_URL, ) from .graph import inquirer_create_network from .network import copy_network_defaults, copy_scenario_defaults @@ -31,7 +30,6 @@ def setup(): """Setup warnet""" class ToolStatus(Enum): - NeedsHelm = auto() Satisfied = auto() Unsatisfied = auto() @@ -429,14 +427,6 @@ def download_file(url, destination): raise Exception(f"Failed to download {url} (status code {response.status_code})") -def get_latest_version_of_helm() -> Optional[str]: - response = requests.get(HELM_LATEST_URL) - if response.status_code == 200: - return response.text.strip() - else: - return None - - def query_arch_from_uname(arch: str) -> Optional[str]: if arch.startswith("armv5"): return "armv5" From 44adc3e85031990b53281b6c1ce81f76383a94ed Mon Sep 17 00:00:00 2001 From: Grant Date: Fri, 27 Sep 2024 10:28:12 -0500 Subject: [PATCH 08/10] shorten user prompt --- src/warnet/project.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/warnet/project.py b/src/warnet/project.py index 014e81ba5..92815d4a1 100644 --- a/src/warnet/project.py +++ b/src/warnet/project.py @@ -186,7 +186,7 @@ def is_helm_installed_and_offer_if_not() -> tuple[bool, str]: inquirer.Confirm( "install_helm", message=click.style( - "Would you like to use Warnet's downloader to install Helm into your virtual environment?", + "Would you like Warnet to install Helm into your virtual environment?", fg="blue", bold=True, ), From c7678a5a3573a580d8eb846cd70bef6a2704ebed Mon Sep 17 00:00:00 2001 From: Grant Date: Fri, 27 Sep 2024 10:28:20 -0500 Subject: [PATCH 09/10] support "arm64" --- src/warnet/project.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/warnet/project.py b/src/warnet/project.py index 92815d4a1..3a9e96deb 100644 --- a/src/warnet/project.py +++ b/src/warnet/project.py @@ -434,7 +434,7 @@ def query_arch_from_uname(arch: str) -> Optional[str]: return "armv6" elif arch.startswith("armv7"): return "arm" - elif arch == "aarch64": + elif arch == "aarch64" or arch == "arm64": return "arm64" elif arch == "x86": return "386" From 82b5e4780a0f6c926643883dd6e6fe7a89525afc Mon Sep 17 00:00:00 2001 From: Grant Date: Fri, 27 Sep 2024 10:35:08 -0500 Subject: [PATCH 10/10] update missing arch error message Show user their arch instead of showing them "None" --- src/warnet/project.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/warnet/project.py b/src/warnet/project.py index 3a9e96deb..05d6237a4 100644 --- a/src/warnet/project.py +++ b/src/warnet/project.py @@ -500,7 +500,7 @@ def install_helm_rootlessly_to_venv(): uname_arch = os.uname().machine arch = query_arch_from_uname(uname_arch) if not arch: - click.secho(f"No Helm binary candidate for arch: {arch}", fg="red") + click.secho(f"No Helm binary candidate for arch: {uname_arch}", fg="red") sys.exit(1) helm_filename = f"{HELM_BINARY_NAME}-{version}-{os_name}-{arch}.tar.gz"