diff --git a/.gitignore b/.gitignore index 6c311bf..e1a3186 100644 --- a/.gitignore +++ b/.gitignore @@ -962,4 +962,3 @@ FodyWeavers.xsd # Additional files built by Visual Studio # End of https://www.toptal.com/developers/gitignore/api/vim,node,data,emacs,python,pycharm,executable,sublimetext,visualstudio,visualstudiocode -krayt diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..38ea521 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021-present Waylon S. Walker + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/krayt/__about__.py b/krayt/__about__.py new file mode 100644 index 0000000..6c8e6b9 --- /dev/null +++ b/krayt/__about__.py @@ -0,0 +1 @@ +__version__ = "0.0.0" diff --git a/krayt/__init__.py b/krayt/__init__.py new file mode 100644 index 0000000..236e53a --- /dev/null +++ b/krayt/__init__.py @@ -0,0 +1,5 @@ +from krayt.__about__ import __version__ + +__all__ = [ + "__version__", +] diff --git a/krayt/cli/__init__.py b/krayt/cli/__init__.py new file mode 100644 index 0000000..71fe124 --- /dev/null +++ b/krayt/cli/__init__.py @@ -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() diff --git a/krayt/cli/create.py b/krayt/cli/create.py new file mode 100644 index 0000000..41be061 --- /dev/null +++ b/krayt/cli/create.py @@ -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() diff --git a/krayt/cli/templates.py b/krayt/cli/templates.py new file mode 100644 index 0000000..fe866c1 --- /dev/null +++ b/krayt/cli/templates.py @@ -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) diff --git a/krayt/templates.py b/krayt/templates.py new file mode 100644 index 0000000..53d902f --- /dev/null +++ b/krayt/templates.py @@ -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])) diff --git a/krayt/templates/.kraytrc b/krayt/templates/.kraytrc new file mode 100644 index 0000000..591b80e --- /dev/null +++ b/krayt/templates/.kraytrc @@ -0,0 +1,3 @@ +if [ -t 1 ] && [ -f /etc/motd ]; then + cat /etc/motd +fi diff --git a/krayt/templates/base.sh b/krayt/templates/base.sh new file mode 100644 index 0000000..c248460 --- /dev/null +++ b/krayt/templates/base.sh @@ -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 diff --git a/krayt/templates/install.sh b/krayt/templates/install.sh new file mode 100644 index 0000000..df37270 --- /dev/null +++ b/krayt/templates/install.sh @@ -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 [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 %} diff --git a/krayt/templates/kraytrc.sh b/krayt/templates/kraytrc.sh new file mode 100644 index 0000000..dd4fe85 --- /dev/null +++ b/krayt/templates/kraytrc.sh @@ -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 </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 diff --git a/krayt/templates/motd.sh b/krayt/templates/motd.sh new file mode 100644 index 0000000..9de1e85 --- /dev/null +++ b/krayt/templates/motd.sh @@ -0,0 +1,40 @@ +cat </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 diff --git a/krayt.py b/krayt1.py similarity index 100% rename from krayt.py rename to krayt1.py diff --git a/krayt2.py b/krayt2.py new file mode 100755 index 0000000..e79d280 --- /dev/null +++ b/krayt2.py @@ -0,0 +1,337 @@ +#!/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 [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() diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..a521984 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,54 @@ +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[tool.hatch.build.targets.wheel] + +[tool.hatch.build.targets.sdist] +exclude = ["/.github"] + +[project] +name = "krayt" +dynamic = ["version"] +description = 'kubernetes volume explorer' +readme = "README.md" +requires-python = ">=3.8" +keywords = [] +classifiers = [ + "Development Status :: 4 - Beta", + "Programming Language :: Python", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: Implementation :: CPython", + "Programming Language :: Python :: Implementation :: PyPy", +] +dependencies = [ + "typer", + "kubernetes", + "InquirerPy", + "jinja2", + "iterfzf", +] + +[[project.authors]] +name = "Waylon Walker" +email = "waylon@waylonwalker.com" + +[project.license] +file = "LICENSE" + +[project.urls] +Homepage = "https://github.com/waylonwalker/krayt#readme" +Documentation = "https://github.com/waylonwalker/krayt#readme" +Changelog = "https://github.com/waylonwalker/krayt#changelog" +Issues = "https://github.com/waylonwalker/krayt/issues" +Source = "https://github.com/waylonwalker/krayt" + +[tool.hatch.version] +path = "krayt/__about__.py" + +[project.scripts] +krayt = "krayt.cli:app" diff --git a/test.sh b/test.sh new file mode 100644 index 0000000..117fb41 --- /dev/null +++ b/test.sh @@ -0,0 +1,51 @@ +detect_package_manager_and_install_command() { + # Accept packages as arguments + PACKAGES=("$@") + + if [[ ${#PACKAGES[@]} -eq 0 ]]; then + echo "Usage: detect_package_manager_and_install_command [package2] [...]" + return 1 + fi + + if command -v apt &>/dev/null; then + PKG_MANAGER="apt" + UPDATE_CMD="sudo apt update" + INSTALL_CMD="sudo apt install -y" + elif command -v dnf &>/dev/null; then + PKG_MANAGER="dnf" + UPDATE_CMD="" + INSTALL_CMD="sudo dnf install -y" + elif command -v yum &>/dev/null; then + PKG_MANAGER="yum" + UPDATE_CMD="" + INSTALL_CMD="sudo yum install -y" + elif command -v pacman &>/dev/null; then + PKG_MANAGER="pacman" + UPDATE_CMD="" + INSTALL_CMD="sudo pacman -Sy --noconfirm" + elif command -v zypper &>/dev/null; then + PKG_MANAGER="zypper" + UPDATE_CMD="" + INSTALL_CMD="sudo zypper install -y" + elif command -v apk &>/dev/null; then + PKG_MANAGER="apk" + UPDATE_CMD="" + INSTALL_CMD="sudo apk add" + else + echo "No supported package manager found." + return 2 + fi + + # Build the full install command + if [[ -n "$UPDATE_CMD" ]]; then + # echo $UPDATE_CMD + # $UPDATE_CMD + echo $INSTALL_CMD ${PACKAGES[*]} + $INSTALL_CMD ${PACKAGES[*]} + else + echo $INSTALL_CMD ${PACKAGES[*]} + $INSTALL_CMD ${PACKAGES[*]} + fi +} + +detect_package_manager_and_install_command git htop diff --git a/test.yaml b/test.yaml new file mode 100644 index 0000000..194d69b --- /dev/null +++ b/test.yaml @@ -0,0 +1,96 @@ +apiVersion: batch/v1 +kind: Job +metadata: + name: htmx-patterns-66bfd987d7-98sw7-krayt-1744164311 + namespace: htmx-patterns + labels: + app: krayt + annotations: + pvcs: none +spec: + template: + metadata: + labels: + app: krayt + spec: + containers: + - name: inspector + image: alpine:latest + command: + - sh + - -c + - "mkdir -p /etc/krayt\ncat <<'KRAYT_INIT_SH_EOF' >/etc/krayt/init.sh\ndetect_package_manager_and_install_command()\ + \ {\n\tif [ $# -eq 0 ]; then\n\t\techo \"Usage: detect_package_manager_and_install_command\ + \ [package2] [...]\"\n\t\treturn 1\n\tfi\n\n\tif command -v apt\ + \ >/dev/null 2>&1; then\n\t\tPKG_MANAGER=\"apt\"\n\t\tUPDATE_CMD=\"apt update\ + \ &&\"\n\t\tINSTALL_CMD=\"apt install -y\"\n\telif command -v dnf >/dev/null\ + \ 2>&1; then\n\t\tPKG_MANAGER=\"dnf\"\n\t\tUPDATE_CMD=\"\"\n\t\tINSTALL_CMD=\"\ + dnf install -y\"\n\telif command -v yum >/dev/null 2>&1; then\n\t\tPKG_MANAGER=\"\ + yum\"\n\t\tUPDATE_CMD=\"\"\n\t\tINSTALL_CMD=\"yum install -y\"\n\telif command\ + \ -v pacman >/dev/null 2>&1; then\n\t\tPKG_MANAGER=\"pacman\"\n\t\tUPDATE_CMD=\"\ + \"\n\t\tINSTALL_CMD=\"pacman -Sy --noconfirm\"\n\telif command -v zypper\ + \ >/dev/null 2>&1; then\n\t\tPKG_MANAGER=\"zypper\"\n\t\tUPDATE_CMD=\"\"\ + \n\t\tINSTALL_CMD=\"zypper install -y\"\n\telif command -v apk >/dev/null\ + \ 2>&1; then\n\t\tPKG_MANAGER=\"apk\"\n\t\tUPDATE_CMD=\"\"\n\t\tINSTALL_CMD=\"\ + apk add\"\n\telse\n\t\techo \"No supported package manager found.\"\n\t\t\ + return 2\n\tfi\n\n\tPACKAGES=\"$*\"\n\n\tif [ -n \"$UPDATE_CMD\" ]; then\n\ + \t\techo \"$UPDATE_CMD\n echo $INSTALL_CMD $PACKAGES\"\n\t\t$UPDATE_CMD\n\ + \t\t$INSTALL_CMD $PACKAGES\n\n\telse\n\t\techo \"$INSTALL_CMD $PACKAGES\"\ + \n\t\t$INSTALL_CMD $PACKAGES\n\tfi\n}\n\ndetect_package_manager_and_install_command\ + \ 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\n\ + \ncat </etc/motd\n\u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\ + \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\ + \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\ + \u2500\u2500\u2500\u2500\u2510\n\u2502Krayt Dragon's Lair \ + \ \u2502\n\u2502A safe haven for volume inspection \u2502\n\u2514\u2500\ + \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\ + \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\ + \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n\n\"\ + Inside every volume lies a pearl of wisdom waiting to be discovered.\"\n\ + \nAdditional Packages:\n- ripgrep\n- exa\n- ncdu\n- dust\n- file\n- hexyl\n\ + - jq\n- yq\n- bat\n- fd\n- fzf\n- htop\n- bottom\n- difftastic\n- mtr\n\ + - bind-tools\n- aws-cli\n- sqlite\n- sqlite-dev\n- sqlite-libs\n- bash\n\ + - neovim\n- starship\n\nEOF\nKRAYT_MARKER_START=\"# >>> Added by krayt-inject\ + \ <<<\"\nKRAYT_MARKER_END='# <<< End krayt-inject >>>'\nKRAYT_BLOCK='\n\ + if [ -t 1 ] && [ -f /etc/motd ] && [ -z \"$MOTD_SHOWN\" ]; then\n cat\ + \ /etc/motd\n export MOTD_SHOWN=1\nfi\n\n# fix $SHELL, not set in some\ + \ distros like alpine\nif [ -n \"$BASH_VERSION\" ]; then\n export SHELL=/bin/bash\n\ + elif [ -n \"$ZSH_VERSION\" ]; then\n export SHELL=/bin/zsh\nelse\n \ + \ export SHELL=/bin/sh\nfi\n\n# krayt ENVIRONMENT\nexport KRAYT_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\"\ + \n# Universal shell initializers\n\n# Prompt\nif command -v starship >/dev/null\ + \ 2>&1; then\n\teval \"$(starship init \"$(basename \"$SHELL\")\")\"\nfi\n\ + \n# Smarter cd\nif command -v zoxide >/dev/null 2>&1; then\n\teval \"$(zoxide\ + \ init \"$(basename \"$SHELL\")\")\"\nfi\n\n# Smarter shell history\nif\ + \ command -v atuin >/dev/null 2>&1; then\n\teval \"$(atuin init \"$(basename\ + \ \"$SHELL\")\")\"\nfi\n\nif command -v mcfly >/dev/null 2>&1; then\n\t\ + eval \"$(mcfly init \"$(basename \"$SHELL\")\")\"\nfi\n\n# Directory-based\ + \ environment\nif command -v direnv >/dev/null 2>&1; then\n\teval \"$(direnv\ + \ hook \"$(basename \"$SHELL\")\")\"\nfi\n\nif command -v fzf >/dev/null\ + \ 2>&1; then\n case \"$(basename \"$SHELL\")\" in\n bash|zsh|fish)\n\ + \ eval \"$(fzf --$(basename \"$SHELL\"))\"\n ;;\n\ + \ *)\n # shell not supported for fzf init\n \ + \ ;;\n esac\nfi\n# \"Did you mean...?\" for mistyped commands\nif command\ + \ -v thefuck >/dev/null 2>&1; then\n\teval \"$(thefuck --alias)\"\nfi\n\ + '\ncat </etc/.kraytrc\n$KRAYT_MARKER_START\n$KRAYT_BLOCK\n$KRAYT_MARKER_END\n\ + EOF\n\nKRAYT_RC_SOURCE='\nif [ -f /etc/.kraytrc ]; then\n . /etc/.kraytrc\n\ + fi\n'\n\n# List of common rc/profile files to patch\nRC_FILES=\"\n/etc/profile\n\ + /etc/bash.bashrc\n/etc/bash/bashrc\n/etc/bashrc\n/etc/ashrc\n/etc/zsh/zshrc\n\ + /etc/zsh/zprofile\n/etc/shinit\n/etc/fish/config.fish\n\"\n\necho \"Searching\ + \ for rc files...\"\n\nfor rc_file in $RC_FILES; do\n\tif [ -f \"$rc_file\"\ + \ ]; then\n\t\techo \"* Found $rc_file\"\n\n\t\t# Check if already patched\n\ + \t\tif grep -q \"$KRAYT_MARKER_START\" \"$rc_file\"; then\n\t\t\techo \"\ + - $rc_file already has krayt block. Skipping.\"\n\t\telse\n\t\t\techo \"\ + + Patching $rc_file\"\n\t\t\techo \"\" >>\"$rc_file\"\n\t\t\techo \"$KRAYT_MARKER_START\"\ + \ >>\"$rc_file\"\n\t\t\techo \"$KRAYT_RC_SOURCE\" >>\"$rc_file\"\n\t\t\t\ + echo \"$KRAYT_MARKER_END\" >>\"$rc_file\"\n\t\tfi\n\tfi\ndone\necho \"Krayt\ + \ environment ready. Sleeping forever...\"\ntrap \"echo 'Received SIGTERM.\ + \ Exiting...'; exit 0\" TERM\ntail -f /dev/null &\nwait\nKRAYT_INIT_SH_EOF\n\ + \nchmod +x /etc/krayt/init.sh\n/etc/krayt/init.sh" + env: [] + volumeMounts: [] + volumes: [] + restartPolicy: Never +