From 378744632fd2ad6051e89cf0335ae55208e36de7 Mon Sep 17 00:00:00 2001 From: "Waylon S. Walker" Date: Thu, 10 Apr 2025 09:21:51 -0500 Subject: [PATCH] wip --- krayt/bundles.py | 12 +- krayt/cli/__init__.py | 12 +- krayt/cli/bundles.py | 25 +++++ krayt/cli/{create.py => pod.py} | 193 +++++++++++++++----------------- krayt/cli/templates.py | 20 +++- krayt/package.py | 116 +++++++++---------- pyproject.toml | 3 +- 7 files changed, 208 insertions(+), 173 deletions(-) create mode 100644 krayt/cli/bundles.py rename krayt/cli/{create.py => pod.py} (84%) diff --git a/krayt/bundles.py b/krayt/bundles.py index baed9c9..a375140 100644 --- a/krayt/bundles.py +++ b/krayt/bundles.py @@ -11,7 +11,7 @@ basics = [ "coreutils", ] pretty = [ - *basics + *basics, "starship", "atuin", "bash", @@ -21,7 +21,7 @@ pretty = [ "eza", ] networking = [ - *basics + *basics, "mtr", "bind-tools", "aws-cli", @@ -34,7 +34,7 @@ networking = [ ] database = [ - *basics + *basics, "sqlite", "sqlite-dev", "sqlite-libs", @@ -46,7 +46,7 @@ database = [ ] storage = [ - *basics + *basics, "ncdu", "dust", "file", @@ -58,7 +58,7 @@ storage = [ ] search = [ - *basics + *basics, "ripgrep", "fd", "fzf", @@ -66,7 +66,7 @@ search = [ ] monitoring = [ - *basics + *basics, "htop", "bottom", "mtr", diff --git a/krayt/cli/__init__.py b/krayt/cli/__init__.py index 71fe124..06e5c40 100644 --- a/krayt/cli/__init__.py +++ b/krayt/cli/__init__.py @@ -1,12 +1,18 @@ from krayt import __version__ -from krayt.cli.create import app as create_app +from krayt.cli.bundles import app as bundles_app +from krayt.cli.pod import app as pod_app, create, exec, logs from krayt.cli.templates import app as templates_app from typer import Typer app = Typer() -app.add_typer(templates_app, name="templates") -app.add_typer(create_app, name="create") +app.add_typer(templates_app, name="templates", no_args_is_help=True) +app.add_typer(pod_app, name="pod", no_args_is_help=True) +app.command(name="create")(create) +app.command(name="c")(create) +app.command(name="exec")(exec) +app.command(name="logs")(logs) +app.add_typer(bundles_app, name="bundles", no_args_is_help=True) @app.command() diff --git a/krayt/cli/bundles.py b/krayt/cli/bundles.py new file mode 100644 index 0000000..be30fe8 --- /dev/null +++ b/krayt/cli/bundles.py @@ -0,0 +1,25 @@ +from krayt import bundles +import typer + +app = typer.Typer() + + +@app.command() +def list( + verbose: bool = typer.Option( + False, + "--verbose", + "-v", + help="Verbose output", + ), +): + """List available bundles""" + typer.echo("Available bundles:") + # get all variables from bundles + for bundle in bundles.__dict__.keys(): + if bundle.startswith("__"): + continue + typer.echo(bundle) + if verbose: + for package in bundles.__dict__[bundle]: + typer.echo(f" - {package}") diff --git a/krayt/cli/create.py b/krayt/cli/pod.py similarity index 84% rename from krayt/cli/create.py rename to krayt/cli/pod.py index 2e7d373..93a9a80 100644 --- a/krayt/cli/create.py +++ b/krayt/cli/pod.py @@ -1,16 +1,13 @@ -from iterfzf import iterfzf +import iterfzf from krayt.templates import env from kubernetes import client, config import logging import os -from pathlib import Path import time import typer from typing import Any, List, Optional import yaml -KRAYT_VERSION = "NIGHTLY" - logging.basicConfig(level=logging.WARNING) app = typer.Typer() @@ -92,7 +89,15 @@ def fuzzy_select(items): # Use fzf for selection try: - selected = iterfzf(formatted_items) + # selected = inquirer.fuzzy( + # message="Select a pod to clone:", choices=formatted_items + # ).execute() + + selected = iterfzf.iterfzf( + formatted_items, + prompt="Select a pod to clone:", + preview='''kubectl describe pod "$(echo {} | awk -F'[(|)]' '{gsub(/\x1b\[[0-9;]*m/, "", $1); print $1}' | xargs)" -n "$(echo {} | awk -F'[(|)]' '{gsub(/\x1b\[[0-9;]*m/, "", $2); print $2}' | xargs)"''', + ) if not selected: return None, None @@ -239,83 +244,6 @@ def get_env_vars_and_secret_volumes(api, namespace: str): return env_vars, volumes -def get_init_scripts(): - """Get the contents of init scripts to be run in the pod""" - init_dir = Path.home() / ".config" / "krayt" / "init.d" - if not init_dir.exists(): - logging.debug("No init.d directory found at %s", init_dir) - return "" - - scripts = sorted(init_dir.glob("*.sh")) - if not scripts: - logging.debug("No init scripts found in %s", init_dir) - return "" - - # Create a combined script that will run all init scripts - init_script = "#!/bin/bash\n\n" - init_script += "exec 2>&1 # Redirect stderr to stdout for proper logging\n" - init_script += "set -e # Exit on error\n\n" - init_script += "echo 'Running initialization scripts...' | tee /tmp/init.log\n\n" - init_script += "mkdir -p /tmp/init.d\n\n" # Create directory once at the start - - for script in scripts: - try: - with open(script, "r") as f: - script_content = f.read() - if not script_content.strip(): - logging.debug("Skipping empty script %s", script) - continue - - # Use a unique heredoc delimiter for each script to avoid nesting issues - delimiter = f"EOF_SCRIPT_{script.stem.upper()}" - - init_script += ( - f"echo '=== Running {script.name} ===' | tee -a /tmp/init.log\n" - ) - init_script += f"cat > /tmp/init.d/{script.name} << '{delimiter}'\n" - init_script += script_content - if not script_content.endswith("\n"): - init_script += "\n" - init_script += f"{delimiter}\n" - init_script += f"chmod +x /tmp/init.d/{script.name}\n" - init_script += f'cd /tmp/init.d && ./{script.name} 2>&1 | tee -a /tmp/init.log || {{ echo "Failed to run {script.name}"; exit 1; }}\n' - init_script += ( - f"echo '=== Finished {script.name} ===' | tee -a /tmp/init.log\n\n" - ) - except Exception as e: - logging.error(f"Failed to load init script {script}: {e}") - - init_script += "echo 'Initialization scripts complete.' | tee -a /tmp/init.log\n" - return init_script - - -def get_motd_script(mount_info, pvc_info): - """Generate the MOTD script with proper escaping""" - return f""" -# Create MOTD -cat << EOF > /etc/motd -==================================== -Krayt Dragon's Lair -A safe haven for volume inspection -==================================== - -"Inside every volume lies a pearl of wisdom waiting to be discovered." - -Mounted Volumes: -$(echo "{",".join(mount_info)}" | tr ',' '\\n' | sed 's/^/- /') - -Persistent Volume Claims: -$(echo "{",".join(pvc_info)}" | tr ',' '\\n' | sed 's/^/- /') - -Mounted Secrets: -$(for d in /mnt/secrets/*; do if [ -d "$d" ]; then echo "- $(basename $d)"; fi; done) - -Init Script Status: -$(if [ -f /tmp/init.log ]; then echo "View initialization log at /tmp/init.log"; fi) -EOF -""" - - def create_inspector_job( api, namespace: str, @@ -418,6 +346,7 @@ def create_inspector_job( "annotations": {"pvcs": ",".join(pvc_info) if pvc_info else "none"}, }, "spec": { + "ttlSecondsAfterFinished": 600, "template": { "metadata": {"labels": {"app": "krayt"}}, "spec": { @@ -459,24 +388,6 @@ PROTECTED_NAMESPACES = { } -def load_init_scripts(): - """Load and execute initialization scripts from ~/.config/krayt/scripts/""" - init_dir = Path.home() / ".config" / "krayt" / "scripts" - if not init_dir.exists(): - return - - # Sort scripts to ensure consistent execution order - scripts = sorted(init_dir.glob("*.py")) - - for script in scripts: - try: - with open(script, "r") as f: - exec(f.read(), globals()) - logging.debug(f"Loaded init script: {script}") - except Exception as e: - logging.error(f"Failed to load init script {script}: {e}") - - def setup_environment(): """Set up the environment with proxy settings and other configurations""" # Load environment variables for proxies @@ -657,8 +568,16 @@ def clean( def create( namespace: Optional[str] = typer.Option( None, + "--namespace", + "-n", help="Kubernetes namespace. If not specified, will search for pods across all namespaces.", ), + clone: Optional[str] = typer.Option( + None, + "--clone", + "-c", + help="Clone an existing pod", + ), image: str = typer.Option( "alpine:latest", "--image", @@ -670,6 +589,39 @@ def create( "--imagepullsecret", help="Name of the image pull secret to use for pulling private images", ), + additional_packages: Optional[List[str]] = typer.Option( + None, + "--additional-packages", + "-ap", + help="additional packages to install in the inspector pod", + ), + additional_package_bundles: Optional[List[str]] = typer.Option( + None, + "--additional-package-bundles", + "-ab", + help="additional packages to install in the inspector pod", + ), + pre_init_scripts: Optional[List[str]] = typer.Option( + None, + "--pre-init-scripts", + help="additional scripts to execute at the end of container initialization", + ), + post_init_scripts: Optional[List[str]] = typer.Option( + None, + "--post-init-scripts", + "--init-scripts", + help="additional scripts to execute at the start of container initialization", + ), + pre_init_hooks: Optional[List[str]] = typer.Option( + None, + "--pre-init-hooks", + help="additional hooks to execute at the end of container initialization", + ), + post_init_hooks: Optional[List[str]] = typer.Option( + None, + "--post-init-hooks", + help="additional hooks to execute at the start of container initialization", + ), ): """ Krack open a Krayt dragon! Create an inspector pod to explore what's inside your volumes. @@ -677,15 +629,36 @@ def create( The inspector will be created in the same namespace as the selected pod. """ # For create, we want to list all pods, not just Krayt pods + selected_namespace = None + selected_pod = None + typer.echo(namespace) + typer.echo(clone) + + if namespace is None and clone is not None and "/" in clone: + selected_namespace, selected_pod = clone.split("/", 1) + elif namespace is not None and clone is not None: + selected_namespace = namespace + selected_pod = clone + pods = get_pods(namespace, label_selector=None) if not pods: typer.echo("No pods found.") raise typer.Exit(1) - selected_pod, selected_namespace = fuzzy_select(pods) - if not selected_pod: - typer.echo("No pod selected.") - raise typer.Exit(1) + if selected_pod not in (p[0] for p in pods) or selected_pod is None: + if selected_pod is not None: + pods = [p for p in pods if selected_pod in p[0]] + if len(pods) == 1: + selected_pod, selected_namespace = pods[0] + else: + selected_pod, selected_namespace = fuzzy_select(pods) + if not selected_pod: + typer.echo("No pod selected.") + raise typer.Exit(1) + + typer.echo(f"Selected pod exists: {selected_pod in (p[0] for p in pods)}") + typer.echo(f"Selected pod: {selected_pod} ({selected_namespace})") + raise typer.Exit(1) pod_spec = get_pod_spec(selected_pod, selected_namespace) volume_mounts, volumes = get_pod_volumes_and_mounts(pod_spec) @@ -758,9 +731,19 @@ def logs( raise typer.Exit(1) +@app.command("list") +def list_pods(): + pods = get_pods() + if not pods: + typer.echo("No pods found.") + raise typer.Exit(1) + + for pod, namespace in pods: + typer.echo(f"{pod} ({namespace})") + + def main(): setup_environment() - load_init_scripts() app() diff --git a/krayt/cli/templates.py b/krayt/cli/templates.py index fe866c1..04a1843 100644 --- a/krayt/cli/templates.py +++ b/krayt/cli/templates.py @@ -2,7 +2,25 @@ from krayt.templates import env import typer from typing import List, Optional -app = typer.Typer() +# app = typer.Typer() +app = typer.Typer( + context_settings={ + "auto_envvar_prefix": "KRAYT", + "help_option_names": ["-h", "--help"], + "show_default": True, + "allow_interspersed_args": True, + "ignore_unknown_options": False, + "max_content_width": None, + "suggest_command": True, + } +) + + +@app.command() +def list(): + typer.echo("Available templates:") + for template in env.list_templates(): + typer.echo(template) @app.command() diff --git a/krayt/package.py b/krayt/package.py index d88d494..26f2a86 100644 --- a/krayt/package.py +++ b/krayt/package.py @@ -40,6 +40,7 @@ class Package(BaseModel): "uv", "i", "curlsh", + "curlbash", "brew", "cargo", "pipx", @@ -49,8 +50,10 @@ class Package(BaseModel): ], BeforeValidator(validate_kind), ] = "system" - dependencies: Optional[List[str]] = None value: str + dependencies: Optional[List["Package"]] = None + pre_install_hook: Optional[str] = None + post_install_hook: Optional[str] = None @classmethod def from_raw(cls, raw: str) -> "Package": @@ -67,52 +70,37 @@ class Package(BaseModel): 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 + dependencies = [] - def __str__(self): - return f"{self.kind}:{self.value}" if self.kind != "system" else self.value + if self.kind in ["uv", "i", "installer", "curlbash", "curlsh", "gh"]: + dependencies.append(Package.from_raw("curl")) + if self.kind == "brew": + dependencies.append(Package.from_raw("git")) + dependencies.append(Package.from_raw("curl")) + self.pre_install_hook = "NONINTERACTIVE=1" + self.post_install_hook = """ +# Setup Homebrew PATH +if [ -f /home/linuxbrew/.linuxbrew/bin/brew ]; then + eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv)" +elif [ -f /opt/homebrew/bin/brew ]; then + eval "$(/opt/homebrew/.linuxbrew/bin/brew shellenv)" +elif [ -f /usr/local/bin/brew ]; then + eval "$(/usr/local/bin/brew shellenv)" +else + echo "⚠️ Brew installed but binary location unknown." +fi +""" + if self.kind == "cargo": + dependencies.append(Package.from_raw("cargo")) + if self.kind == "pipx": + dependencies.append(Package.from_raw("pipx")) + if self.kind == "npm": + dependencies.append(Package.from_raw("npm")) + if self.kind == "go": + dependencies.append(Package.from_raw("go")) + + self.dependencies = dependencies + return self def is_system(self) -> bool: return self.kind == "system" @@ -121,29 +109,36 @@ class Package(BaseModel): """ Generate the bash install command snippet for this package. """ + cmd = "" if self.kind == "system": - return f"detect_package_manager_and_install {self.value}" + cmd = f"detect_package_manager_and_install {self.value}" elif self.kind == "uv": - return f"uv tool install {self.value}" + cmd = f"uv tool install {self.value}" elif self.kind in ["i", "installer", "gh"]: - return f"curl -fsSL https://i.jpillora.com/{self.value} | sh" + cmd = f"curl -fsSL https://i.jpillora.com/{self.value} | sh" elif self.kind == "curlsh": - return f"curl -fsSL {self.value} | sh" + cmd = f"curl -fsSL {self.value} | sh" elif self.kind == "curlbash": - return f"curl -fsSL {self.value} | bash" + cmd = f"curl -fsSL {self.value} | bash" elif self.kind == "brew": - return f"brew install {self.value}" + cmd = "curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh | bash" elif self.kind == "cargo": - return f"cargo install {self.value}" + cmd = f"cargo install {self.value}" elif self.kind == "pipx": - return f"pipx install {self.value}" + cmd = f"pipx install {self.value}" elif self.kind == "npm": - return f"npm install -g {self.value}" + cmd = f"npm install -g {self.value}" elif self.kind == "go": - return f"go install {self.value}@latest" + cmd = f"go install {self.value}@latest" else: raise ValueError(f"Unknown install method for kind={self.kind}") + # Add pre-install hook if necessary + if self.pre_install_hook: + return f"{self.pre_install_hook} {cmd}" + else: + return cmd + if __name__ == "__main__": raw_inputs = [ @@ -163,4 +158,11 @@ if __name__ == "__main__": [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]))) + post_hooks = [] + for package in packages: + if package.post_install_hook: + post_hooks.append(package.post_install_hook.strip()) + + # Final full script + full_script = list(unique_everseen([*dependencies, *installs, *post_hooks])) + print("\n".join(full_script)) diff --git a/pyproject.toml b/pyproject.toml index cfeb2a1..ce11345 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,7 +28,8 @@ classifiers = [ dependencies = [ "typer", "kubernetes", - "InquirerPy", + "inquirerPy", + "inquirer", "jinja2", "iterfzf", "pydantic",