From 97377469232f33868e427c0c8b239d77d086d112 Mon Sep 17 00:00:00 2001 From: "Waylon S. Walker" Date: Wed, 9 Apr 2025 09:15:00 -0500 Subject: [PATCH] more package support --- krayt/bundles.py | 87 +++++++++++++++++++ krayt/cli/create.py | 7 +- krayt/package.py | 166 +++++++++++++++++++++++++++++++++++++ krayt/templates/install.sh | 33 +++++--- pyproject.toml | 1 + 5 files changed, 283 insertions(+), 11 deletions(-) create mode 100644 krayt/bundles.py create mode 100644 krayt/package.py diff --git a/krayt/bundles.py b/krayt/bundles.py new file mode 100644 index 0000000..baed9c9 --- /dev/null +++ b/krayt/bundles.py @@ -0,0 +1,87 @@ +""" +Bundles of packages available in most package managers. +""" + +basics = [ + "curl", + "wget", + "jq", + "yq", + "bash", + "coreutils", +] +pretty = [ + *basics + "starship", + "atuin", + "bash", + "zsh", + "fish", + "bat", + "eza", +] +networking = [ + *basics + "mtr", + "bind-tools", + "aws-cli", + "curl", + "wget", + "iperf3", + "nmap", + "traceroute", + "netcat-openbsd", +] + +database = [ + *basics + "sqlite", + "sqlite-dev", + "sqlite-libs", + "postgresql", + "mysql", + "mariadb", + "redis", + "mongodb", +] + +storage = [ + *basics + "ncdu", + "dust", + "file", + "hexyl", + "ripgrep", + "fd", + "fzf", + "difftastic", +] + +search = [ + *basics + "ripgrep", + "fd", + "fzf", + "difftastic", +] + +monitoring = [ + *basics + "htop", + "bottom", + "mtr", +] + +all = list( + set( + [ + *basics, + *pretty, + *networking, + *database, + *storage, + *search, + *monitoring, + ] + ) +) diff --git a/krayt/cli/create.py b/krayt/cli/create.py index 41be061..2e7d373 100644 --- a/krayt/cli/create.py +++ b/krayt/cli/create.py @@ -6,7 +6,7 @@ import os from pathlib import Path import time import typer -from typing import Any, Optional +from typing import Any, List, Optional import yaml KRAYT_VERSION = "NIGHTLY" @@ -324,6 +324,11 @@ def create_inspector_job( volumes: list, image: str = "alpine:latest", imagepullsecret: Optional[str] = None, + additional_packages: Optional[List[str]] = None, + pre_init_scripts: Optional[List[str]] = None, + post_init_scripts: Optional[List[str]] = None, + pre_init_hooks: Optional[List[str]] = None, + post_init_hooks: Optional[List[str]] = None, ): """Create a Krayt inspector job with the given mounts""" timestamp = int(time.time()) diff --git a/krayt/package.py b/krayt/package.py new file mode 100644 index 0000000..d88d494 --- /dev/null +++ b/krayt/package.py @@ -0,0 +1,166 @@ +from more_itertools import unique_everseen +from pydantic import BaseModel, BeforeValidator, model_validator +from typing import Annotated, List, Literal, Optional +from typing_extensions import Self + + +SUPPORTED_KINDS = { + "system", + "uv", + "installer", + "i", + "curlbash", + "curlsh", + "brew", + "cargo", + "pipx", + "npm", + "go", + "gh", +} + + +def validate_kind(v): + if v not in SUPPORTED_KINDS: + raise ValueError( + f"Unknown installer kind: {v}\n Supported kinds: {SUPPORTED_KINDS}\n " + ) + return v + + +class Package(BaseModel): + """ + Represents a package to be installed, either via system package manager + or an alternative installer like uv, installer.sh, brew, etc. + """ + + kind: Annotated[ + Literal[ + "system", + "uv", + "i", + "curlsh", + "brew", + "cargo", + "pipx", + "npm", + "go", + "gh", + ], + BeforeValidator(validate_kind), + ] = "system" + dependencies: Optional[List[str]] = None + value: str + + @classmethod + def from_raw(cls, raw: str) -> "Package": + """ + Parse a raw input string like 'uv:copier' into a Package(kind='uv', value='copier') + """ + if ":" in raw: + prefix, value = raw.split(":", 1) + return cls(kind=prefix.strip(), value=value.strip()) + else: + return cls(kind="system", value=raw.strip()) + + @model_validator(mode="after") + def validate_dependencies(self) -> Self: + if self.dependencies: + return self + else: + if self.kind == "system": + return self + dependencies = [] + if self.kind in ["uv", "i", "installer", "curlbash", "curlsh", "gh"]: + dependencies.extend( + [ + Package.from_raw("curl"), + ] + ) + if self.kind == "brew": + dependencies.extend( + [ + Package.from_raw("brew"), + Package.from_raw("git"), + ] + ) + if self.kind == "cargo": + dependencies.extend( + [ + Package.from_raw("cargo"), + ] + ) + if self.kind == "pipx": + dependencies.extend( + [ + Package.from_raw("pipx"), + ] + ) + if self.kind == "npm": + dependencies.extend( + [ + Package.from_raw("npm"), + ] + ) + if self.kind == "go": + dependencies.extend( + [ + Package.from_raw("go"), + ] + ) + self.dependencies = dependencies + return self + + def __str__(self): + return f"{self.kind}:{self.value}" if self.kind != "system" else self.value + + def is_system(self) -> bool: + return self.kind == "system" + + def install_command(self) -> str: + """ + Generate the bash install command snippet for this package. + """ + if self.kind == "system": + return f"detect_package_manager_and_install {self.value}" + elif self.kind == "uv": + return f"uv tool install {self.value}" + elif self.kind in ["i", "installer", "gh"]: + return f"curl -fsSL https://i.jpillora.com/{self.value} | sh" + elif self.kind == "curlsh": + return f"curl -fsSL {self.value} | sh" + elif self.kind == "curlbash": + return f"curl -fsSL {self.value} | bash" + elif self.kind == "brew": + return f"brew install {self.value}" + elif self.kind == "cargo": + return f"cargo install {self.value}" + elif self.kind == "pipx": + return f"pipx install {self.value}" + elif self.kind == "npm": + return f"npm install -g {self.value}" + elif self.kind == "go": + return f"go install {self.value}@latest" + else: + raise ValueError(f"Unknown install method for kind={self.kind}") + + +if __name__ == "__main__": + raw_inputs = [ + "curl", + "wget", + "uv:copier", + "i:sharkdp/fd", + "curlsh:https://example.com/install.sh", + "brew:bat", + ] + + packages = [Package.from_raw(raw) for raw in raw_inputs] + dependencies = [] + for package in packages: + if package.dependencies: + dependencies.extend( + [dependency.install_command() for dependency in package.dependencies] + ) + installs = [package.install_command() for package in packages] + print("\n".join(install for install in unique_everseen([*dependencies, *installs]))) diff --git a/krayt/templates/install.sh b/krayt/templates/install.sh index df37270..8e0c8d9 100644 --- a/krayt/templates/install.sh +++ b/krayt/templates/install.sh @@ -1,7 +1,7 @@ {% if additional_packages %} -detect_package_manager_and_install_command() { +detect_package_manager_and_install() { if [ $# -eq 0 ]; then - echo "Usage: detect_package_manager_and_install_command [package2] [...]" + echo "Usage: detect_package_manager_and_install [package2] [...]" return 1 fi @@ -34,19 +34,32 @@ detect_package_manager_and_install_command() { return 2 fi - PACKAGES="$*" + echo "Using package manager: $PKG_MANAGER" if [ -n "$UPDATE_CMD" ]; then - echo "$UPDATE_CMD - echo $INSTALL_CMD $PACKAGES" - $UPDATE_CMD - $INSTALL_CMD $PACKAGES + echo "Running package manager update..." + eval "$UPDATE_CMD" + fi + FAILED_PKGS=() + + for pkg in "$@"; do + echo "Installing package: $pkg" + if ! eval "$INSTALL_CMD $pkg"; then + echo "⚠️ Warning: Failed to install package: $pkg" + FAILED_PKGS+=("$pkg") + fi + done + + if [ ${#FAILED_PKGS[@]} -ne 0 ]; then + echo "⚠️ The following packages failed to install:" + for failed_pkg in "${FAILED_PKGS[@]}"; do + echo " - $failed_pkg" + done else - echo "$INSTALL_CMD $PACKAGES" - $INSTALL_CMD $PACKAGES + echo "✅ All requested packages installed successfully." fi } -detect_package_manager_and_install_command {% for package in additional_packages %}{{ package | trim }}{% if not loop.last %} {% endif %}{% endfor %} +detect_package_manager_and_install {% for package in additional_packages %}{{ package | trim }}{% if not loop.last %} {% endif %}{% endfor %} {% endif %} diff --git a/pyproject.toml b/pyproject.toml index a521984..cfeb2a1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -31,6 +31,7 @@ dependencies = [ "InquirerPy", "jinja2", "iterfzf", + "pydantic", ] [[project.authors]]