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 (
19+ HELM_BINARY_NAME ,
20+ HELM_BLESSED_NAME_AND_CHECKSUMS ,
21+ HELM_BLESSED_VERSION ,
22+ HELM_DOWNLOAD_URL_STUB ,
23+ )
1324from .graph import inquirer_create_network
1425from .network import copy_network_defaults , copy_scenario_defaults
1526
@@ -155,7 +166,7 @@ def is_kubectl_installed() -> tuple[bool, str]:
155166 except FileNotFoundError as err :
156167 return False , str (err )
157168
158- def is_helm_installed () -> tuple [bool , str ]:
169+ def is_helm_installed_and_offer_if_not () -> tuple [bool , str ]:
159170 try :
160171 version_result = subprocess .run (["helm" , "version" ], capture_output = True , text = True )
161172 location_result = subprocess .run (
@@ -167,8 +178,31 @@ def is_helm_installed() -> tuple[bool, str]:
167178 return version_result .returncode == 0 , location_result .stdout .strip ()
168179 else :
169180 return False , ""
170- except FileNotFoundError as err :
171- return False , str (err )
181+
182+ except FileNotFoundError :
183+ print ()
184+ helm_answer = inquirer .prompt (
185+ [
186+ inquirer .Confirm (
187+ "install_helm" ,
188+ message = click .style (
189+ "Would you like Warnet to install Helm into your virtual environment?" ,
190+ fg = "blue" ,
191+ bold = True ,
192+ ),
193+ default = True ,
194+ ),
195+ ]
196+ )
197+ if helm_answer is None :
198+ msg = "Setup cancelled by user."
199+ click .secho (msg , fg = "yellow" )
200+ return False , msg
201+ if helm_answer ["install_helm" ]:
202+ click .secho (" Installing Helm..." , fg = "yellow" , bold = True )
203+ install_helm_rootlessly_to_venv ()
204+ return is_helm_installed_and_offer_if_not ()
205+ return False , "Please install Helm."
172206
173207 def check_installation (tool_info : ToolInfo ) -> ToolStatus :
174208 has_good_version , location = tool_info .is_installed_func ()
@@ -218,8 +252,8 @@ def check_installation(tool_info: ToolInfo) -> ToolStatus:
218252 )
219253 helm_info = ToolInfo (
220254 tool_name = "Helm" ,
221- is_installed_func = is_helm_installed ,
222- install_instruction = "Install Helm from Helm's official site." ,
255+ is_installed_func = is_helm_installed_and_offer_if_not ,
256+ install_instruction = "Install Helm from Helm's official site, or rootlessly install Helm using Warnet's downloader when prompted ." ,
223257 install_url = "https://helm.sh/docs/intro/install/" ,
224258 )
225259 minikube_info = ToolInfo (
@@ -361,3 +395,137 @@ def init():
361395 """Initialize a warnet project in the current directory"""
362396 current_dir = Path .cwd ()
363397 new_internal (directory = current_dir , from_init = True )
398+
399+
400+ def get_os_name_for_helm () -> Optional [str ]:
401+ """Return a short operating system name suitable for downloading a helm binary."""
402+ uname_sys = platform .system ().lower ()
403+ if "linux" in uname_sys :
404+ return "linux"
405+ elif uname_sys == "darwin" :
406+ return "darwin"
407+ elif "win" in uname_sys :
408+ return "windows"
409+ return None
410+
411+
412+ def is_in_virtualenv () -> bool :
413+ """Check if the user is in a virtual environment."""
414+ return hasattr (sys , "real_prefix" ) or (
415+ hasattr (sys , "base_prefix" ) and sys .base_prefix != sys .prefix
416+ )
417+
418+
419+ def download_file (url , destination ):
420+ click .secho (f" Downloading { url } " , fg = "blue" )
421+ response = requests .get (url , stream = True )
422+ if response .status_code == 200 :
423+ with open (destination , "wb" ) as f :
424+ for chunk in response .iter_content (1024 ):
425+ f .write (chunk )
426+ else :
427+ raise Exception (f"Failed to download { url } (status code { response .status_code } )" )
428+
429+
430+ def query_arch_from_uname (arch : str ) -> Optional [str ]:
431+ if arch .startswith ("armv5" ):
432+ return "armv5"
433+ elif arch .startswith ("armv6" ):
434+ return "armv6"
435+ elif arch .startswith ("armv7" ):
436+ return "arm"
437+ elif arch == "aarch64" or arch == "arm64" :
438+ return "arm64"
439+ elif arch == "x86" :
440+ return "386"
441+ elif arch == "x86_64" :
442+ return "amd64"
443+ elif arch == "i686" or arch == "i386" :
444+ return "386"
445+ else :
446+ return None
447+
448+
449+ def write_blessed_checksum (helm_filename : str , dest_path : str ):
450+ checksum = next (
451+ (b ["checksum" ] for b in HELM_BLESSED_NAME_AND_CHECKSUMS if b ["name" ] == helm_filename ), None
452+ )
453+ if checksum :
454+ with open (dest_path , "w" ) as f :
455+ f .write (checksum )
456+ else :
457+ click .secho ("Could not find a matching helm binary and checksum" , fg = "red" )
458+
459+
460+ def verify_checksum (file_path , checksum_path ):
461+ click .secho (" Verifying checksum..." , fg = "blue" )
462+ sha256_hash = hashlib .sha256 ()
463+ with open (file_path , "rb" ) as f :
464+ for byte_block in iter (lambda : f .read (4096 ), b"" ):
465+ sha256_hash .update (byte_block )
466+
467+ with open (checksum_path ) as f :
468+ expected_checksum = f .read ().strip ()
469+
470+ if sha256_hash .hexdigest () != expected_checksum :
471+ raise Exception ("Checksum verification failed!" )
472+ click .secho (" Checksum verified." , fg = "blue" )
473+
474+
475+ def install_helm_to_venv (helm_bin_path ):
476+ venv_bin_dir = os .path .join (sys .prefix , "bin" )
477+ helm_dst_path = os .path .join (venv_bin_dir , HELM_BINARY_NAME )
478+ shutil .move (helm_bin_path , helm_dst_path )
479+ os .chmod (helm_dst_path , 0o755 )
480+ click .secho (f" { HELM_BINARY_NAME } installed into { helm_dst_path } " , fg = "blue" )
481+
482+
483+ def install_helm_rootlessly_to_venv ():
484+ if not is_in_virtualenv ():
485+ click .secho (
486+ "Error: You are not in a virtual environment. Please activate a virtual environment and try again." ,
487+ fg = "yellow" ,
488+ )
489+ sys .exit (1 )
490+
491+ version = HELM_BLESSED_VERSION
492+
493+ os_name = get_os_name_for_helm ()
494+ if os_name is None :
495+ click .secho (
496+ "Error: Could not determine the operating system of this computer." , fg = "yellow"
497+ )
498+ sys .exit (1 )
499+
500+ uname_arch = os .uname ().machine
501+ arch = query_arch_from_uname (uname_arch )
502+ if not arch :
503+ click .secho (f"No Helm binary candidate for arch: { uname_arch } " , fg = "red" )
504+ sys .exit (1 )
505+
506+ helm_filename = f"{ HELM_BINARY_NAME } -{ version } -{ os_name } -{ arch } .tar.gz"
507+ helm_url = f"{ HELM_DOWNLOAD_URL_STUB } { helm_filename } "
508+
509+ try :
510+ with tempfile .TemporaryDirectory () as temp_dir :
511+ helm_archive_path = os .path .join (temp_dir , helm_filename )
512+ checksum_path = os .path .join (temp_dir , f"{ helm_filename } .sha256" )
513+
514+ download_file (helm_url , helm_archive_path )
515+ write_blessed_checksum (helm_filename , checksum_path )
516+ verify_checksum (helm_archive_path , checksum_path )
517+
518+ # Extract Helm and install it in the virtual environment's bin folder
519+ with tarfile .open (helm_archive_path , "r:gz" ) as tar :
520+ tar .extractall (path = temp_dir )
521+ helm_bin_path = os .path .join (temp_dir , os_name + "-" + arch , HELM_BINARY_NAME )
522+ install_helm_to_venv (helm_bin_path )
523+
524+ click .secho (
525+ f" { HELM_BINARY_NAME } { version } installed successfully to your virtual environment!\n " ,
526+ fg = "blue" ,
527+ )
528+
529+ except Exception as e :
530+ click .secho (f"Error: { e } \n Could not install helm." , fg = "yellow" )
531+ sys .exit (1 )
0 commit comments