1+ import hashlib
12import os
23import platform
4+ import shutil
35import subprocess
46import sys
7+ import tarfile
8+ import tempfile
59from dataclasses import dataclass
610from enum import Enum , auto
711from pathlib import Path
8- from typing import Callable
12+ from typing import Callable , Optional
913
1014import click
1115import inquirer
16+ import requests
1217
18+ from .constants import HELM_BINARY_NAME , HELM_DOWNLOAD_URL_STUB , HELM_LATEST_URL
1319from .graph import inquirer_create_network
1420from .network import copy_network_defaults , copy_scenario_defaults
1521
@@ -19,6 +25,7 @@ def setup():
1925 """Setup warnet"""
2026
2127 class ToolStatus (Enum ):
28+ NeedsHelm = auto ()
2229 Satisfied = auto ()
2330 Unsatisfied = auto ()
2431
@@ -155,7 +162,7 @@ def is_kubectl_installed() -> tuple[bool, str]:
155162 except FileNotFoundError as err :
156163 return False , str (err )
157164
158- def is_helm_installed () -> tuple [bool , str ]:
165+ def is_helm_installed_and_offer_if_not () -> tuple [bool , str ]:
159166 try :
160167 version_result = subprocess .run (["helm" , "version" ], capture_output = True , text = True )
161168 location_result = subprocess .run (
@@ -167,8 +174,31 @@ def is_helm_installed() -> tuple[bool, str]:
167174 return version_result .returncode == 0 , location_result .stdout .strip ()
168175 else :
169176 return False , ""
170- except FileNotFoundError as err :
171- return False , str (err )
177+
178+ except FileNotFoundError :
179+ print ()
180+ helm_answer = inquirer .prompt (
181+ [
182+ inquirer .Confirm (
183+ "install_helm" ,
184+ message = click .style (
185+ "Would you like to use Warnet's downloader to install Helm into your virtual environment?" ,
186+ fg = "blue" ,
187+ bold = True ,
188+ ),
189+ default = True ,
190+ ),
191+ ]
192+ )
193+ if helm_answer is None :
194+ msg = "Setup cancelled by user."
195+ click .secho (msg , fg = "yellow" )
196+ return False , msg
197+ if helm_answer ["install_helm" ]:
198+ click .secho (" Installing Helm..." , fg = "yellow" , bold = True )
199+ install_helm_rootlessly_to_venv ()
200+ return is_helm_installed_and_offer_if_not ()
201+ return False , "Please install Helm."
172202
173203 def check_installation (tool_info : ToolInfo ) -> ToolStatus :
174204 has_good_version , location = tool_info .is_installed_func ()
@@ -218,8 +248,8 @@ def check_installation(tool_info: ToolInfo) -> ToolStatus:
218248 )
219249 helm_info = ToolInfo (
220250 tool_name = "Helm" ,
221- is_installed_func = is_helm_installed ,
222- install_instruction = "Install Helm from Helm's official site." ,
251+ is_installed_func = is_helm_installed_and_offer_if_not ,
252+ install_instruction = "Install Helm from Helm's official site, or rootlessly install Helm using Warnet's downloader when prompted ." ,
223253 install_url = "https://helm.sh/docs/intro/install/" ,
224254 )
225255 minikube_info = ToolInfo (
@@ -361,3 +391,120 @@ def init():
361391 """Initialize a warnet project in the current directory"""
362392 current_dir = Path .cwd ()
363393 new_internal (directory = current_dir , from_init = True )
394+
395+
396+ def get_os_name_for_helm () -> Optional [str ]:
397+ """Return a short operating system name suitable for downloading a helm binary."""
398+ uname_sys = platform .system ().lower ()
399+ if "linux" in uname_sys :
400+ return "linux"
401+ elif uname_sys == "darwin" :
402+ return "darwin"
403+ elif "win" in uname_sys :
404+ return "windows"
405+ return None
406+
407+
408+ def is_in_virtualenv () -> bool :
409+ """Check if the user is in a virtual environment."""
410+ return hasattr (sys , "real_prefix" ) or (
411+ hasattr (sys , "base_prefix" ) and sys .base_prefix != sys .prefix
412+ )
413+
414+
415+ def download_file (url , destination ):
416+ click .secho (f" Downloading { url } " , fg = "blue" )
417+ response = requests .get (url , stream = True )
418+ if response .status_code == 200 :
419+ with open (destination , "wb" ) as f :
420+ for chunk in response .iter_content (1024 ):
421+ f .write (chunk )
422+ else :
423+ raise Exception (f"Failed to download { url } (status code { response .status_code } )" )
424+
425+
426+ def get_latest_version_of_helm () -> Optional [str ]:
427+ response = requests .get (HELM_LATEST_URL )
428+ if response .status_code == 200 :
429+ return response .text .strip ()
430+ else :
431+ return None
432+
433+
434+ def verify_checksum (file_path , checksum_path ):
435+ click .secho (" Verifying checksum..." , fg = "blue" )
436+ sha256_hash = hashlib .sha256 ()
437+ with open (file_path , "rb" ) as f :
438+ for byte_block in iter (lambda : f .read (4096 ), b"" ):
439+ sha256_hash .update (byte_block )
440+
441+ with open (checksum_path ) as f :
442+ expected_checksum = f .read ().strip ()
443+
444+ if sha256_hash .hexdigest () != expected_checksum :
445+ raise Exception ("Checksum verification failed!" )
446+ click .secho (" Checksum verified." , fg = "blue" )
447+
448+
449+ def install_helm_to_venv (helm_bin_path ):
450+ venv_bin_dir = os .path .join (sys .prefix , "bin" )
451+ helm_dst_path = os .path .join (venv_bin_dir , HELM_BINARY_NAME )
452+ shutil .move (helm_bin_path , helm_dst_path )
453+ os .chmod (helm_dst_path , 0o755 )
454+ click .secho (f" { HELM_BINARY_NAME } installed into { helm_dst_path } " , fg = "blue" )
455+
456+
457+ def install_helm_rootlessly_to_venv ():
458+ if not is_in_virtualenv ():
459+ click .secho (
460+ "Error: You are not in a virtual environment. Please activate a virtual environment and try again." ,
461+ fg = "yellow" ,
462+ )
463+ sys .exit (1 )
464+
465+ version = get_latest_version_of_helm ()
466+ if version is None :
467+ click .secho (
468+ "Error: Could not fetch the latest version of Helm. Please check your internet connection." ,
469+ fg = "yellow" ,
470+ )
471+ sys .exit (1 )
472+
473+ os_name = get_os_name_for_helm ()
474+ if os_name is None :
475+ click .secho (
476+ "Error: Could not determine the operating system of this computer." , fg = "yellow"
477+ )
478+ sys .exit (1 )
479+
480+ arch = os .uname ().machine
481+ arch_map = {"x86_64" : "amd64" , "i386" : "386" , "aarch64" : "arm64" , "armv7l" : "arm" }
482+ arch = arch_map .get (arch , arch )
483+
484+ helm_filename = f"{ HELM_BINARY_NAME } -{ version } -{ os_name } -{ arch } .tar.gz"
485+ helm_url = f"{ HELM_DOWNLOAD_URL_STUB } { helm_filename } "
486+ checksum_url = f"{ helm_url } .sha256"
487+
488+ try :
489+ with tempfile .TemporaryDirectory () as temp_dir :
490+ helm_archive_path = os .path .join (temp_dir , helm_filename )
491+ checksum_path = os .path .join (temp_dir , f"{ helm_filename } .sha256" )
492+
493+ download_file (helm_url , helm_archive_path )
494+ download_file (checksum_url , checksum_path )
495+ verify_checksum (helm_archive_path , checksum_path )
496+
497+ # Extract Helm and install it in the virtual environment's bin folder
498+ with tarfile .open (helm_archive_path , "r:gz" ) as tar :
499+ tar .extractall (path = temp_dir )
500+ helm_bin_path = os .path .join (temp_dir , os_name + "-" + arch , HELM_BINARY_NAME )
501+ install_helm_to_venv (helm_bin_path )
502+
503+ click .secho (
504+ f" { HELM_BINARY_NAME } { version } installed successfully to your virtual environment!\n " ,
505+ fg = "blue" ,
506+ )
507+
508+ except Exception as e :
509+ click .secho (f"Error: { e } \n Could not install helm." , fg = "yellow" )
510+ sys .exit (1 )
0 commit comments