Skip to content

Commit d343793

Browse files
authored
Merge pull request #610 from mplsgrant/2024-09-install-helm
Offer to install helm into venv
2 parents e6ac03e + 82b5e47 commit d343793

File tree

2 files changed

+225
-6
lines changed

2 files changed

+225
-6
lines changed

src/warnet/constants.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,3 +107,54 @@
107107
"helm repo update",
108108
f"helm upgrade --install ingress-nginx ingress-nginx/ingress-nginx --namespace {INGRESS_NAMESPACE} --create-namespace",
109109
]
110+
111+
# Helm binary
112+
HELM_DOWNLOAD_URL_STUB = "https://get.helm.sh/"
113+
HELM_BINARY_NAME = "helm"
114+
HELM_BLESSED_VERSION = "v3.16.1"
115+
HELM_BLESSED_NAME_AND_CHECKSUMS = [
116+
{
117+
"name": "helm-v3.16.1-darwin-amd64.tar.gz",
118+
"checksum": "1b194824e36da3e3889920960a93868b541c7888c905a06757e88666cfb562c9",
119+
},
120+
{
121+
"name": "helm-v3.16.1-darwin-arm64.tar.gz",
122+
"checksum": "405a3b13f0e194180f7b84010dfe86689d7703e80612729882ad71e2a4ef3504",
123+
},
124+
{
125+
"name": "helm-v3.16.1-linux-amd64.tar.gz",
126+
"checksum": "e57e826410269d72be3113333dbfaac0d8dfdd1b0cc4e9cb08bdf97722731ca9",
127+
},
128+
{
129+
"name": "helm-v3.16.1-linux-arm.tar.gz",
130+
"checksum": "a15a8ddfc373628b13cd2a987206756004091a1f6a91c3b9ee8de6f0b1e2ce90",
131+
},
132+
{
133+
"name": "helm-v3.16.1-linux-arm64.tar.gz",
134+
"checksum": "780b5b86f0db5546769b3e9f0204713bbdd2f6696dfdaac122fbe7f2f31541d2",
135+
},
136+
{
137+
"name": "helm-v3.16.1-linux-386.tar.gz",
138+
"checksum": "92d7a47a90734b50528ffffc99cd1b2d4b9fc0f4291bac92c87ef03406a5a7b2",
139+
},
140+
{
141+
"name": "helm-v3.16.1-linux-ppc64le.tar.gz",
142+
"checksum": "9f0178957c94516eff9a3897778edb93d78fab1f76751bd282883f584ea81c23",
143+
},
144+
{
145+
"name": "helm-v3.16.1-linux-s390x.tar.gz",
146+
"checksum": "357f8b441cc535240f1b0ba30a42b44571d4c303dab004c9e013697b97160360",
147+
},
148+
{
149+
"name": "helm-v3.16.1-linux-riscv64.tar.gz",
150+
"checksum": "9a2cab45b7d9282e9be7b42f86d8034dcaa2e81ab338642884843676c2f6929f",
151+
},
152+
{
153+
"name": "helm-v3.16.1-windows-amd64.zip",
154+
"checksum": "89952ea1bace0a9498053606296ea03cf743c48294969dfc731e7f78d1dc809a",
155+
},
156+
{
157+
"name": "helm-v3.16.1-windows-arm64.zip",
158+
"checksum": "fc370a291ed926da5e77acf42006de48e7fd5ff94d20c3f6aa10c04fea66e53c",
159+
},
160+
]

src/warnet/project.py

Lines changed: 174 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,26 @@
1+
import hashlib
12
import os
23
import platform
4+
import shutil
35
import subprocess
46
import sys
7+
import tarfile
8+
import tempfile
59
from dataclasses import dataclass
610
from enum import Enum, auto
711
from pathlib import Path
8-
from typing import Callable
12+
from typing import Callable, Optional
913

1014
import click
1115
import 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+
)
1324
from .graph import inquirer_create_network
1425
from .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}\nCould not install helm.", fg="yellow")
531+
sys.exit(1)

0 commit comments

Comments
 (0)