This commit is contained in:
Waylon S. Walker 2025-04-10 09:21:51 -05:00
parent 9737746923
commit 378744632f
7 changed files with 208 additions and 173 deletions

View file

@ -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",

View file

@ -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()

25
krayt/cli/bundles.py Normal file
View file

@ -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}")

View file

@ -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()

View file

@ -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()

View file

@ -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))

View file

@ -28,7 +28,8 @@ classifiers = [
dependencies = [
"typer",
"kubernetes",
"InquirerPy",
"inquirerPy",
"inquirer",
"jinja2",
"iterfzf",
"pydantic",