1
+ import hashlib
1
2
import os
2
3
import platform
4
+ import shutil
3
5
import subprocess
4
6
import sys
7
+ import tarfile
8
+ import tempfile
5
9
from dataclasses import dataclass
6
10
from enum import Enum , auto
7
11
from pathlib import Path
8
- from typing import Callable
12
+ from typing import Callable , Optional
9
13
10
14
import click
11
15
import inquirer
16
+ import requests
12
17
18
+ from .constants import (
19
+ HELM_BINARY_NAME ,
20
+ HELM_BLESSED_NAME_AND_CHECKSUMS ,
21
+ HELM_BLESSED_VERSION ,
22
+ HELM_DOWNLOAD_URL_STUB ,
23
+ )
13
24
from .graph import inquirer_create_network
14
25
from .network import copy_network_defaults , copy_scenario_defaults
15
26
@@ -155,7 +166,7 @@ def is_kubectl_installed() -> tuple[bool, str]:
155
166
except FileNotFoundError as err :
156
167
return False , str (err )
157
168
158
- def is_helm_installed () -> tuple [bool , str ]:
169
+ def is_helm_installed_and_offer_if_not () -> tuple [bool , str ]:
159
170
try :
160
171
version_result = subprocess .run (["helm" , "version" ], capture_output = True , text = True )
161
172
location_result = subprocess .run (
@@ -167,8 +178,31 @@ def is_helm_installed() -> tuple[bool, str]:
167
178
return version_result .returncode == 0 , location_result .stdout .strip ()
168
179
else :
169
180
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."
172
206
173
207
def check_installation (tool_info : ToolInfo ) -> ToolStatus :
174
208
has_good_version , location = tool_info .is_installed_func ()
@@ -218,8 +252,8 @@ def check_installation(tool_info: ToolInfo) -> ToolStatus:
218
252
)
219
253
helm_info = ToolInfo (
220
254
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 ." ,
223
257
install_url = "https://helm.sh/docs/intro/install/" ,
224
258
)
225
259
minikube_info = ToolInfo (
@@ -361,3 +395,137 @@ def init():
361
395
"""Initialize a warnet project in the current directory"""
362
396
current_dir = Path .cwd ()
363
397
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