wip
This commit is contained in:
parent
90491d17bf
commit
a60562b7fc
18 changed files with 1683 additions and 1 deletions
1
krayt/__about__.py
Normal file
1
krayt/__about__.py
Normal file
|
|
@ -0,0 +1 @@
|
|||
__version__ = "0.0.0"
|
||||
5
krayt/__init__.py
Normal file
5
krayt/__init__.py
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
from krayt.__about__ import __version__
|
||||
|
||||
__all__ = [
|
||||
"__version__",
|
||||
]
|
||||
18
krayt/cli/__init__.py
Normal file
18
krayt/cli/__init__.py
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
from krayt import __version__
|
||||
from krayt.cli.create import app as create_app
|
||||
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.command()
|
||||
def version():
|
||||
print(__version__)
|
||||
|
||||
|
||||
def main():
|
||||
app()
|
||||
763
krayt/cli/create.py
Normal file
763
krayt/cli/create.py
Normal file
|
|
@ -0,0 +1,763 @@
|
|||
from 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, Optional
|
||||
import yaml
|
||||
|
||||
KRAYT_VERSION = "NIGHTLY"
|
||||
|
||||
logging.basicConfig(level=logging.WARNING)
|
||||
|
||||
app = typer.Typer()
|
||||
|
||||
|
||||
def clean_dict(d: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Remove None values and empty dicts from a dictionary recursively."""
|
||||
if not isinstance(d, dict):
|
||||
return d
|
||||
return {
|
||||
k: clean_dict(v)
|
||||
for k, v in d.items()
|
||||
if v is not None and v != {} and not (isinstance(v, dict) and not clean_dict(v))
|
||||
}
|
||||
|
||||
|
||||
def format_volume_mount(vm: client.V1VolumeMount) -> dict[str, Any]:
|
||||
"""Format volume mount with only relevant fields."""
|
||||
# Skip Kubernetes service account mounts
|
||||
if vm.mount_path.startswith("/var/run/secrets/kubernetes.io/"):
|
||||
return None
|
||||
|
||||
return clean_dict(
|
||||
{
|
||||
"name": vm.name,
|
||||
"mountPath": vm.mount_path,
|
||||
"readOnly": vm.read_only if vm.read_only else None,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def format_volume(v: client.V1Volume) -> dict[str, Any]:
|
||||
"""Format volume into a dictionary, return None if it should be skipped"""
|
||||
# Skip Kubernetes service account volumes
|
||||
if v.name.startswith("kube-api-access-"):
|
||||
return None
|
||||
|
||||
volume_source = None
|
||||
if v.persistent_volume_claim:
|
||||
volume_source = {
|
||||
"persistentVolumeClaim": {"claimName": v.persistent_volume_claim.claim_name}
|
||||
}
|
||||
elif v.config_map:
|
||||
volume_source = {"configMap": {"name": v.config_map.name}}
|
||||
elif v.secret:
|
||||
volume_source = {"secret": {"secretName": v.secret.secret_name}}
|
||||
elif v.host_path: # Add support for hostPath volumes (used for device mounts)
|
||||
volume_source = {
|
||||
"hostPath": {
|
||||
"path": v.host_path.path,
|
||||
"type": v.host_path.type if v.host_path.type else None,
|
||||
}
|
||||
}
|
||||
elif v.empty_dir: # Add support for emptyDir volumes (used for /dev/shm)
|
||||
volume_source = {
|
||||
"emptyDir": {
|
||||
"medium": v.empty_dir.medium if v.empty_dir.medium else None,
|
||||
"sizeLimit": v.empty_dir.size_limit if v.empty_dir.size_limit else None,
|
||||
}
|
||||
}
|
||||
|
||||
if not volume_source:
|
||||
return None
|
||||
|
||||
return clean_dict({"name": v.name, **volume_source})
|
||||
|
||||
|
||||
def fuzzy_select(items):
|
||||
"""Use fzf to select from a list of (name, namespace) tuples"""
|
||||
if not items:
|
||||
return None, None
|
||||
|
||||
# If there's only one item, return it without prompting
|
||||
if len(items) == 1:
|
||||
return items[0]
|
||||
|
||||
# Format items for display
|
||||
formatted_items = [f"{name} ({namespace})" for name, namespace in items]
|
||||
|
||||
# Use fzf for selection
|
||||
try:
|
||||
selected = iterfzf(formatted_items)
|
||||
if not selected:
|
||||
return None, None
|
||||
|
||||
# Parse selection back into name and namespace
|
||||
# Example: "pod-name (namespace)" -> ("pod-name", "namespace")
|
||||
name = selected.split(" (")[0]
|
||||
namespace = selected.split(" (")[1][:-1]
|
||||
return name, namespace
|
||||
|
||||
except Exception as e:
|
||||
typer.echo(f"Error during selection: {e}")
|
||||
return None, None
|
||||
|
||||
|
||||
def get_pods(
|
||||
namespace=None,
|
||||
label_selector: str = "app=krayt",
|
||||
):
|
||||
"""Get list of pods in the specified namespace or all namespaces"""
|
||||
try:
|
||||
config.load_kube_config()
|
||||
api = client.CoreV1Api()
|
||||
if namespace:
|
||||
pods = api.list_namespaced_pod(
|
||||
namespace=namespace,
|
||||
label_selector=label_selector,
|
||||
)
|
||||
else:
|
||||
pods = api.list_pod_for_all_namespaces(
|
||||
label_selector=label_selector,
|
||||
)
|
||||
|
||||
# Convert to list of (name, namespace) tuples
|
||||
pod_list = []
|
||||
for pod in pods.items:
|
||||
if pod.metadata.namespace not in PROTECTED_NAMESPACES:
|
||||
pod_list.append((pod.metadata.name, pod.metadata.namespace))
|
||||
return pod_list
|
||||
|
||||
except client.rest.ApiException as e:
|
||||
typer.echo(f"Error listing pods: {e}")
|
||||
raise typer.Exit(1)
|
||||
|
||||
|
||||
def get_pod_spec(pod_name, namespace):
|
||||
config.load_kube_config()
|
||||
v1 = client.CoreV1Api()
|
||||
return v1.read_namespaced_pod(pod_name, namespace)
|
||||
|
||||
|
||||
def get_pod_volumes_and_mounts(pod_spec):
|
||||
"""Extract all volumes and mounts from a pod spec"""
|
||||
volume_mounts = []
|
||||
for container in pod_spec.spec.containers:
|
||||
if container.volume_mounts:
|
||||
volume_mounts.extend(container.volume_mounts)
|
||||
|
||||
# Filter out None values from volume mounts
|
||||
volume_mounts = [vm for vm in volume_mounts if format_volume_mount(vm)]
|
||||
|
||||
# Get all volumes, including device mounts
|
||||
volumes = []
|
||||
if pod_spec.spec.volumes:
|
||||
for v in pod_spec.spec.volumes:
|
||||
# Handle device mounts
|
||||
if v.name in ["cache-volume"]:
|
||||
volumes.append(
|
||||
client.V1Volume(
|
||||
name=v.name,
|
||||
empty_dir=client.V1EmptyDirVolumeSource(medium="Memory"),
|
||||
)
|
||||
)
|
||||
elif v.name in ["coral-device"]:
|
||||
volumes.append(
|
||||
client.V1Volume(
|
||||
name=v.name,
|
||||
host_path=client.V1HostPathVolumeSource(
|
||||
path="/dev/apex_0", type="CharDevice"
|
||||
),
|
||||
)
|
||||
)
|
||||
elif v.name in ["qsv-device"]:
|
||||
volumes.append(
|
||||
client.V1Volume(
|
||||
name=v.name,
|
||||
host_path=client.V1HostPathVolumeSource(
|
||||
path="/dev/dri", type="Directory"
|
||||
),
|
||||
)
|
||||
)
|
||||
else:
|
||||
volumes.append(v)
|
||||
|
||||
# Filter out None values from volumes
|
||||
volumes = [v for v in volumes if format_volume(v)]
|
||||
|
||||
return volume_mounts, volumes
|
||||
|
||||
|
||||
def get_env_vars_and_secret_volumes(api, namespace: str):
|
||||
"""Get environment variables and secret volumes for the inspector pod"""
|
||||
env_vars = []
|
||||
volumes = []
|
||||
|
||||
# Add proxy environment variables if they exist in the host environment
|
||||
proxy_vars = [
|
||||
"HTTP_PROXY",
|
||||
"HTTPS_PROXY",
|
||||
"NO_PROXY",
|
||||
"http_proxy",
|
||||
"https_proxy",
|
||||
"no_proxy",
|
||||
]
|
||||
|
||||
for var in proxy_vars:
|
||||
if var in os.environ:
|
||||
env_vars.append({"name": var, "value": os.environ[var]})
|
||||
|
||||
# Look for secret volumes in the namespace
|
||||
try:
|
||||
secrets = api.list_namespaced_secret(namespace)
|
||||
for secret in secrets.items:
|
||||
# Skip service account tokens and other system secrets
|
||||
if secret.type != "Opaque" or secret.metadata.name.startswith(
|
||||
"default-token-"
|
||||
):
|
||||
continue
|
||||
|
||||
# Mount each secret as a volume
|
||||
volume_name = f"secret-{secret.metadata.name}"
|
||||
volumes.append(
|
||||
client.V1Volume(
|
||||
name=volume_name,
|
||||
secret=client.V1SecretVolumeSource(
|
||||
secret_name=secret.metadata.name
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
except client.exceptions.ApiException as e:
|
||||
if e.status != 404: # Ignore if no secrets found
|
||||
logging.warning(f"Failed to list secrets in namespace {namespace}: {e}")
|
||||
|
||||
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,
|
||||
pod_name: str,
|
||||
volume_mounts: list,
|
||||
volumes: list,
|
||||
image: str = "alpine:latest",
|
||||
imagepullsecret: Optional[str] = None,
|
||||
):
|
||||
"""Create a Krayt inspector job with the given mounts"""
|
||||
timestamp = int(time.time())
|
||||
job_name = f"{pod_name}-krayt-{timestamp}"
|
||||
|
||||
# Get environment variables and secret volumes from the target pod
|
||||
env_vars, secret_volumes = get_env_vars_and_secret_volumes(api, namespace)
|
||||
|
||||
# Add secret volumes to our volumes list
|
||||
volumes.extend(secret_volumes)
|
||||
|
||||
# Create corresponding volume mounts for secrets
|
||||
secret_mounts = []
|
||||
for vol in secret_volumes:
|
||||
secret_mounts.append(
|
||||
{
|
||||
"name": vol.name,
|
||||
"mountPath": f"/mnt/secrets/{vol.secret.secret_name}",
|
||||
"readOnly": True,
|
||||
}
|
||||
)
|
||||
|
||||
# Convert volume mounts to dictionaries
|
||||
formatted_mounts = [format_volume_mount(vm) for vm in volume_mounts]
|
||||
formatted_mounts.extend(secret_mounts)
|
||||
|
||||
# Format mount and PVC info for MOTD
|
||||
mount_info = []
|
||||
for vm in formatted_mounts:
|
||||
if vm:
|
||||
mount_info.append(f"{vm['name']}:{vm['mountPath']}")
|
||||
|
||||
pvc_info = []
|
||||
for v in volumes:
|
||||
if hasattr(v, "persistent_volume_claim") and v.persistent_volume_claim:
|
||||
pvc_info.append(f"{v.name}:{v.persistent_volume_claim.claim_name}")
|
||||
|
||||
template_name = "base.sh"
|
||||
template = env.get_template(template_name)
|
||||
additional_packages = [
|
||||
"ripgrep",
|
||||
"exa",
|
||||
"ncdu",
|
||||
"dust",
|
||||
"file",
|
||||
"hexyl",
|
||||
"jq",
|
||||
"yq",
|
||||
"bat",
|
||||
"fd",
|
||||
"fzf",
|
||||
"htop",
|
||||
"bottom",
|
||||
"difftastic",
|
||||
"mtr",
|
||||
"bind-tools",
|
||||
"aws-cli",
|
||||
"sqlite",
|
||||
"sqlite-dev",
|
||||
"sqlite-libs",
|
||||
"bash",
|
||||
"neovim",
|
||||
"starship",
|
||||
]
|
||||
pvcs = None
|
||||
pre_init_scripts = None
|
||||
post_init_scripts = None
|
||||
pre_init_hooks = None
|
||||
post_init_hooks = None
|
||||
command = template.render(
|
||||
volumes=volumes,
|
||||
pvcs=pvcs,
|
||||
additional_packages=additional_packages,
|
||||
pre_init_scripts=pre_init_scripts,
|
||||
post_init_scripts=post_init_scripts,
|
||||
pre_init_hooks=pre_init_hooks,
|
||||
post_init_hooks=post_init_hooks,
|
||||
)
|
||||
|
||||
inspector_job = {
|
||||
"apiVersion": "batch/v1",
|
||||
"kind": "Job",
|
||||
"metadata": {
|
||||
"name": job_name,
|
||||
"namespace": namespace,
|
||||
"labels": {"app": "krayt"},
|
||||
"annotations": {"pvcs": ",".join(pvc_info) if pvc_info else "none"},
|
||||
},
|
||||
"spec": {
|
||||
"template": {
|
||||
"metadata": {"labels": {"app": "krayt"}},
|
||||
"spec": {
|
||||
"containers": [
|
||||
{
|
||||
"name": "inspector",
|
||||
"image": image,
|
||||
"command": ["sh", "-c", command],
|
||||
"env": env_vars,
|
||||
"volumeMounts": formatted_mounts,
|
||||
}
|
||||
],
|
||||
"volumes": [format_volume(v) for v in volumes if format_volume(v)],
|
||||
"imagePullSecrets": [{"name": imagepullsecret}]
|
||||
if imagepullsecret
|
||||
else None,
|
||||
"restartPolicy": "Never",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
return inspector_job
|
||||
|
||||
|
||||
PROTECTED_NAMESPACES = {
|
||||
"kube-system",
|
||||
"kube-public",
|
||||
"kube-node-lease",
|
||||
"argo-events",
|
||||
"argo-rollouts",
|
||||
"argo-workflows",
|
||||
"argocd",
|
||||
"cert-manager",
|
||||
"ingress-nginx",
|
||||
"monitoring",
|
||||
"prometheus",
|
||||
"istio-system",
|
||||
"linkerd",
|
||||
}
|
||||
|
||||
|
||||
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
|
||||
proxy_vars = [
|
||||
"HTTP_PROXY",
|
||||
"HTTPS_PROXY",
|
||||
"NO_PROXY",
|
||||
"http_proxy",
|
||||
"https_proxy",
|
||||
"no_proxy",
|
||||
]
|
||||
|
||||
for var in proxy_vars:
|
||||
if var in os.environ:
|
||||
# Make both upper and lower case versions available
|
||||
os.environ[var.upper()] = os.environ[var]
|
||||
os.environ[var.lower()] = os.environ[var]
|
||||
|
||||
|
||||
def version_callback(value: bool):
|
||||
if value:
|
||||
typer.echo(f"Version: {KRAYT_VERSION}")
|
||||
raise typer.Exit()
|
||||
|
||||
|
||||
@app.command()
|
||||
def exec(
|
||||
namespace: Optional[str] = typer.Option(
|
||||
None,
|
||||
help="Kubernetes namespace. If not specified, will search for inspectors across all namespaces.",
|
||||
),
|
||||
):
|
||||
"""
|
||||
Enter the Krayt dragon's lair! Connect to a running inspector pod.
|
||||
If multiple inspectors are found, you'll get to choose which one to explore.
|
||||
"""
|
||||
config.load_kube_config()
|
||||
batch_api = client.BatchV1Api()
|
||||
|
||||
try:
|
||||
if namespace:
|
||||
logging.debug(f"Listing jobs in namespace {namespace}")
|
||||
jobs = batch_api.list_namespaced_job(
|
||||
namespace=namespace, label_selector="app=krayt"
|
||||
)
|
||||
else:
|
||||
logging.debug("Listing jobs in all namespaces")
|
||||
jobs = batch_api.list_job_for_all_namespaces(label_selector="app=krayt")
|
||||
|
||||
running_inspectors = []
|
||||
for job in jobs.items:
|
||||
# Get the pod for this job
|
||||
v1 = client.CoreV1Api()
|
||||
pods = v1.list_namespaced_pod(
|
||||
namespace=job.metadata.namespace,
|
||||
label_selector=f"job-name={job.metadata.name}",
|
||||
)
|
||||
for pod in pods.items:
|
||||
if pod.status.phase == "Running":
|
||||
running_inspectors.append(
|
||||
(pod.metadata.name, pod.metadata.namespace)
|
||||
)
|
||||
|
||||
if not running_inspectors:
|
||||
typer.echo("No running inspector pods found.")
|
||||
raise typer.Exit(1)
|
||||
|
||||
if len(running_inspectors) == 1:
|
||||
pod_name, pod_namespace = running_inspectors[0]
|
||||
else:
|
||||
pod_name, pod_namespace = fuzzy_select(running_inspectors)
|
||||
if not pod_name:
|
||||
typer.echo("No inspector selected.")
|
||||
raise typer.Exit(1)
|
||||
|
||||
exec_command = [
|
||||
"kubectl",
|
||||
"exec",
|
||||
"-it",
|
||||
"-n",
|
||||
pod_namespace,
|
||||
pod_name,
|
||||
"--",
|
||||
"/bin/bash",
|
||||
"-l",
|
||||
]
|
||||
|
||||
os.execvp("kubectl", exec_command)
|
||||
|
||||
except client.exceptions.ApiException as e:
|
||||
logging.error(f"Failed to list jobs: {e}")
|
||||
typer.echo(f"Failed to list jobs: {e}", err=True)
|
||||
raise typer.Exit(1)
|
||||
|
||||
|
||||
@app.command()
|
||||
def clean(
|
||||
namespace: Optional[str] = typer.Option(
|
||||
None,
|
||||
help="Kubernetes namespace. If not specified, will cleanup in all namespaces.",
|
||||
),
|
||||
yes: bool = typer.Option(
|
||||
False,
|
||||
"--yes",
|
||||
"-y",
|
||||
help="Skip confirmation prompt.",
|
||||
),
|
||||
):
|
||||
"""
|
||||
Clean up after your hunt! Remove all Krayt inspector jobs.
|
||||
Use --yes/-y to skip confirmation and clean up immediately.
|
||||
"""
|
||||
config.load_kube_config()
|
||||
batch_api = client.BatchV1Api()
|
||||
|
||||
try:
|
||||
if namespace:
|
||||
if namespace in PROTECTED_NAMESPACES:
|
||||
typer.echo(f"Error: Cannot cleanup in protected namespace {namespace}")
|
||||
raise typer.Exit(1)
|
||||
logging.debug(f"Listing jobs in namespace {namespace}")
|
||||
jobs = batch_api.list_namespaced_job(
|
||||
namespace=namespace, label_selector="app=krayt"
|
||||
)
|
||||
else:
|
||||
logging.debug("Listing jobs in all namespaces")
|
||||
jobs = batch_api.list_job_for_all_namespaces(label_selector="app=krayt")
|
||||
|
||||
# Filter out jobs in protected namespaces
|
||||
jobs.items = [
|
||||
job
|
||||
for job in jobs.items
|
||||
if job.metadata.namespace not in PROTECTED_NAMESPACES
|
||||
]
|
||||
|
||||
if not jobs.items:
|
||||
typer.echo("No Krayt inspector jobs found.")
|
||||
return
|
||||
|
||||
# Show confirmation prompt
|
||||
if not yes:
|
||||
job_list = "\n".join(
|
||||
f" {job.metadata.namespace}/{job.metadata.name}" for job in jobs.items
|
||||
)
|
||||
typer.echo(f"The following inspector jobs will be deleted:\n{job_list}")
|
||||
if not typer.confirm("Are you sure you want to continue?"):
|
||||
typer.echo("Operation cancelled.")
|
||||
return
|
||||
|
||||
# Delete each job
|
||||
for job in jobs.items:
|
||||
try:
|
||||
logging.debug(
|
||||
f"Deleting job {job.metadata.namespace}/{job.metadata.name}"
|
||||
)
|
||||
batch_api.delete_namespaced_job(
|
||||
name=job.metadata.name,
|
||||
namespace=job.metadata.namespace,
|
||||
body=client.V1DeleteOptions(propagation_policy="Background"),
|
||||
)
|
||||
typer.echo(f"Deleted job: {job.metadata.namespace}/{job.metadata.name}")
|
||||
except client.exceptions.ApiException as e:
|
||||
logging.error(
|
||||
f"Failed to delete job {job.metadata.namespace}/{job.metadata.name}: {e}"
|
||||
)
|
||||
typer.echo(
|
||||
f"Failed to delete job {job.metadata.namespace}/{job.metadata.name}: {e}",
|
||||
err=True,
|
||||
)
|
||||
|
||||
except client.exceptions.ApiException as e:
|
||||
logging.error(f"Failed to list jobs: {e}")
|
||||
typer.echo(f"Failed to list jobs: {e}", err=True)
|
||||
raise typer.Exit(1)
|
||||
|
||||
|
||||
@app.command()
|
||||
def create(
|
||||
namespace: Optional[str] = typer.Option(
|
||||
None,
|
||||
help="Kubernetes namespace. If not specified, will search for pods across all namespaces.",
|
||||
),
|
||||
image: str = typer.Option(
|
||||
"alpine:latest",
|
||||
"--image",
|
||||
"-i",
|
||||
help="Container image to use for the inspector pod",
|
||||
),
|
||||
imagepullsecret: Optional[str] = typer.Option(
|
||||
None,
|
||||
"--imagepullsecret",
|
||||
help="Name of the image pull secret to use for pulling private images",
|
||||
),
|
||||
):
|
||||
"""
|
||||
Krack open a Krayt dragon! Create an inspector pod to explore what's inside your volumes.
|
||||
If namespace is not specified, will search for pods across all namespaces.
|
||||
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
|
||||
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)
|
||||
|
||||
pod_spec = get_pod_spec(selected_pod, selected_namespace)
|
||||
volume_mounts, volumes = get_pod_volumes_and_mounts(pod_spec)
|
||||
|
||||
inspector_job = create_inspector_job(
|
||||
client.CoreV1Api(),
|
||||
selected_namespace,
|
||||
selected_pod,
|
||||
volume_mounts,
|
||||
volumes,
|
||||
image=image,
|
||||
imagepullsecret=imagepullsecret,
|
||||
)
|
||||
|
||||
# Output the job manifest
|
||||
typer.echo(yaml.dump(clean_dict(inspector_job), sort_keys=False))
|
||||
|
||||
|
||||
@app.command()
|
||||
def version():
|
||||
"""Show the version of Krayt."""
|
||||
typer.echo(f"Version: {KRAYT_VERSION}")
|
||||
|
||||
|
||||
@app.command()
|
||||
def logs(
|
||||
namespace: Optional[str] = typer.Option(
|
||||
None,
|
||||
help="Kubernetes namespace. If not specified, will search for inspectors across all namespaces.",
|
||||
),
|
||||
follow: bool = typer.Option(
|
||||
False,
|
||||
"--follow",
|
||||
"-f",
|
||||
help="Follow the logs in real-time",
|
||||
),
|
||||
):
|
||||
"""
|
||||
View logs from a Krayt inspector pod.
|
||||
If multiple inspectors are found, you'll get to choose which one to explore.
|
||||
"""
|
||||
pods = get_pods(namespace)
|
||||
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)
|
||||
|
||||
try:
|
||||
config.load_kube_config()
|
||||
api = client.CoreV1Api()
|
||||
logs = api.read_namespaced_pod_log(
|
||||
name=selected_pod,
|
||||
namespace=selected_namespace,
|
||||
follow=follow,
|
||||
_preload_content=False,
|
||||
)
|
||||
|
||||
if follow:
|
||||
for line in logs:
|
||||
typer.echo(line.decode("utf-8").strip())
|
||||
else:
|
||||
typer.echo(logs.read().decode("utf-8"))
|
||||
|
||||
except client.rest.ApiException as e:
|
||||
typer.echo(f"Error reading logs: {e}")
|
||||
raise typer.Exit(1)
|
||||
|
||||
|
||||
def main():
|
||||
setup_environment()
|
||||
load_init_scripts()
|
||||
app()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
90
krayt/cli/templates.py
Normal file
90
krayt/cli/templates.py
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
from krayt.templates import env
|
||||
import typer
|
||||
from typing import List, Optional
|
||||
|
||||
app = typer.Typer()
|
||||
|
||||
|
||||
@app.command()
|
||||
def base(
|
||||
volumes: Optional[List[str]] = typer.Option(
|
||||
None,
|
||||
"--volume",
|
||||
),
|
||||
pvcs: Optional[List[str]] = typer.Option(
|
||||
None,
|
||||
"--pvc",
|
||||
),
|
||||
additional_packages: Optional[List[str]] = typer.Option(
|
||||
None, "--additional-packages", "-ap"
|
||||
),
|
||||
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",
|
||||
"--init-hooks",
|
||||
help="additional hooks to execute at the start of container initialization",
|
||||
),
|
||||
):
|
||||
template_name = "base.sh"
|
||||
template = env.get_template(template_name)
|
||||
rendered = template.render(
|
||||
volumes=volumes,
|
||||
pvcs=pvcs,
|
||||
additional_packages=additional_packages,
|
||||
pre_init_scripts=pre_init_scripts,
|
||||
post_init_scripts=post_init_scripts,
|
||||
pre_init_hooks=pre_init_hooks,
|
||||
post_init_hooks=post_init_hooks,
|
||||
)
|
||||
print(rendered)
|
||||
|
||||
|
||||
@app.command()
|
||||
def install(
|
||||
additional_packages: Optional[List[str]] = typer.Option(
|
||||
..., "--additional-packages", "-ap"
|
||||
),
|
||||
):
|
||||
template_name = "install.sh"
|
||||
breakpoint()
|
||||
template = env.get_template(template_name)
|
||||
rendered = template.render(packages=packages)
|
||||
print(rendered)
|
||||
|
||||
|
||||
@app.command()
|
||||
def motd(
|
||||
volumes: Optional[List[str]] = typer.Option(
|
||||
None,
|
||||
"--volume",
|
||||
),
|
||||
pvcs: Optional[List[str]] = typer.Option(
|
||||
None,
|
||||
"--pvc",
|
||||
),
|
||||
additional_packages: Optional[List[str]] = typer.Option(
|
||||
..., "--additional-packages", "-ap"
|
||||
),
|
||||
):
|
||||
template_name = "motd.sh"
|
||||
template = env.get_template(template_name)
|
||||
rendered = template.render(
|
||||
volumes=volumes, pvcs=pvcs, additional_packages=additional_packages
|
||||
)
|
||||
print(rendered)
|
||||
11
krayt/templates.py
Normal file
11
krayt/templates.py
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
from jinja2 import Environment, FileSystemLoader
|
||||
from pathlib import Path
|
||||
|
||||
# Get the two template directories
|
||||
template_dirs = [
|
||||
Path(__file__).resolve().parents[0] / "templates",
|
||||
Path.home() / ".config" / "krayt" / "templates",
|
||||
]
|
||||
|
||||
# Create the Jinja environment
|
||||
env = Environment(loader=FileSystemLoader([str(path) for path in template_dirs]))
|
||||
3
krayt/templates/.kraytrc
Normal file
3
krayt/templates/.kraytrc
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
if [ -t 1 ] && [ -f /etc/motd ]; then
|
||||
cat /etc/motd
|
||||
fi
|
||||
25
krayt/templates/base.sh
Normal file
25
krayt/templates/base.sh
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
mkdir -p /etc/krayt
|
||||
cat <<'KRAYT_INIT_SH_EOF' >/etc/krayt/init.sh
|
||||
{%- if pre_init_hooks %}
|
||||
{% for hook in pre_init_hooks %}{{ hook }}{% endfor %}
|
||||
{% endif -%}
|
||||
{%- if pre_init_scripts %}
|
||||
{% for script in pre_init_scripts %}{{ script }}{% endfor %}
|
||||
{% endif -%}
|
||||
{% include 'install.sh' %}
|
||||
{% include 'motd.sh' %}
|
||||
{% include 'kraytrc.sh' %}
|
||||
{%- if post_init_scripts %}
|
||||
{% for script in post_init_scripts %}{{ script }}{% endfor %}
|
||||
{% endif %}
|
||||
{%- if post_init_hooks %}
|
||||
{% for hook in post_init_hooks %}{{ hook }}{% endfor %}
|
||||
{% endif %}
|
||||
echo "Krayt environment ready. Sleeping forever..."
|
||||
trap "echo 'Received SIGTERM. Exiting...'; exit 0" TERM
|
||||
tail -f /dev/null &
|
||||
wait
|
||||
KRAYT_INIT_SH_EOF
|
||||
|
||||
chmod +x /etc/krayt/init.sh
|
||||
/etc/krayt/init.sh
|
||||
52
krayt/templates/install.sh
Normal file
52
krayt/templates/install.sh
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
{% if additional_packages %}
|
||||
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
|
||||
}
|
||||
|
||||
detect_package_manager_and_install_command {% for package in additional_packages %}{{ package | trim }}{% if not loop.last %} {% endif %}{% endfor %}
|
||||
{% endif %}
|
||||
116
krayt/templates/kraytrc.sh
Normal file
116
krayt/templates/kraytrc.sh
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
KRAYT_MARKER_START="# >>> Added by krayt-inject <<<"
|
||||
KRAYT_MARKER_END='# <<< End krayt-inject >>>'
|
||||
KRAYT_BLOCK='
|
||||
if [ -t 1 ] && [ -f /etc/motd ] && [ -z "$MOTD_SHOWN" ]; then
|
||||
cat /etc/motd
|
||||
export MOTD_SHOWN=1
|
||||
fi
|
||||
|
||||
# fix $SHELL, not set in some distros like alpine
|
||||
if [ -n "$BASH_VERSION" ]; then
|
||||
export SHELL=/bin/bash
|
||||
elif [ -n "$ZSH_VERSION" ]; then
|
||||
export SHELL=/bin/zsh
|
||||
else
|
||||
export SHELL=/bin/sh
|
||||
fi
|
||||
|
||||
# krayt ENVIRONMENT
|
||||
|
||||
{%- if pvcs %}
|
||||
export KRAYT_PVCS="{{ pvcs | join(' ') }}"
|
||||
{% endif -%}
|
||||
{%- if volumes %}
|
||||
export KRAYT_VOLUMES="{{ volumes | join(' ') }}"
|
||||
{% endif -%}
|
||||
{%- if secrets %}
|
||||
export KRAYT_SECRETS="{{ secrets | join(' ') }}"
|
||||
{% endif -%}
|
||||
{%- if additional_packages %}
|
||||
export KRAYT_ADDITIONAL_PACKAGES="{{ additional_packages | join(' ') }}"
|
||||
{% endif -%}
|
||||
|
||||
# Universal shell initializers
|
||||
|
||||
# Prompt
|
||||
if command -v starship >/dev/null 2>&1; then
|
||||
eval "$(starship init "$(basename "$SHELL")")"
|
||||
fi
|
||||
|
||||
# Smarter cd
|
||||
if command -v zoxide >/dev/null 2>&1; then
|
||||
eval "$(zoxide init "$(basename "$SHELL")")"
|
||||
fi
|
||||
|
||||
# Smarter shell history
|
||||
if command -v atuin >/dev/null 2>&1; then
|
||||
eval "$(atuin init "$(basename "$SHELL")")"
|
||||
fi
|
||||
|
||||
if command -v mcfly >/dev/null 2>&1; then
|
||||
eval "$(mcfly init "$(basename "$SHELL")")"
|
||||
fi
|
||||
|
||||
# Directory-based environment
|
||||
if command -v direnv >/dev/null 2>&1; then
|
||||
eval "$(direnv hook "$(basename "$SHELL")")"
|
||||
fi
|
||||
|
||||
if command -v fzf >/dev/null 2>&1; then
|
||||
case "$(basename "$SHELL")" in
|
||||
bash|zsh|fish)
|
||||
eval "$(fzf --$(basename "$SHELL"))"
|
||||
;;
|
||||
*)
|
||||
# shell not supported for fzf init
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
# "Did you mean...?" for mistyped commands
|
||||
if command -v thefuck >/dev/null 2>&1; then
|
||||
eval "$(thefuck --alias)"
|
||||
fi
|
||||
'
|
||||
cat <<EOF >/etc/.kraytrc
|
||||
$KRAYT_MARKER_START
|
||||
$KRAYT_BLOCK
|
||||
$KRAYT_MARKER_END
|
||||
EOF
|
||||
|
||||
KRAYT_RC_SOURCE='
|
||||
if [ -f /etc/.kraytrc ]; then
|
||||
. /etc/.kraytrc
|
||||
fi
|
||||
'
|
||||
|
||||
# List of common rc/profile files to patch
|
||||
RC_FILES="
|
||||
/etc/profile
|
||||
/etc/bash.bashrc
|
||||
/etc/bash/bashrc
|
||||
/etc/bashrc
|
||||
/etc/ashrc
|
||||
/etc/zsh/zshrc
|
||||
/etc/zsh/zprofile
|
||||
/etc/shinit
|
||||
/etc/fish/config.fish
|
||||
"
|
||||
|
||||
echo "Searching for rc files..."
|
||||
|
||||
for rc_file in $RC_FILES; do
|
||||
if [ -f "$rc_file" ]; then
|
||||
echo "* Found $rc_file"
|
||||
|
||||
# Check if already patched
|
||||
if grep -q "$KRAYT_MARKER_START" "$rc_file"; then
|
||||
echo "- $rc_file already has krayt block. Skipping."
|
||||
else
|
||||
echo "+ Patching $rc_file"
|
||||
echo "" >>"$rc_file"
|
||||
echo "$KRAYT_MARKER_START" >>"$rc_file"
|
||||
echo "$KRAYT_RC_SOURCE" >>"$rc_file"
|
||||
echo "$KRAYT_MARKER_END" >>"$rc_file"
|
||||
fi
|
||||
fi
|
||||
done
|
||||
40
krayt/templates/motd.sh
Normal file
40
krayt/templates/motd.sh
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
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."
|
||||
{%- if volumes %}
|
||||
|
||||
Mounted Volumes:
|
||||
{%- for volume in volumes %}
|
||||
- {{ volume }}
|
||||
{%- endfor %}
|
||||
{%- endif %}
|
||||
|
||||
{%- if pvcs %}
|
||||
|
||||
Persistent Volume Claims:
|
||||
{%- for pvc in pvcs %}
|
||||
- {{ pvc }}
|
||||
{%- endfor %}
|
||||
{%- endif %}
|
||||
|
||||
{%- if secrets %}
|
||||
|
||||
Mounted Secrets:
|
||||
{%- for secret in secrets %}
|
||||
- {{ secret }}
|
||||
{%- endfor %}
|
||||
{%- endif %}
|
||||
|
||||
{%- if additional_packages %}
|
||||
|
||||
Additional Packages:
|
||||
{%- for package in additional_packages %}
|
||||
- {{ package }}
|
||||
{%- endfor %}
|
||||
{%- endif %}
|
||||
|
||||
EOF
|
||||
Loading…
Add table
Add a link
Reference in a new issue