337 lines
10 KiB
Python
Executable file
337 lines
10 KiB
Python
Executable file
#!/usr/bin/env -S uv run --quiet --script
|
|
# /// script
|
|
# requires-python = ">=3.12"
|
|
# dependencies = [
|
|
# "typer",
|
|
# "kubernetes",
|
|
# "InquirerPy",
|
|
# ]
|
|
# ///
|
|
|
|
from InquirerPy import inquirer
|
|
from kubernetes import client, config
|
|
import os
|
|
import typer
|
|
from typing import List, Optional
|
|
|
|
app = typer.Typer(name="krayt")
|
|
|
|
VERSION = "0.1.0"
|
|
|
|
# Default values
|
|
container_image_default = "ubuntu:22.04"
|
|
container_name_default = "krayt-container"
|
|
|
|
KNOWN_PACKAGE_MANAGERS = {
|
|
"apk": "apk add",
|
|
"dnf": "dnf install -y",
|
|
"yum": "yum install -y",
|
|
"apt-get": "apt-get update && apt-get install -y",
|
|
"apt": "apt update && apt install -y",
|
|
"zypper": "zypper install -y",
|
|
"pacman": "pacman -Sy --noconfirm",
|
|
}
|
|
|
|
|
|
def load_kube_config():
|
|
try:
|
|
config.load_kube_config()
|
|
except Exception as e:
|
|
typer.secho(f"Failed to load kubeconfig: {e}", fg=typer.colors.RED)
|
|
raise typer.Exit(1)
|
|
|
|
|
|
def detect_package_manager_command() -> str:
|
|
checks = [
|
|
f"which {pm} >/dev/null 2>&1 && echo {cmd}"
|
|
for pm, cmd in KNOWN_PACKAGE_MANAGERS.items()
|
|
]
|
|
return " || ".join(checks)
|
|
|
|
|
|
def get_proxy_env_vars() -> List[client.V1EnvVar]:
|
|
proxy_vars = [
|
|
"HTTP_PROXY",
|
|
"http_proxy",
|
|
"HTTPS_PROXY",
|
|
"https_proxy",
|
|
"NO_PROXY",
|
|
"no_proxy",
|
|
]
|
|
env_vars = []
|
|
for var in proxy_vars:
|
|
value = os.environ.get(var)
|
|
if value:
|
|
env_vars.append(client.V1EnvVar(name=var, value=value))
|
|
return env_vars
|
|
|
|
|
|
def fuzzy_pick_pod(namespace: Optional[str] = None) -> str:
|
|
load_kube_config()
|
|
core_v1 = client.CoreV1Api()
|
|
if namespace is None:
|
|
pods = core_v1.list_pod_for_all_namespaces()
|
|
else:
|
|
pods = core_v1.list_namespaced_pod(namespace=namespace)
|
|
pods = {pod.metadata.name: pod for pod in pods.items}
|
|
if not pods:
|
|
typer.secho("No pods found to clone.", fg=typer.colors.RED)
|
|
raise typer.Exit(1)
|
|
choice = inquirer.fuzzy(
|
|
message="Select a pod to clone:", choices=pods.keys()
|
|
).execute()
|
|
return pods[choice]
|
|
|
|
|
|
def clone_pod(core_v1, namespace: str, source_pod_name: str):
|
|
source_pod = core_v1.read_namespaced_pod(name=source_pod_name, namespace=namespace)
|
|
container = source_pod.spec.containers[0]
|
|
breakpoint()
|
|
return (
|
|
container.image,
|
|
container.volume_mounts,
|
|
source_pod.spec.volumes,
|
|
container.env,
|
|
source_pod.spec.image_pull_secrets,
|
|
)
|
|
|
|
|
|
@app.command()
|
|
def create(
|
|
image: str = typer.Option(
|
|
container_image_default, "--image", "-i", help="Image to use for the container"
|
|
),
|
|
name: str = typer.Option(
|
|
container_name_default, "--name", "-n", help="Name for the krayt container"
|
|
),
|
|
yes: bool = typer.Option(
|
|
False, "--yes", "-Y", help="Non-interactive, pull images without asking"
|
|
),
|
|
fuzzy_clone: bool = typer.Option(
|
|
False,
|
|
"--fuzzy-clone",
|
|
"-f",
|
|
help="Clone an existing pod",
|
|
),
|
|
clone: Optional[str] = typer.Option(
|
|
None, "--clone", "-c", help="Clone an existing krayt container"
|
|
),
|
|
volume: List[str] = typer.Option(
|
|
[],
|
|
"--volume",
|
|
help="Additional volumes to add to the container (pvc-name:/mount/path)",
|
|
),
|
|
additional_flags: List[str] = typer.Option(
|
|
[],
|
|
"--additional-flags",
|
|
"-a",
|
|
help="Additional flags to pass to the container manager command",
|
|
),
|
|
additional_packages: List[str] = typer.Option(
|
|
[],
|
|
"--additional-packages",
|
|
"-ap",
|
|
help="Additional packages to install during setup",
|
|
),
|
|
init_hooks: List[str] = typer.Option(
|
|
[], "--init-hooks", help="Commands to execute at the end of initialization"
|
|
),
|
|
pre_init_hooks: List[str] = typer.Option(
|
|
[],
|
|
"--pre-init-hooks",
|
|
help="Commands to execute at the start of initialization",
|
|
),
|
|
namespace: str = typer.Option(None, help="Kubernetes namespace"),
|
|
dry_run: bool = typer.Option(
|
|
False, "--dry-run", "-d", help="Only print the generated Kubernetes manifest"
|
|
),
|
|
verbose: bool = typer.Option(False, "--verbose", "-v", help="Show more verbosity"),
|
|
image_pull_secret: Optional[str] = typer.Option(
|
|
None,
|
|
"--image-pull-secret",
|
|
help="Name of the Kubernetes secret for pulling the image",
|
|
),
|
|
):
|
|
"""Create a new Kubernetes pod inspired by distrobox."""
|
|
load_kube_config()
|
|
core_v1 = client.CoreV1Api()
|
|
|
|
if fuzzy_clone:
|
|
namespace, clone = fuzzy_pick_pod(namespace)
|
|
|
|
if clone is not None:
|
|
image, volume_mounts, volumes, env_vars, image_pull_secrets = clone_pod(
|
|
core_v1, namespace, clone
|
|
)
|
|
else:
|
|
volume_mounts = []
|
|
volumes = []
|
|
env_vars = get_proxy_env_vars()
|
|
for idx, pvc_entry in enumerate(volume):
|
|
try:
|
|
pvc_name, mount_path = pvc_entry.split(":", 1)
|
|
except ValueError:
|
|
typer.secho(
|
|
f"Invalid volume format: {pvc_entry}. Use pvc-name:/mount/path",
|
|
fg=typer.colors.RED,
|
|
)
|
|
raise typer.Exit(1)
|
|
|
|
volumes.append(
|
|
client.V1Volume(
|
|
name=f"volume-{idx}",
|
|
persistent_volume_claim=client.V1PersistentVolumeClaimVolumeSource(
|
|
claim_name=pvc_name
|
|
),
|
|
)
|
|
)
|
|
volume_mounts.append(
|
|
client.V1VolumeMount(
|
|
name=f"volume-{idx}",
|
|
mount_path=mount_path,
|
|
)
|
|
)
|
|
|
|
package_manager_detection = detect_package_manager_command()
|
|
package_manager_detection = """
|
|
detect_package_manager_and_install_command() {
|
|
if [ $# -eq 0 ]; then
|
|
echo "Usage: detect_package_manager_and_install_command <package1> [package2] [...]"
|
|
return 1
|
|
fi
|
|
|
|
if command -v apt >/dev/null 2>&1; then
|
|
PKG_MANAGER="apt"
|
|
UPDATE_CMD="apt update &&"
|
|
INSTALL_CMD="apt install -y"
|
|
elif command -v dnf >/dev/null 2>&1; then
|
|
PKG_MANAGER="dnf"
|
|
UPDATE_CMD=""
|
|
INSTALL_CMD="dnf install -y"
|
|
elif command -v yum >/dev/null 2>&1; then
|
|
PKG_MANAGER="yum"
|
|
UPDATE_CMD=""
|
|
INSTALL_CMD="yum install -y"
|
|
elif command -v pacman >/dev/null 2>&1; then
|
|
PKG_MANAGER="pacman"
|
|
UPDATE_CMD=""
|
|
INSTALL_CMD="pacman -Sy --noconfirm"
|
|
elif command -v zypper >/dev/null 2>&1; then
|
|
PKG_MANAGER="zypper"
|
|
UPDATE_CMD=""
|
|
INSTALL_CMD="zypper install -y"
|
|
elif command -v apk >/dev/null 2>&1; then
|
|
PKG_MANAGER="apk"
|
|
UPDATE_CMD=""
|
|
INSTALL_CMD="apk add"
|
|
else
|
|
echo "No supported package manager found."
|
|
return 2
|
|
fi
|
|
|
|
PACKAGES="$*"
|
|
|
|
if [ -n "$UPDATE_CMD" ]; then
|
|
echo "$UPDATE_CMD
|
|
echo $INSTALL_CMD $PACKAGES"
|
|
$UPDATE_CMD
|
|
$INSTALL_CMD $PACKAGES
|
|
|
|
else
|
|
echo "$INSTALL_CMD $PACKAGES"
|
|
$INSTALL_CMD $PACKAGES
|
|
fi
|
|
}
|
|
"""
|
|
|
|
pre_hooks_command = " && ".join(pre_init_hooks) if pre_init_hooks else ""
|
|
install_packages_command = ""
|
|
if additional_packages:
|
|
install_packages_command = f"{package_manager_detection}\n detect_package_manager_and_install_command {' '.join(additional_packages)}"
|
|
# install_packages_command = (
|
|
# f"$({{package_manager_detection}} {' '.join(additional_packages)})"
|
|
# )
|
|
post_hooks_command = " && ".join(init_hooks) if init_hooks else ""
|
|
|
|
combined_command_parts = [
|
|
cmd
|
|
for cmd in [pre_hooks_command, install_packages_command, post_hooks_command]
|
|
if cmd
|
|
]
|
|
command = None
|
|
|
|
if combined_command_parts:
|
|
combined_command = " && ".join(combined_command_parts)
|
|
command = ["/bin/sh", "-c", f"{combined_command} && tail -f /dev/null"]
|
|
|
|
pod_spec = client.V1PodSpec(
|
|
containers=[
|
|
client.V1Container(
|
|
name=name,
|
|
image=image,
|
|
command=command,
|
|
volume_mounts=volume_mounts if volume_mounts else None,
|
|
env=env_vars if env_vars else None,
|
|
)
|
|
],
|
|
volumes=volumes if volumes else None,
|
|
restart_policy="Never",
|
|
)
|
|
|
|
if image_pull_secret:
|
|
pod_spec.image_pull_secrets = [
|
|
client.V1LocalObjectReference(name=image_pull_secret)
|
|
]
|
|
elif clone and image_pull_secrets:
|
|
pod_spec.image_pull_secrets = image_pull_secrets
|
|
|
|
pod = client.V1Pod(
|
|
metadata=client.V1ObjectMeta(name=name, namespace=namespace), spec=pod_spec
|
|
)
|
|
|
|
if dry_run or verbose:
|
|
typer.secho(f"Dry-run/Verbose: Pod definition:\n{pod}", fg=typer.colors.BLUE)
|
|
|
|
if dry_run:
|
|
typer.secho("Dry run completed.", fg=typer.colors.GREEN)
|
|
raise typer.Exit()
|
|
|
|
typer.secho(
|
|
f"Creating pod '{name}' in namespace '{namespace}'...", fg=typer.colors.GREEN
|
|
)
|
|
core_v1.create_namespaced_pod(namespace=namespace, body=pod)
|
|
typer.secho("Pod created successfully.", fg=typer.colors.GREEN)
|
|
|
|
|
|
@app.command("fuzzy-pick-pod")
|
|
def cli_fuzzy_pick_pod(
|
|
namespace: str = typer.Option(None, help="Kubernetes namespace"),
|
|
):
|
|
load_kube_config()
|
|
pod = fuzzy_pick_pod(namespace)
|
|
|
|
if not pod:
|
|
typer.secho("No pod selected.", fg=typer.colors.RED)
|
|
raise typer.Exit(1)
|
|
|
|
typer.secho("Selected pod", fg=typer.colors.GREEN)
|
|
typer.secho(f"Name: {pod.metadata.name}", fg=typer.colors.GREEN)
|
|
typer.secho(f"Namespace: {pod.metadata.namespace}", fg=typer.colors.GREEN)
|
|
typer.secho(f"Image: {pod.spec.containers[0].image}", fg=typer.colors.GREEN)
|
|
typer.secho(f"Command: {pod.spec.containers[0].command}", fg=typer.colors.GREEN)
|
|
typer.secho(f"Volume mounts: {pod.spec.volumes}", fg=typer.colors.GREEN)
|
|
typer.secho(
|
|
f"Environment variables: {pod.spec.containers[0].env}", fg=typer.colors.GREEN
|
|
)
|
|
|
|
return pod
|
|
|
|
|
|
@app.command()
|
|
def version(show: bool = typer.Option(False, "--version", "-V", help="Show version")):
|
|
if show:
|
|
typer.echo(f"krayt version {VERSION}")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
app()
|