From 86985a62a928a0d441462d8a9a252678c6e8086d Mon Sep 17 00:00:00 2001 From: Waylon Walker Date: Mon, 24 Mar 2025 15:01:30 -0500 Subject: [PATCH 01/50] Update README.md --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index c05e73c..b9a38f3 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # Krayt - The Kubernetes Volume Inspector +![krayt hero image](./krayt.webp "A dark, cartoon-style wide-format illustration featuring a heroic explorer standing in a twilight desert beside a cracked-open dragon skull. The explorer holds a glowing pearl that reveals floating icons representing data and technology. The hero wears utility gear and a sword, with terminal and file icons on their belt. The desert backdrop includes jagged rocks, two moons in a starry sky, and moody blue and purple tones. At the top, the word “KRAYT” is displayed in bold, tech-inspired fantasy lettering.") + Like cracking open a Krayt dragon pearl, this tool helps you inspect what's inside your Kubernetes volumes. Hunt down storage issues and explore your persistent data like a true Tatooine dragon hunter. From 90491d17bf771f282c05848d6c52c62d4f690b4e Mon Sep 17 00:00:00 2001 From: "Waylon S. Walker" Date: Tue, 25 Mar 2025 10:14:19 -0500 Subject: [PATCH 02/50] release 0.2.0 --- CHANGELOG.md | 7 +++++++ README.md | 6 ++++++ krayt.py | 8 ++++++++ version | 2 +- 4 files changed, 22 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bb1a694..4f04f65 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +## 0.2.0 + +### Added + +- Support for imagepullsecret flag on krayt create command + - Allows pulling from private container registries by specifying an image pull secret + ## 0.1.0 ### Added diff --git a/README.md b/README.md index 01ff9fb..fc9dd26 100644 --- a/README.md +++ b/README.md @@ -38,6 +38,12 @@ This will install the `krayt` command to `/usr/local/bin`. # Create a new inspector and apply it directly krayt create | kubectl apply -f - +# Use a custom image +krayt create --image custom-image:latest | kubectl apply -f - + +# Use a private image with pull secret +krayt create --image private-registry.com/image:latest --imagepullsecret my-registry-secret | kubectl apply -f - + # Or review the manifest first krayt create > inspector.yaml kubectl apply -f inspector.yaml diff --git a/krayt.py b/krayt.py index 1edee10..d602a34 100755 --- a/krayt.py +++ b/krayt.py @@ -336,6 +336,7 @@ def create_inspector_job( 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()) @@ -532,6 +533,7 @@ def create_inspector_job( } ], "volumes": [format_volume(v) for v in volumes if format_volume(v)], + "imagePullSecrets": [{"name": imagepullsecret}] if imagepullsecret else None, "restartPolicy": "Never", }, }, @@ -777,6 +779,11 @@ def create( "-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. @@ -804,6 +811,7 @@ def create( volume_mounts, volumes, image=image, + imagepullsecret=imagepullsecret, ) # Output the job manifest diff --git a/version b/version index 6e8bf73..0ea3a94 100644 --- a/version +++ b/version @@ -1 +1 @@ -0.1.0 +0.2.0 From a60562b7fcb13eb27a3599c741b0a4d5b0243ead Mon Sep 17 00:00:00 2001 From: "Waylon S. Walker" Date: Tue, 8 Apr 2025 21:09:10 -0500 Subject: [PATCH 03/50] wip --- .gitignore | 1 - LICENSE | 21 + krayt/__about__.py | 1 + krayt/__init__.py | 5 + krayt/cli/__init__.py | 18 + krayt/cli/create.py | 763 +++++++++++++++++++++++++++++++++++++ krayt/cli/templates.py | 90 +++++ krayt/templates.py | 11 + krayt/templates/.kraytrc | 3 + krayt/templates/base.sh | 25 ++ krayt/templates/install.sh | 52 +++ krayt/templates/kraytrc.sh | 116 ++++++ krayt/templates/motd.sh | 40 ++ krayt.py => krayt1.py | 0 krayt2.py | 337 ++++++++++++++++ pyproject.toml | 54 +++ test.sh | 51 +++ test.yaml | 96 +++++ 18 files changed, 1683 insertions(+), 1 deletion(-) create mode 100644 LICENSE create mode 100644 krayt/__about__.py create mode 100644 krayt/__init__.py create mode 100644 krayt/cli/__init__.py create mode 100644 krayt/cli/create.py create mode 100644 krayt/cli/templates.py create mode 100644 krayt/templates.py create mode 100644 krayt/templates/.kraytrc create mode 100644 krayt/templates/base.sh create mode 100644 krayt/templates/install.sh create mode 100644 krayt/templates/kraytrc.sh create mode 100644 krayt/templates/motd.sh rename krayt.py => krayt1.py (100%) create mode 100755 krayt2.py create mode 100644 pyproject.toml create mode 100644 test.sh create mode 100644 test.yaml 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 + From 97377469232f33868e427c0c8b239d77d086d112 Mon Sep 17 00:00:00 2001 From: "Waylon S. Walker" Date: Wed, 9 Apr 2025 09:15:00 -0500 Subject: [PATCH 04/50] more package support --- krayt/bundles.py | 87 +++++++++++++++++++ krayt/cli/create.py | 7 +- krayt/package.py | 166 +++++++++++++++++++++++++++++++++++++ krayt/templates/install.sh | 33 +++++--- pyproject.toml | 1 + 5 files changed, 283 insertions(+), 11 deletions(-) create mode 100644 krayt/bundles.py create mode 100644 krayt/package.py diff --git a/krayt/bundles.py b/krayt/bundles.py new file mode 100644 index 0000000..baed9c9 --- /dev/null +++ b/krayt/bundles.py @@ -0,0 +1,87 @@ +""" +Bundles of packages available in most package managers. +""" + +basics = [ + "curl", + "wget", + "jq", + "yq", + "bash", + "coreutils", +] +pretty = [ + *basics + "starship", + "atuin", + "bash", + "zsh", + "fish", + "bat", + "eza", +] +networking = [ + *basics + "mtr", + "bind-tools", + "aws-cli", + "curl", + "wget", + "iperf3", + "nmap", + "traceroute", + "netcat-openbsd", +] + +database = [ + *basics + "sqlite", + "sqlite-dev", + "sqlite-libs", + "postgresql", + "mysql", + "mariadb", + "redis", + "mongodb", +] + +storage = [ + *basics + "ncdu", + "dust", + "file", + "hexyl", + "ripgrep", + "fd", + "fzf", + "difftastic", +] + +search = [ + *basics + "ripgrep", + "fd", + "fzf", + "difftastic", +] + +monitoring = [ + *basics + "htop", + "bottom", + "mtr", +] + +all = list( + set( + [ + *basics, + *pretty, + *networking, + *database, + *storage, + *search, + *monitoring, + ] + ) +) diff --git a/krayt/cli/create.py b/krayt/cli/create.py index 41be061..2e7d373 100644 --- a/krayt/cli/create.py +++ b/krayt/cli/create.py @@ -6,7 +6,7 @@ import os from pathlib import Path import time import typer -from typing import Any, Optional +from typing import Any, List, Optional import yaml KRAYT_VERSION = "NIGHTLY" @@ -324,6 +324,11 @@ def create_inspector_job( volumes: list, image: str = "alpine:latest", imagepullsecret: Optional[str] = None, + additional_packages: Optional[List[str]] = None, + pre_init_scripts: Optional[List[str]] = None, + post_init_scripts: Optional[List[str]] = None, + pre_init_hooks: Optional[List[str]] = None, + post_init_hooks: Optional[List[str]] = None, ): """Create a Krayt inspector job with the given mounts""" timestamp = int(time.time()) diff --git a/krayt/package.py b/krayt/package.py new file mode 100644 index 0000000..d88d494 --- /dev/null +++ b/krayt/package.py @@ -0,0 +1,166 @@ +from more_itertools import unique_everseen +from pydantic import BaseModel, BeforeValidator, model_validator +from typing import Annotated, List, Literal, Optional +from typing_extensions import Self + + +SUPPORTED_KINDS = { + "system", + "uv", + "installer", + "i", + "curlbash", + "curlsh", + "brew", + "cargo", + "pipx", + "npm", + "go", + "gh", +} + + +def validate_kind(v): + if v not in SUPPORTED_KINDS: + raise ValueError( + f"Unknown installer kind: {v}\n Supported kinds: {SUPPORTED_KINDS}\n " + ) + return v + + +class Package(BaseModel): + """ + Represents a package to be installed, either via system package manager + or an alternative installer like uv, installer.sh, brew, etc. + """ + + kind: Annotated[ + Literal[ + "system", + "uv", + "i", + "curlsh", + "brew", + "cargo", + "pipx", + "npm", + "go", + "gh", + ], + BeforeValidator(validate_kind), + ] = "system" + dependencies: Optional[List[str]] = None + value: str + + @classmethod + def from_raw(cls, raw: str) -> "Package": + """ + Parse a raw input string like 'uv:copier' into a Package(kind='uv', value='copier') + """ + if ":" in raw: + prefix, value = raw.split(":", 1) + return cls(kind=prefix.strip(), value=value.strip()) + else: + return cls(kind="system", value=raw.strip()) + + @model_validator(mode="after") + def validate_dependencies(self) -> Self: + if self.dependencies: + return self + else: + if self.kind == "system": + return self + dependencies = [] + if self.kind in ["uv", "i", "installer", "curlbash", "curlsh", "gh"]: + dependencies.extend( + [ + Package.from_raw("curl"), + ] + ) + if self.kind == "brew": + dependencies.extend( + [ + Package.from_raw("brew"), + Package.from_raw("git"), + ] + ) + if self.kind == "cargo": + dependencies.extend( + [ + Package.from_raw("cargo"), + ] + ) + if self.kind == "pipx": + dependencies.extend( + [ + Package.from_raw("pipx"), + ] + ) + if self.kind == "npm": + dependencies.extend( + [ + Package.from_raw("npm"), + ] + ) + if self.kind == "go": + dependencies.extend( + [ + Package.from_raw("go"), + ] + ) + self.dependencies = dependencies + return self + + def __str__(self): + return f"{self.kind}:{self.value}" if self.kind != "system" else self.value + + def is_system(self) -> bool: + return self.kind == "system" + + def install_command(self) -> str: + """ + Generate the bash install command snippet for this package. + """ + if self.kind == "system": + return f"detect_package_manager_and_install {self.value}" + elif self.kind == "uv": + return f"uv tool install {self.value}" + elif self.kind in ["i", "installer", "gh"]: + return f"curl -fsSL https://i.jpillora.com/{self.value} | sh" + elif self.kind == "curlsh": + return f"curl -fsSL {self.value} | sh" + elif self.kind == "curlbash": + return f"curl -fsSL {self.value} | bash" + elif self.kind == "brew": + return f"brew install {self.value}" + elif self.kind == "cargo": + return f"cargo install {self.value}" + elif self.kind == "pipx": + return f"pipx install {self.value}" + elif self.kind == "npm": + return f"npm install -g {self.value}" + elif self.kind == "go": + return f"go install {self.value}@latest" + else: + raise ValueError(f"Unknown install method for kind={self.kind}") + + +if __name__ == "__main__": + raw_inputs = [ + "curl", + "wget", + "uv:copier", + "i:sharkdp/fd", + "curlsh:https://example.com/install.sh", + "brew:bat", + ] + + packages = [Package.from_raw(raw) for raw in raw_inputs] + dependencies = [] + for package in packages: + if package.dependencies: + dependencies.extend( + [dependency.install_command() for dependency in package.dependencies] + ) + installs = [package.install_command() for package in packages] + print("\n".join(install for install in unique_everseen([*dependencies, *installs]))) diff --git a/krayt/templates/install.sh b/krayt/templates/install.sh index df37270..8e0c8d9 100644 --- a/krayt/templates/install.sh +++ b/krayt/templates/install.sh @@ -1,7 +1,7 @@ {% if additional_packages %} -detect_package_manager_and_install_command() { +detect_package_manager_and_install() { if [ $# -eq 0 ]; then - echo "Usage: detect_package_manager_and_install_command [package2] [...]" + echo "Usage: detect_package_manager_and_install [package2] [...]" return 1 fi @@ -34,19 +34,32 @@ detect_package_manager_and_install_command() { return 2 fi - PACKAGES="$*" + echo "Using package manager: $PKG_MANAGER" if [ -n "$UPDATE_CMD" ]; then - echo "$UPDATE_CMD - echo $INSTALL_CMD $PACKAGES" - $UPDATE_CMD - $INSTALL_CMD $PACKAGES + echo "Running package manager update..." + eval "$UPDATE_CMD" + fi + FAILED_PKGS=() + + for pkg in "$@"; do + echo "Installing package: $pkg" + if ! eval "$INSTALL_CMD $pkg"; then + echo "⚠️ Warning: Failed to install package: $pkg" + FAILED_PKGS+=("$pkg") + fi + done + + if [ ${#FAILED_PKGS[@]} -ne 0 ]; then + echo "⚠️ The following packages failed to install:" + for failed_pkg in "${FAILED_PKGS[@]}"; do + echo " - $failed_pkg" + done else - echo "$INSTALL_CMD $PACKAGES" - $INSTALL_CMD $PACKAGES + echo "✅ All requested packages installed successfully." fi } -detect_package_manager_and_install_command {% for package in additional_packages %}{{ package | trim }}{% if not loop.last %} {% endif %}{% endfor %} +detect_package_manager_and_install {% for package in additional_packages %}{{ package | trim }}{% if not loop.last %} {% endif %}{% endfor %} {% endif %} diff --git a/pyproject.toml b/pyproject.toml index a521984..cfeb2a1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -31,6 +31,7 @@ dependencies = [ "InquirerPy", "jinja2", "iterfzf", + "pydantic", ] [[project.authors]] From 378744632fd2ad6051e89cf0335ae55208e36de7 Mon Sep 17 00:00:00 2001 From: "Waylon S. Walker" Date: Thu, 10 Apr 2025 09:21:51 -0500 Subject: [PATCH 05/50] wip --- krayt/bundles.py | 12 +- krayt/cli/__init__.py | 12 +- krayt/cli/bundles.py | 25 +++++ krayt/cli/{create.py => pod.py} | 193 +++++++++++++++----------------- krayt/cli/templates.py | 20 +++- krayt/package.py | 116 +++++++++---------- pyproject.toml | 3 +- 7 files changed, 208 insertions(+), 173 deletions(-) create mode 100644 krayt/cli/bundles.py rename krayt/cli/{create.py => pod.py} (84%) diff --git a/krayt/bundles.py b/krayt/bundles.py index baed9c9..a375140 100644 --- a/krayt/bundles.py +++ b/krayt/bundles.py @@ -11,7 +11,7 @@ basics = [ "coreutils", ] pretty = [ - *basics + *basics, "starship", "atuin", "bash", @@ -21,7 +21,7 @@ pretty = [ "eza", ] networking = [ - *basics + *basics, "mtr", "bind-tools", "aws-cli", @@ -34,7 +34,7 @@ networking = [ ] database = [ - *basics + *basics, "sqlite", "sqlite-dev", "sqlite-libs", @@ -46,7 +46,7 @@ database = [ ] storage = [ - *basics + *basics, "ncdu", "dust", "file", @@ -58,7 +58,7 @@ storage = [ ] search = [ - *basics + *basics, "ripgrep", "fd", "fzf", @@ -66,7 +66,7 @@ search = [ ] monitoring = [ - *basics + *basics, "htop", "bottom", "mtr", diff --git a/krayt/cli/__init__.py b/krayt/cli/__init__.py index 71fe124..06e5c40 100644 --- a/krayt/cli/__init__.py +++ b/krayt/cli/__init__.py @@ -1,12 +1,18 @@ from krayt import __version__ -from krayt.cli.create import app as create_app +from krayt.cli.bundles import app as bundles_app +from krayt.cli.pod import app as pod_app, create, exec, logs from krayt.cli.templates import app as templates_app from typer import Typer app = Typer() -app.add_typer(templates_app, name="templates") -app.add_typer(create_app, name="create") +app.add_typer(templates_app, name="templates", no_args_is_help=True) +app.add_typer(pod_app, name="pod", no_args_is_help=True) +app.command(name="create")(create) +app.command(name="c")(create) +app.command(name="exec")(exec) +app.command(name="logs")(logs) +app.add_typer(bundles_app, name="bundles", no_args_is_help=True) @app.command() diff --git a/krayt/cli/bundles.py b/krayt/cli/bundles.py new file mode 100644 index 0000000..be30fe8 --- /dev/null +++ b/krayt/cli/bundles.py @@ -0,0 +1,25 @@ +from krayt import bundles +import typer + +app = typer.Typer() + + +@app.command() +def list( + verbose: bool = typer.Option( + False, + "--verbose", + "-v", + help="Verbose output", + ), +): + """List available bundles""" + typer.echo("Available bundles:") + # get all variables from bundles + for bundle in bundles.__dict__.keys(): + if bundle.startswith("__"): + continue + typer.echo(bundle) + if verbose: + for package in bundles.__dict__[bundle]: + typer.echo(f" - {package}") diff --git a/krayt/cli/create.py b/krayt/cli/pod.py similarity index 84% rename from krayt/cli/create.py rename to krayt/cli/pod.py index 2e7d373..93a9a80 100644 --- a/krayt/cli/create.py +++ b/krayt/cli/pod.py @@ -1,16 +1,13 @@ -from iterfzf import iterfzf +import iterfzf from krayt.templates import env from kubernetes import client, config import logging import os -from pathlib import Path import time import typer from typing import Any, List, Optional import yaml -KRAYT_VERSION = "NIGHTLY" - logging.basicConfig(level=logging.WARNING) app = typer.Typer() @@ -92,7 +89,15 @@ def fuzzy_select(items): # Use fzf for selection try: - selected = iterfzf(formatted_items) + # selected = inquirer.fuzzy( + # message="Select a pod to clone:", choices=formatted_items + # ).execute() + + selected = iterfzf.iterfzf( + formatted_items, + prompt="Select a pod to clone:", + preview='''kubectl describe pod "$(echo {} | awk -F'[(|)]' '{gsub(/\x1b\[[0-9;]*m/, "", $1); print $1}' | xargs)" -n "$(echo {} | awk -F'[(|)]' '{gsub(/\x1b\[[0-9;]*m/, "", $2); print $2}' | xargs)"''', + ) if not selected: return None, None @@ -239,83 +244,6 @@ def get_env_vars_and_secret_volumes(api, namespace: str): return env_vars, volumes -def get_init_scripts(): - """Get the contents of init scripts to be run in the pod""" - init_dir = Path.home() / ".config" / "krayt" / "init.d" - if not init_dir.exists(): - logging.debug("No init.d directory found at %s", init_dir) - return "" - - scripts = sorted(init_dir.glob("*.sh")) - if not scripts: - logging.debug("No init scripts found in %s", init_dir) - return "" - - # Create a combined script that will run all init scripts - init_script = "#!/bin/bash\n\n" - init_script += "exec 2>&1 # Redirect stderr to stdout for proper logging\n" - init_script += "set -e # Exit on error\n\n" - init_script += "echo 'Running initialization scripts...' | tee /tmp/init.log\n\n" - init_script += "mkdir -p /tmp/init.d\n\n" # Create directory once at the start - - for script in scripts: - try: - with open(script, "r") as f: - script_content = f.read() - if not script_content.strip(): - logging.debug("Skipping empty script %s", script) - continue - - # Use a unique heredoc delimiter for each script to avoid nesting issues - delimiter = f"EOF_SCRIPT_{script.stem.upper()}" - - init_script += ( - f"echo '=== Running {script.name} ===' | tee -a /tmp/init.log\n" - ) - init_script += f"cat > /tmp/init.d/{script.name} << '{delimiter}'\n" - init_script += script_content - if not script_content.endswith("\n"): - init_script += "\n" - init_script += f"{delimiter}\n" - init_script += f"chmod +x /tmp/init.d/{script.name}\n" - init_script += f'cd /tmp/init.d && ./{script.name} 2>&1 | tee -a /tmp/init.log || {{ echo "Failed to run {script.name}"; exit 1; }}\n' - init_script += ( - f"echo '=== Finished {script.name} ===' | tee -a /tmp/init.log\n\n" - ) - except Exception as e: - logging.error(f"Failed to load init script {script}: {e}") - - init_script += "echo 'Initialization scripts complete.' | tee -a /tmp/init.log\n" - return init_script - - -def get_motd_script(mount_info, pvc_info): - """Generate the MOTD script with proper escaping""" - return f""" -# Create MOTD -cat << EOF > /etc/motd -==================================== -Krayt Dragon's Lair -A safe haven for volume inspection -==================================== - -"Inside every volume lies a pearl of wisdom waiting to be discovered." - -Mounted Volumes: -$(echo "{",".join(mount_info)}" | tr ',' '\\n' | sed 's/^/- /') - -Persistent Volume Claims: -$(echo "{",".join(pvc_info)}" | tr ',' '\\n' | sed 's/^/- /') - -Mounted Secrets: -$(for d in /mnt/secrets/*; do if [ -d "$d" ]; then echo "- $(basename $d)"; fi; done) - -Init Script Status: -$(if [ -f /tmp/init.log ]; then echo "View initialization log at /tmp/init.log"; fi) -EOF -""" - - def create_inspector_job( api, namespace: str, @@ -418,6 +346,7 @@ def create_inspector_job( "annotations": {"pvcs": ",".join(pvc_info) if pvc_info else "none"}, }, "spec": { + "ttlSecondsAfterFinished": 600, "template": { "metadata": {"labels": {"app": "krayt"}}, "spec": { @@ -459,24 +388,6 @@ PROTECTED_NAMESPACES = { } -def load_init_scripts(): - """Load and execute initialization scripts from ~/.config/krayt/scripts/""" - init_dir = Path.home() / ".config" / "krayt" / "scripts" - if not init_dir.exists(): - return - - # Sort scripts to ensure consistent execution order - scripts = sorted(init_dir.glob("*.py")) - - for script in scripts: - try: - with open(script, "r") as f: - exec(f.read(), globals()) - logging.debug(f"Loaded init script: {script}") - except Exception as e: - logging.error(f"Failed to load init script {script}: {e}") - - def setup_environment(): """Set up the environment with proxy settings and other configurations""" # Load environment variables for proxies @@ -657,8 +568,16 @@ def clean( def create( namespace: Optional[str] = typer.Option( None, + "--namespace", + "-n", help="Kubernetes namespace. If not specified, will search for pods across all namespaces.", ), + clone: Optional[str] = typer.Option( + None, + "--clone", + "-c", + help="Clone an existing pod", + ), image: str = typer.Option( "alpine:latest", "--image", @@ -670,6 +589,39 @@ def create( "--imagepullsecret", help="Name of the image pull secret to use for pulling private images", ), + additional_packages: Optional[List[str]] = typer.Option( + None, + "--additional-packages", + "-ap", + help="additional packages to install in the inspector pod", + ), + additional_package_bundles: Optional[List[str]] = typer.Option( + None, + "--additional-package-bundles", + "-ab", + help="additional packages to install in the inspector pod", + ), + pre_init_scripts: Optional[List[str]] = typer.Option( + None, + "--pre-init-scripts", + help="additional scripts to execute at the end of container initialization", + ), + post_init_scripts: Optional[List[str]] = typer.Option( + None, + "--post-init-scripts", + "--init-scripts", + help="additional scripts to execute at the start of container initialization", + ), + pre_init_hooks: Optional[List[str]] = typer.Option( + None, + "--pre-init-hooks", + help="additional hooks to execute at the end of container initialization", + ), + post_init_hooks: Optional[List[str]] = typer.Option( + None, + "--post-init-hooks", + help="additional hooks to execute at the start of container initialization", + ), ): """ Krack open a Krayt dragon! Create an inspector pod to explore what's inside your volumes. @@ -677,15 +629,36 @@ def create( The inspector will be created in the same namespace as the selected pod. """ # For create, we want to list all pods, not just Krayt pods + selected_namespace = None + selected_pod = None + typer.echo(namespace) + typer.echo(clone) + + if namespace is None and clone is not None and "/" in clone: + selected_namespace, selected_pod = clone.split("/", 1) + elif namespace is not None and clone is not None: + selected_namespace = namespace + selected_pod = clone + pods = get_pods(namespace, label_selector=None) if not pods: typer.echo("No pods found.") raise typer.Exit(1) - selected_pod, selected_namespace = fuzzy_select(pods) - if not selected_pod: - typer.echo("No pod selected.") - raise typer.Exit(1) + if selected_pod not in (p[0] for p in pods) or selected_pod is None: + if selected_pod is not None: + pods = [p for p in pods if selected_pod in p[0]] + if len(pods) == 1: + selected_pod, selected_namespace = pods[0] + else: + selected_pod, selected_namespace = fuzzy_select(pods) + if not selected_pod: + typer.echo("No pod selected.") + raise typer.Exit(1) + + typer.echo(f"Selected pod exists: {selected_pod in (p[0] for p in pods)}") + typer.echo(f"Selected pod: {selected_pod} ({selected_namespace})") + raise typer.Exit(1) pod_spec = get_pod_spec(selected_pod, selected_namespace) volume_mounts, volumes = get_pod_volumes_and_mounts(pod_spec) @@ -758,9 +731,19 @@ def logs( raise typer.Exit(1) +@app.command("list") +def list_pods(): + pods = get_pods() + if not pods: + typer.echo("No pods found.") + raise typer.Exit(1) + + for pod, namespace in pods: + typer.echo(f"{pod} ({namespace})") + + def main(): setup_environment() - load_init_scripts() app() diff --git a/krayt/cli/templates.py b/krayt/cli/templates.py index fe866c1..04a1843 100644 --- a/krayt/cli/templates.py +++ b/krayt/cli/templates.py @@ -2,7 +2,25 @@ from krayt.templates import env import typer from typing import List, Optional -app = typer.Typer() +# app = typer.Typer() +app = typer.Typer( + context_settings={ + "auto_envvar_prefix": "KRAYT", + "help_option_names": ["-h", "--help"], + "show_default": True, + "allow_interspersed_args": True, + "ignore_unknown_options": False, + "max_content_width": None, + "suggest_command": True, + } +) + + +@app.command() +def list(): + typer.echo("Available templates:") + for template in env.list_templates(): + typer.echo(template) @app.command() diff --git a/krayt/package.py b/krayt/package.py index d88d494..26f2a86 100644 --- a/krayt/package.py +++ b/krayt/package.py @@ -40,6 +40,7 @@ class Package(BaseModel): "uv", "i", "curlsh", + "curlbash", "brew", "cargo", "pipx", @@ -49,8 +50,10 @@ class Package(BaseModel): ], BeforeValidator(validate_kind), ] = "system" - dependencies: Optional[List[str]] = None value: str + dependencies: Optional[List["Package"]] = None + pre_install_hook: Optional[str] = None + post_install_hook: Optional[str] = None @classmethod def from_raw(cls, raw: str) -> "Package": @@ -67,52 +70,37 @@ class Package(BaseModel): def validate_dependencies(self) -> Self: if self.dependencies: return self - else: - if self.kind == "system": - return self - dependencies = [] - if self.kind in ["uv", "i", "installer", "curlbash", "curlsh", "gh"]: - dependencies.extend( - [ - Package.from_raw("curl"), - ] - ) - if self.kind == "brew": - dependencies.extend( - [ - Package.from_raw("brew"), - Package.from_raw("git"), - ] - ) - if self.kind == "cargo": - dependencies.extend( - [ - Package.from_raw("cargo"), - ] - ) - if self.kind == "pipx": - dependencies.extend( - [ - Package.from_raw("pipx"), - ] - ) - if self.kind == "npm": - dependencies.extend( - [ - Package.from_raw("npm"), - ] - ) - if self.kind == "go": - dependencies.extend( - [ - Package.from_raw("go"), - ] - ) - self.dependencies = dependencies - return self + dependencies = [] - def __str__(self): - return f"{self.kind}:{self.value}" if self.kind != "system" else self.value + if self.kind in ["uv", "i", "installer", "curlbash", "curlsh", "gh"]: + dependencies.append(Package.from_raw("curl")) + if self.kind == "brew": + dependencies.append(Package.from_raw("git")) + dependencies.append(Package.from_raw("curl")) + self.pre_install_hook = "NONINTERACTIVE=1" + self.post_install_hook = """ +# Setup Homebrew PATH +if [ -f /home/linuxbrew/.linuxbrew/bin/brew ]; then + eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv)" +elif [ -f /opt/homebrew/bin/brew ]; then + eval "$(/opt/homebrew/.linuxbrew/bin/brew shellenv)" +elif [ -f /usr/local/bin/brew ]; then + eval "$(/usr/local/bin/brew shellenv)" +else + echo "⚠️ Brew installed but binary location unknown." +fi +""" + if self.kind == "cargo": + dependencies.append(Package.from_raw("cargo")) + if self.kind == "pipx": + dependencies.append(Package.from_raw("pipx")) + if self.kind == "npm": + dependencies.append(Package.from_raw("npm")) + if self.kind == "go": + dependencies.append(Package.from_raw("go")) + + self.dependencies = dependencies + return self def is_system(self) -> bool: return self.kind == "system" @@ -121,29 +109,36 @@ class Package(BaseModel): """ Generate the bash install command snippet for this package. """ + cmd = "" if self.kind == "system": - return f"detect_package_manager_and_install {self.value}" + cmd = f"detect_package_manager_and_install {self.value}" elif self.kind == "uv": - return f"uv tool install {self.value}" + cmd = f"uv tool install {self.value}" elif self.kind in ["i", "installer", "gh"]: - return f"curl -fsSL https://i.jpillora.com/{self.value} | sh" + cmd = f"curl -fsSL https://i.jpillora.com/{self.value} | sh" elif self.kind == "curlsh": - return f"curl -fsSL {self.value} | sh" + cmd = f"curl -fsSL {self.value} | sh" elif self.kind == "curlbash": - return f"curl -fsSL {self.value} | bash" + cmd = f"curl -fsSL {self.value} | bash" elif self.kind == "brew": - return f"brew install {self.value}" + cmd = "curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh | bash" elif self.kind == "cargo": - return f"cargo install {self.value}" + cmd = f"cargo install {self.value}" elif self.kind == "pipx": - return f"pipx install {self.value}" + cmd = f"pipx install {self.value}" elif self.kind == "npm": - return f"npm install -g {self.value}" + cmd = f"npm install -g {self.value}" elif self.kind == "go": - return f"go install {self.value}@latest" + cmd = f"go install {self.value}@latest" else: raise ValueError(f"Unknown install method for kind={self.kind}") + # Add pre-install hook if necessary + if self.pre_install_hook: + return f"{self.pre_install_hook} {cmd}" + else: + return cmd + if __name__ == "__main__": raw_inputs = [ @@ -163,4 +158,11 @@ if __name__ == "__main__": [dependency.install_command() for dependency in package.dependencies] ) installs = [package.install_command() for package in packages] - print("\n".join(install for install in unique_everseen([*dependencies, *installs]))) + post_hooks = [] + for package in packages: + if package.post_install_hook: + post_hooks.append(package.post_install_hook.strip()) + + # Final full script + full_script = list(unique_everseen([*dependencies, *installs, *post_hooks])) + print("\n".join(full_script)) diff --git a/pyproject.toml b/pyproject.toml index cfeb2a1..ce11345 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,7 +28,8 @@ classifiers = [ dependencies = [ "typer", "kubernetes", - "InquirerPy", + "inquirerPy", + "inquirer", "jinja2", "iterfzf", "pydantic", From 0d913f7656cbb72d46e2acb41817b2394c056248 Mon Sep 17 00:00:00 2001 From: "Waylon S. Walker" Date: Thu, 10 Apr 2025 20:08:34 -0500 Subject: [PATCH 06/50] brew is gonna take some work --- krayt/package.py | 23 +---------------------- 1 file changed, 1 insertion(+), 22 deletions(-) diff --git a/krayt/package.py b/krayt/package.py index 26f2a86..2e78ab1 100644 --- a/krayt/package.py +++ b/krayt/package.py @@ -11,7 +11,6 @@ SUPPORTED_KINDS = { "i", "curlbash", "curlsh", - "brew", "cargo", "pipx", "npm", @@ -31,7 +30,7 @@ def validate_kind(v): class Package(BaseModel): """ Represents a package to be installed, either via system package manager - or an alternative installer like uv, installer.sh, brew, etc. + or an alternative installer like uv, installer.sh, etc. """ kind: Annotated[ @@ -41,7 +40,6 @@ class Package(BaseModel): "i", "curlsh", "curlbash", - "brew", "cargo", "pipx", "npm", @@ -74,22 +72,6 @@ class Package(BaseModel): if self.kind in ["uv", "i", "installer", "curlbash", "curlsh", "gh"]: dependencies.append(Package.from_raw("curl")) - if self.kind == "brew": - dependencies.append(Package.from_raw("git")) - dependencies.append(Package.from_raw("curl")) - self.pre_install_hook = "NONINTERACTIVE=1" - self.post_install_hook = """ -# Setup Homebrew PATH -if [ -f /home/linuxbrew/.linuxbrew/bin/brew ]; then - eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv)" -elif [ -f /opt/homebrew/bin/brew ]; then - eval "$(/opt/homebrew/.linuxbrew/bin/brew shellenv)" -elif [ -f /usr/local/bin/brew ]; then - eval "$(/usr/local/bin/brew shellenv)" -else - echo "⚠️ Brew installed but binary location unknown." -fi -""" if self.kind == "cargo": dependencies.append(Package.from_raw("cargo")) if self.kind == "pipx": @@ -120,8 +102,6 @@ fi cmd = f"curl -fsSL {self.value} | sh" elif self.kind == "curlbash": cmd = f"curl -fsSL {self.value} | bash" - elif self.kind == "brew": - cmd = "curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh | bash" elif self.kind == "cargo": cmd = f"cargo install {self.value}" elif self.kind == "pipx": @@ -147,7 +127,6 @@ if __name__ == "__main__": "uv:copier", "i:sharkdp/fd", "curlsh:https://example.com/install.sh", - "brew:bat", ] packages = [Package.from_raw(raw) for raw in raw_inputs] From 2899ee23ebbd677a74972129f3cc29d58fc8369d Mon Sep 17 00:00:00 2001 From: "Waylon S. Walker" Date: Fri, 11 Apr 2025 22:09:04 -0500 Subject: [PATCH 07/50] its working! --- .gitignore | 1 + krayt/bundles.py | 139 ++++++++--------- krayt/cli/__init__.py | 3 +- krayt/cli/pod.py | 37 +---- krayt/cli/templates.py | 17 +-- krayt/package.py | 152 ++++++++++++------- krayt/templates.py | 2 + krayt/templates/install.sh | 109 ++++++++------ test.sh | 254 +++++++++++++++++++++++++++---- test.yaml | 300 +++++++++++++++++++++++++------------ 10 files changed, 680 insertions(+), 334 deletions(-) diff --git a/.gitignore b/.gitignore index e1a3186..6f4e31a 100644 --- a/.gitignore +++ b/.gitignore @@ -962,3 +962,4 @@ 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 +*.null-ls* diff --git a/krayt/bundles.py b/krayt/bundles.py index a375140..cecc8d9 100644 --- a/krayt/bundles.py +++ b/krayt/bundles.py @@ -10,78 +10,79 @@ basics = [ "bash", "coreutils", ] -pretty = [ - *basics, - "starship", - "atuin", - "bash", - "zsh", - "fish", - "bat", - "eza", -] -networking = [ - *basics, - "mtr", - "bind-tools", - "aws-cli", - "curl", - "wget", - "iperf3", - "nmap", - "traceroute", - "netcat-openbsd", -] +bundles = { + "basics": [ + *basics, + ], + "pretty": [ + *basics, + "starship", + "atuin", + "bash", + "zsh", + "fish", + "bat", + "eza", + ], + "networking": [ + *basics, + "mtr", + "bind-tools", + "aws-cli", + "curl", + "wget", + "iperf3", + "nmap", + "traceroute", + "netcat-openbsd", + ], + "database": [ + *basics, + "sqlite", + "sqlite-dev", + "sqlite-libs", + "postgresql", + "mysql", + "mariadb", + "redis", + "mongodb", + ], + "storage": [ + *basics, + "ncdu", + "dust", + "file", + "hexyl", + "ripgrep", + "fd", + "fzf", + "difftastic", + ], + "search": [ + *basics, + "ripgrep", + "fd", + "fzf", + "difftastic", + ], + "monitoring": [ + *basics, + "htop", + "bottom", + "mtr", + ], +} -database = [ - *basics, - "sqlite", - "sqlite-dev", - "sqlite-libs", - "postgresql", - "mysql", - "mariadb", - "redis", - "mongodb", -] - -storage = [ - *basics, - "ncdu", - "dust", - "file", - "hexyl", - "ripgrep", - "fd", - "fzf", - "difftastic", -] - -search = [ - *basics, - "ripgrep", - "fd", - "fzf", - "difftastic", -] - -monitoring = [ - *basics, - "htop", - "bottom", - "mtr", -] - -all = list( +bundles["all"] = list( set( [ - *basics, - *pretty, - *networking, - *database, - *storage, - *search, - *monitoring, + *bundles["basics"], + *bundles["pretty"], + *bundles["networking"], + *bundles["database"], + *bundles["storage"], + *bundles["search"], + *bundles["monitoring"], ] ) ) diff --git a/krayt/cli/__init__.py b/krayt/cli/__init__.py index 06e5c40..d5c6f0c 100644 --- a/krayt/cli/__init__.py +++ b/krayt/cli/__init__.py @@ -1,6 +1,6 @@ from krayt import __version__ from krayt.cli.bundles import app as bundles_app -from krayt.cli.pod import app as pod_app, create, exec, logs +from krayt.cli.pod import app as pod_app, create, exec, logs, clean from krayt.cli.templates import app as templates_app from typer import Typer @@ -10,6 +10,7 @@ app.add_typer(templates_app, name="templates", no_args_is_help=True) app.add_typer(pod_app, name="pod", no_args_is_help=True) app.command(name="create")(create) app.command(name="c")(create) +app.command(name="clean")(clean) app.command(name="exec")(exec) app.command(name="logs")(logs) app.add_typer(bundles_app, name="bundles", no_args_is_help=True) diff --git a/krayt/cli/pod.py b/krayt/cli/pod.py index 93a9a80..8190073 100644 --- a/krayt/cli/pod.py +++ b/krayt/cli/pod.py @@ -296,31 +296,6 @@ def create_inspector_job( 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 @@ -631,8 +606,6 @@ def create( # For create, we want to list all pods, not just Krayt pods selected_namespace = None selected_pod = None - typer.echo(namespace) - typer.echo(clone) if namespace is None and clone is not None and "/" in clone: selected_namespace, selected_pod = clone.split("/", 1) @@ -656,9 +629,8 @@ def create( typer.echo("No pod selected.") raise typer.Exit(1) - typer.echo(f"Selected pod exists: {selected_pod in (p[0] for p in pods)}") - typer.echo(f"Selected pod: {selected_pod} ({selected_namespace})") - raise typer.Exit(1) + # typer.echo(f"Selected pod exists: {selected_pod in (p[0] for p in pods)}") + # typer.echo(f"Selected pod: {selected_pod} ({selected_namespace})") pod_spec = get_pod_spec(selected_pod, selected_namespace) volume_mounts, volumes = get_pod_volumes_and_mounts(pod_spec) @@ -671,6 +643,11 @@ def create( volumes, image=image, imagepullsecret=imagepullsecret, + 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, ) # Output the job manifest diff --git a/krayt/cli/templates.py b/krayt/cli/templates.py index 04a1843..5a0c361 100644 --- a/krayt/cli/templates.py +++ b/krayt/cli/templates.py @@ -2,18 +2,7 @@ from krayt.templates import env import typer from typing import List, Optional -# app = typer.Typer() -app = typer.Typer( - context_settings={ - "auto_envvar_prefix": "KRAYT", - "help_option_names": ["-h", "--help"], - "show_default": True, - "allow_interspersed_args": True, - "ignore_unknown_options": False, - "max_content_width": None, - "suggest_command": True, - } -) +app = typer.Typer() @app.command() @@ -103,6 +92,8 @@ def motd( template_name = "motd.sh" template = env.get_template(template_name) rendered = template.render( - volumes=volumes, pvcs=pvcs, additional_packages=additional_packages + volumes=volumes, + pvcs=pvcs, + additional_packages=additional_packages, ) print(rendered) diff --git a/krayt/package.py b/krayt/package.py index 2e78ab1..907a412 100644 --- a/krayt/package.py +++ b/krayt/package.py @@ -1,7 +1,7 @@ +from krayt.bundles import bundles from more_itertools import unique_everseen -from pydantic import BaseModel, BeforeValidator, model_validator -from typing import Annotated, List, Literal, Optional -from typing_extensions import Self +from pydantic import BaseModel, BeforeValidator +from typing import Annotated, List, Literal, Optional, Union SUPPORTED_KINDS = { @@ -16,6 +16,26 @@ SUPPORTED_KINDS = { "npm", "go", "gh", + "group", + "bundle", +} + +DEPENDENCIES = { + "uv": [ + "curl", + "curlsh:https://astral.sh/uv/install.sh", + ], + "installer": [ + "curl", + ], + "i": ["curl"], + "curlbash": ["curl"], + "curlsh": ["curl"], + "cargo": ["cargo"], + "pipx": ["pipx"], + "npm": ["npm"], + "go": ["go"], + "gh": ["gh"], } @@ -34,22 +54,11 @@ class Package(BaseModel): """ kind: Annotated[ - Literal[ - "system", - "uv", - "i", - "curlsh", - "curlbash", - "cargo", - "pipx", - "npm", - "go", - "gh", - ], + Literal[*SUPPORTED_KINDS], BeforeValidator(validate_kind), ] = "system" value: str - dependencies: Optional[List["Package"]] = None + # dependencies: Optional[List["Package"]] = None pre_install_hook: Optional[str] = None post_install_hook: Optional[str] = None @@ -64,26 +73,29 @@ class Package(BaseModel): else: return cls(kind="system", value=raw.strip()) - @model_validator(mode="after") - def validate_dependencies(self) -> Self: - if self.dependencies: - return self - dependencies = [] - - if self.kind in ["uv", "i", "installer", "curlbash", "curlsh", "gh"]: - dependencies.append(Package.from_raw("curl")) - if self.kind == "cargo": - dependencies.append(Package.from_raw("cargo")) - if self.kind == "pipx": - dependencies.append(Package.from_raw("pipx")) - if self.kind == "npm": - dependencies.append(Package.from_raw("npm")) - if self.kind == "go": - dependencies.append(Package.from_raw("go")) - - self.dependencies = dependencies - return self - + # @model_validator(mode="after") + # def validate_dependencies(self) -> Self: + # if self.dependencies: + # return self + # dependencies = [] + # + # if self.kind in ["uv", "i", "installer", "curlbash", "curlsh", "gh"]: + # dependencies.append(Package.from_raw("curl")) + # dependencies.append( + # Package.from_raw("curlsh:https://astral.sh/uv/install.sh") + # ) + # if self.kind == "cargo": + # dependencies.append(Package.from_raw("cargo")) + # if self.kind == "pipx": + # dependencies.append(Package.from_raw("pipx")) + # if self.kind == "npm": + # dependencies.append(Package.from_raw("npm")) + # if self.kind == "go": + # dependencies.append(Package.from_raw("go")) + # + # self.dependencies = dependencies + # return self + # def is_system(self) -> bool: return self.kind == "system" @@ -92,12 +104,14 @@ class Package(BaseModel): Generate the bash install command snippet for this package. """ cmd = "" - if self.kind == "system": + if self.kind in ["bundle", "group"]: + cmd = "" + elif self.kind == "system": cmd = f"detect_package_manager_and_install {self.value}" elif self.kind == "uv": cmd = f"uv tool install {self.value}" elif self.kind in ["i", "installer", "gh"]: - cmd = f"curl -fsSL https://i.jpillora.com/{self.value} | sh" + cmd = f"installer {self.value}" elif self.kind == "curlsh": cmd = f"curl -fsSL {self.value} | sh" elif self.kind == "curlbash": @@ -120,28 +134,58 @@ class Package(BaseModel): return cmd -if __name__ == "__main__": - raw_inputs = [ - "curl", - "wget", - "uv:copier", - "i:sharkdp/fd", - "curlsh:https://example.com/install.sh", - ] - - packages = [Package.from_raw(raw) for raw in raw_inputs] - dependencies = [] +def get_install_script(packages: Union[str, List[str]]) -> str: + if packages is None: + return [] + if isinstance(packages, str): + packages = [packages] + bundled_packages = [] for package in packages: - if package.dependencies: - dependencies.extend( - [dependency.install_command() for dependency in package.dependencies] - ) + if package.startswith("bundle:") or package.startswith("group:"): + _package = package.split(":")[1].strip() + bundled_packages.extend(bundles.get(_package, [])) + packages = list(unique_everseen([*bundled_packages, *packages])) + + packages = [Package.from_raw(raw) for raw in packages] + kinds_used = [package.kind for package in packages] + dependencies = [] + for kind in kinds_used: + dependencies.extend(DEPENDENCIES.get(kind, [])) + dependencies = list( + unique_everseen( + [Package.from_raw(raw).install_command() for raw in dependencies] + ) + ) + # for package in packages: + # if package.dependencies: + # dependencies.extend( + # [dependency.install_command() for dependency in package.dependencies] + # ) installs = [package.install_command() for package in packages] post_hooks = [] for package in packages: if package.post_install_hook: post_hooks.append(package.post_install_hook.strip()) + pre_hooks = [] + for package in packages: + if package.pre_install_hook: + pre_hooks.append(package.pre_install_hook.strip()) # Final full script - full_script = list(unique_everseen([*dependencies, *installs, *post_hooks])) + full_script = list( + unique_everseen([*pre_hooks, *dependencies, *installs, *post_hooks]) + ) + return "\n".join(full_script) if full_script else full_script + + +if __name__ == "__main__": + raw_inputs = [ + "bundle:storage", + "wget", + "uv:copier", + "i:sharkdp/fd", + "curlsh:https://example.com/install.sh", + ] + full_script = get_install_script(raw_inputs) + print("\n".join(full_script)) diff --git a/krayt/templates.py b/krayt/templates.py index 53d902f..7b3ba95 100644 --- a/krayt/templates.py +++ b/krayt/templates.py @@ -1,4 +1,5 @@ from jinja2 import Environment, FileSystemLoader +from krayt.package import get_install_script from pathlib import Path # Get the two template directories @@ -9,3 +10,4 @@ template_dirs = [ # Create the Jinja environment env = Environment(loader=FileSystemLoader([str(path) for path in template_dirs])) +env.globals["get_install_script"] = get_install_script diff --git a/krayt/templates/install.sh b/krayt/templates/install.sh index 8e0c8d9..de056ad 100644 --- a/krayt/templates/install.sh +++ b/krayt/templates/install.sh @@ -1,65 +1,88 @@ {% if additional_packages %} +# Detect package manager +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." + exit 2 +fi + +echo "Using package manager: $PKG_MANAGER" + +# Run update once if needed +if [ -n "$UPDATE_CMD" ]; then + echo "Running package manager update..." + eval "$UPDATE_CMD" +fi + detect_package_manager_and_install() { if [ $# -eq 0 ]; then echo "Usage: detect_package_manager_and_install [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 - - echo "Using package manager: $PKG_MANAGER" - - if [ -n "$UPDATE_CMD" ]; then - echo "Running package manager update..." - eval "$UPDATE_CMD" - fi - - FAILED_PKGS=() + FAILED_PKGS="" for pkg in "$@"; do echo "Installing package: $pkg" - if ! eval "$INSTALL_CMD $pkg"; then + if ! $INSTALL_CMD $pkg; then echo "⚠️ Warning: Failed to install package: $pkg" - FAILED_PKGS+=("$pkg") + FAILED_PKGS="$FAILED_PKGS $pkg" fi done - - if [ ${#FAILED_PKGS[@]} -ne 0 ]; then + {% raw %} + if [ -n "$FAILED_PKGS" ]; then echo "⚠️ The following packages failed to install:" - for failed_pkg in "${FAILED_PKGS[@]}"; do + for failed_pkg in $FAILED_PKGS; do echo " - $failed_pkg" done else echo "✅ All requested packages installed successfully." fi + {% endraw %} } -detect_package_manager_and_install {% for package in additional_packages %}{{ package | trim }}{% if not loop.last %} {% endif %}{% endfor %} +installer() { + if [ $# -eq 0 ]; then + echo "Usage: installer [package2] [...]" + return 1 + fi + + for pkg in "$@"; do + echo "Installing package with installer: $pkg" + ( + orig_dir="$(pwd)" + cd /usr/local/bin || exit 1 + curl -fsSL https://i.jpillora.com/${pkg} | sh + cd "$orig_dir" || exit 1 + ) + done +} {% endif %} + +{% if additional_packages %} +{{ get_install_script(additional_packages) | safe }} +{% endif %} + diff --git a/test.sh b/test.sh index 117fb41..1643cab 100644 --- a/test.sh +++ b/test.sh @@ -1,51 +1,247 @@ -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] [...]" +mkdir -p /etc/krayt +cat <<'KRAYT_INIT_SH_EOF' >/etc/krayt/init.sh +detect_package_manager_and_install() { + if [ $# -eq 0 ]; then + echo "Usage: detect_package_manager_and_install [package2] [...]" return 1 fi - if command -v apt &>/dev/null; then + if command -v apt >/dev/null 2>&1; then PKG_MANAGER="apt" - UPDATE_CMD="sudo apt update" - INSTALL_CMD="sudo apt install -y" - elif command -v dnf &>/dev/null; then + 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="sudo dnf install -y" - elif command -v yum &>/dev/null; then + INSTALL_CMD="dnf install -y" + elif command -v yum >/dev/null 2>&1; then PKG_MANAGER="yum" UPDATE_CMD="" - INSTALL_CMD="sudo yum install -y" - elif command -v pacman &>/dev/null; then + INSTALL_CMD="yum install -y" + elif command -v pacman >/dev/null 2>&1; then PKG_MANAGER="pacman" UPDATE_CMD="" - INSTALL_CMD="sudo pacman -Sy --noconfirm" - elif command -v zypper &>/dev/null; then + INSTALL_CMD="pacman -Sy --noconfirm" + elif command -v zypper >/dev/null 2>&1; then PKG_MANAGER="zypper" UPDATE_CMD="" - INSTALL_CMD="sudo zypper install -y" - elif command -v apk &>/dev/null; then + INSTALL_CMD="zypper install -y" + elif command -v apk >/dev/null 2>&1; then PKG_MANAGER="apk" UPDATE_CMD="" - INSTALL_CMD="sudo apk add" + INSTALL_CMD="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[*]} + echo "Using package manager: $PKG_MANAGER" + + if [ -n "$UPDATE_CMD" ]; then + echo "Running package manager update..." + eval "$UPDATE_CMD" fi + + FAILED_PKGS="" + + for pkg in "$@"; do + echo "Installing package: $pkg" + if ! eval "$INSTALL_CMD $pkg"; then + echo "⚠️ Warning: Failed to install package: $pkg" + FAILED_PKGS="$FAILED_PKGS $pkg" + fi + done + + if [ -n "$FAILED_PKGS" ]; then + echo "⚠️ The following packages failed to install:" + for failed_pkg in $FAILED_PKGS; do + echo " - $failed_pkg" + done + else + echo "✅ All requested packages installed successfully." + fi + } -detect_package_manager_and_install_command git htop +installer() { + if [ $# -eq 0 ]; then + echo "Usage: installer [package2] [...]" + return 1 + fi + + for pkg in "$@"; do + echo "Installing package with installer: $pkg" + ( + orig_dir="$(pwd)" + cd /usr/local/bin || exit 1 + curl -fsSL https://i.jpillora.com/${pkg} | sh + cd "$orig_dir" || exit 1 + ) + done +} + + + +detect_package_manager_and_install eza +detect_package_manager_and_install hexyl +detect_package_manager_and_install mariadb +detect_package_manager_and_install coreutils +detect_package_manager_and_install ncdu +detect_package_manager_and_install postgresql +detect_package_manager_and_install atuin +detect_package_manager_and_install redis +detect_package_manager_and_install file +detect_package_manager_and_install netcat-openbsd +detect_package_manager_and_install traceroute +detect_package_manager_and_install fd +detect_package_manager_and_install iperf3 +detect_package_manager_and_install aws-cli +detect_package_manager_and_install dust +detect_package_manager_and_install sqlite-dev +detect_package_manager_and_install fish +detect_package_manager_and_install bat +detect_package_manager_and_install ripgrep +detect_package_manager_and_install difftastic +detect_package_manager_and_install zsh +detect_package_manager_and_install sqlite-libs +detect_package_manager_and_install bind-tools +detect_package_manager_and_install nmap +detect_package_manager_and_install mysql +detect_package_manager_and_install htop +detect_package_manager_and_install sqlite +detect_package_manager_and_install fzf +detect_package_manager_and_install bottom +detect_package_manager_and_install wget +detect_package_manager_and_install mtr +detect_package_manager_and_install bash +detect_package_manager_and_install curl +detect_package_manager_and_install starship +detect_package_manager_and_install mongodb +detect_package_manager_and_install jq +detect_package_manager_and_install yq + + + +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." + +Additional Packages: +- bundle:all + +EOF +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 +export KRAYT_ADDITIONAL_PACKAGES="bundle:all" +# 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 +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/test.yaml b/test.yaml index 194d69b..b2b19a0 100644 --- a/test.yaml +++ b/test.yaml @@ -1,96 +1,206 @@ -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 +mkdir -p /etc/krayt +cat <<'KRAYT_INIT_SH_EOF' >/etc/krayt/init.sh +detect_package_manager_and_install() { + if [ $# -eq 0 ]; then + echo "Usage: detect_package_manager_and_install [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 + + echo "Using package manager: $PKG_MANAGER" + + if [ -n "$UPDATE_CMD" ]; then + echo "Running package manager update..." + eval "$UPDATE_CMD" + fi + + FAILED_PKGS=() + + for pkg in "$@"; do + echo "Installing package: $pkg" + if ! eval "$INSTALL_CMD $pkg"; then + echo "⚠️ Warning: Failed to install package: $pkg" + FAILED_PKGS+=("$pkg") + fi + done + + if [ ${#FAILED_PKGS[@]} -ne 0 ]; then + echo "⚠️ The following packages failed to install:" + for failed_pkg in "${FAILED_PKGS[@]}"; do + echo " - $failed_pkg" + done + else + echo "✅ All requested packages installed successfully." + fi + +} + + + + +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." + +Mounted Volumes: +- hi + +Persistent Volume Claims: +- hi +- hello + +Additional Packages: +- htop +- ripgrep +- uv:copier + +EOF +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 +export KRAYT_PVCS="hi hello" + +export KRAYT_VOLUMES="hi" + +export KRAYT_ADDITIONAL_PACKAGES="htop ripgrep uv:copier" +# 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 +touch here.txt + +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 From 47e7f1cb5ec80c9a92fb6030eb3b7c054641183f Mon Sep 17 00:00:00 2001 From: "Waylon S. Walker" Date: Thu, 17 Apr 2025 09:09:40 -0500 Subject: [PATCH 08/50] implement port forward --- krayt/cli/pod.py | 91 +++++++++++++++++++++++++++++++++++------------- 1 file changed, 66 insertions(+), 25 deletions(-) diff --git a/krayt/cli/pod.py b/krayt/cli/pod.py index 8190073..db84920 100644 --- a/krayt/cli/pod.py +++ b/krayt/cli/pod.py @@ -388,17 +388,7 @@ def version_callback(value: bool): 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. - """ +def get_pod(namespace: Optional[str] = None): config.load_kube_config() batch_api = client.BatchV1Api() @@ -438,25 +428,76 @@ def exec( 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) + return pod_name, pod_namespace + + +@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. + """ + + pod_name, pod_namespace = get_pod(namespace) + exec_command = [ + "kubectl", + "exec", + "-it", + "-n", + pod_namespace, + pod_name, + "--", + "/bin/bash", + "-l", + ] + + os.execvp("kubectl", exec_command) + + +@app.command() +def port_forward( + namespace: Optional[str] = typer.Option( + None, + help="Kubernetes namespace. If not specified, will search for inspectors across all namespaces.", + ), + port: str = typer.Option( + "8080:8080", + "--port", + "-p", + help="Port to forward to the inspector pod", + ), +): + """ + 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. + """ + if ":" not in port: + # if port does not contain a ":" it should be an int + port = int(port) + port = f"{port}:{port}" + + pod_name, pod_namespace = get_pod(namespace) + port_forward_command = [ + "kubectl", + "port-forward", + "-n", + pod_namespace, + pod_name, + port, + ] + + os.execvp("kubectl", port_forward_command) + @app.command() def clean( From e8686a2c66fda6e6153c4d106a03eedcc7d5a0f6 Mon Sep 17 00:00:00 2001 From: "Waylon S. Walker" Date: Thu, 17 Apr 2025 09:19:16 -0500 Subject: [PATCH 09/50] lint --- krayt/cli/pod.py | 6 ++++-- krayt/cli/templates.py | 3 +-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/krayt/cli/pod.py b/krayt/cli/pod.py index db84920..591a98e 100644 --- a/krayt/cli/pod.py +++ b/krayt/cli/pod.py @@ -7,6 +7,8 @@ import time import typer from typing import Any, List, Optional import yaml +from krayt.__about__ import __version__ + logging.basicConfig(level=logging.WARNING) @@ -384,7 +386,7 @@ def setup_environment(): def version_callback(value: bool): if value: - typer.echo(f"Version: {KRAYT_VERSION}") + typer.echo(f"Version: {__version__}") raise typer.Exit() @@ -698,7 +700,7 @@ def create( @app.command() def version(): """Show the version of Krayt.""" - typer.echo(f"Version: {KRAYT_VERSION}") + typer.echo(f"Version: {__version__}") @app.command() diff --git a/krayt/cli/templates.py b/krayt/cli/templates.py index 5a0c361..841cd54 100644 --- a/krayt/cli/templates.py +++ b/krayt/cli/templates.py @@ -69,9 +69,8 @@ def install( ), ): template_name = "install.sh" - breakpoint() template = env.get_template(template_name) - rendered = template.render(packages=packages) + rendered = template.render(additional_packages=additional_packages) print(rendered) From a6ecb8240c38a5cc7e307de0729df49eed6febef Mon Sep 17 00:00:00 2001 From: "Waylon S. Walker" Date: Thu, 17 Apr 2025 09:36:57 -0500 Subject: [PATCH 10/50] add lint-format --- pyproject.toml | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index ce11345..c2ec4f8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,6 +7,8 @@ build-backend = "hatchling.build" [tool.hatch.build.targets.sdist] exclude = ["/.github"] +[tool.hatch.build.targets.binary] + [project] name = "krayt" dynamic = ["version"] @@ -54,3 +56,14 @@ path = "krayt/__about__.py" [project.scripts] krayt = "krayt.cli:app" + +[tool.hatch.envs.default] +dependencies = [ + "ruff", + "pyinstrument", +] + +[tool.hatch.envs.default.scripts] +lint = "ruff check krayt" +format = "ruff format krayt" +lint-format = ['lint', 'format'] From 61fbec4e319e77b46e6899e88ecc7d62b67682f1 Mon Sep 17 00:00:00 2001 From: "Waylon S. Walker" Date: Thu, 17 Apr 2025 09:37:14 -0500 Subject: [PATCH 11/50] bump version --- krayt/__about__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/krayt/__about__.py b/krayt/__about__.py index 6c8e6b9..493f741 100644 --- a/krayt/__about__.py +++ b/krayt/__about__.py @@ -1 +1 @@ -__version__ = "0.0.0" +__version__ = "0.3.0" From 301b628d39344a6a6b2989ff59e95341894c68a8 Mon Sep 17 00:00:00 2001 From: "Waylon S. Walker" Date: Thu, 17 Apr 2025 09:37:32 -0500 Subject: [PATCH 12/50] add release workflow --- .github/workflows/release-pypi.yaml | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 .github/workflows/release-pypi.yaml diff --git a/.github/workflows/release-pypi.yaml b/.github/workflows/release-pypi.yaml new file mode 100644 index 0000000..e69de29 From 74fba4f23eaf4234b2d3543388c00cdc08e8ad40 Mon Sep 17 00:00:00 2001 From: "Waylon S. Walker" Date: Thu, 17 Apr 2025 09:41:18 -0500 Subject: [PATCH 13/50] add content --- .github/workflows/release-pypi.yaml | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/.github/workflows/release-pypi.yaml b/.github/workflows/release-pypi.yaml index e69de29..b5d156f 100644 --- a/.github/workflows/release-pypi.yaml +++ b/.github/workflows/release-pypi.yaml @@ -0,0 +1,21 @@ +name: Release Krayt +on: + workflow_dispatch: + push: + paths: + - "krayt/**" + - "pyproject.toml" +env: + HATCH_INDEX_USER: __token__ + HATCH_INDEX_AUTH: ${{ secrets.pypi_password }} +jobs: + release-markata: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: waylonwalker/hatch-action@v4 + with: + before-command: "lint-format" + env: + # required for gh release + GH_TOKEN: ${{ github.token }} From 9b6efdc7fe1150ad7e990fe7cbd67a5913a51f65 Mon Sep 17 00:00:00 2001 From: "Waylon S. Walker" Date: Thu, 17 Apr 2025 09:45:04 -0500 Subject: [PATCH 14/50] rename job --- .github/workflows/release-pypi.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release-pypi.yaml b/.github/workflows/release-pypi.yaml index b5d156f..a676db3 100644 --- a/.github/workflows/release-pypi.yaml +++ b/.github/workflows/release-pypi.yaml @@ -9,7 +9,7 @@ env: HATCH_INDEX_USER: __token__ HATCH_INDEX_AUTH: ${{ secrets.pypi_password }} jobs: - release-markata: + release-krayt: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 From 96788a412b54ee220d0cdf952f73b68c9f56c6df Mon Sep 17 00:00:00 2001 From: "Waylon S. Walker" Date: Thu, 17 Apr 2025 09:45:39 -0500 Subject: [PATCH 15/50] bump for build --- krayt/__about__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/krayt/__about__.py b/krayt/__about__.py index 493f741..7380a2a 100644 --- a/krayt/__about__.py +++ b/krayt/__about__.py @@ -1 +1 @@ -__version__ = "0.3.0" +__version__ = "0.3.0b0" From a9669afff8f5f6f15405b159d46cb39a4a9d4a1d Mon Sep 17 00:00:00 2001 From: "Waylon S. Walker" Date: Thu, 17 Apr 2025 09:47:50 -0500 Subject: [PATCH 16/50] give write permissions --- .github/workflows/release-pypi.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/release-pypi.yaml b/.github/workflows/release-pypi.yaml index a676db3..9941b78 100644 --- a/.github/workflows/release-pypi.yaml +++ b/.github/workflows/release-pypi.yaml @@ -5,6 +5,8 @@ on: paths: - "krayt/**" - "pyproject.toml" +permissions: + contents: write env: HATCH_INDEX_USER: __token__ HATCH_INDEX_AUTH: ${{ secrets.pypi_password }} From 575c70ee9fb5ef788a65efc0744523f8783d767a Mon Sep 17 00:00:00 2001 From: "Waylon S. Walker" Date: Thu, 17 Apr 2025 09:49:02 -0500 Subject: [PATCH 17/50] bump for build --- krayt/__about__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/krayt/__about__.py b/krayt/__about__.py index 7380a2a..417e2e8 100644 --- a/krayt/__about__.py +++ b/krayt/__about__.py @@ -1 +1 @@ -__version__ = "0.3.0b0" +__version__ = "0.3.0b1" From caa07ac771507a033f92754ad8d147c154d873f5 Mon Sep 17 00:00:00 2001 From: WaylonWalker Date: Thu, 17 Apr 2025 14:49:58 +0000 Subject: [PATCH 18/50] =?UTF-8?q?Bump=20version:=200.3.0b1=20=E2=86=92=200?= =?UTF-8?q?.3.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- krayt/__about__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/krayt/__about__.py b/krayt/__about__.py index 417e2e8..493f741 100644 --- a/krayt/__about__.py +++ b/krayt/__about__.py @@ -1 +1 @@ -__version__ = "0.3.0b1" +__version__ = "0.3.0" From 2e4eb8ee5f613b0befb2967e04db8b9201c8b694 Mon Sep 17 00:00:00 2001 From: "Waylon S. Walker" Date: Thu, 17 Apr 2025 09:58:11 -0500 Subject: [PATCH 19/50] update workflow permissions --- .github/workflows/release-pypi.yaml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/release-pypi.yaml b/.github/workflows/release-pypi.yaml index 9941b78..52ceb50 100644 --- a/.github/workflows/release-pypi.yaml +++ b/.github/workflows/release-pypi.yaml @@ -7,6 +7,10 @@ on: - "pyproject.toml" permissions: contents: write + pull-requests: write + issues: read + packages: none + id-token: write env: HATCH_INDEX_USER: __token__ HATCH_INDEX_AUTH: ${{ secrets.pypi_password }} From 0bd055f881d794493b81948df8de74587e413cd6 Mon Sep 17 00:00:00 2001 From: "Waylon S. Walker" Date: Thu, 17 Apr 2025 10:00:36 -0500 Subject: [PATCH 20/50] bump for build --- justfile | 24 ++++++++++++++++++++++++ krayt/__about__.py | 2 +- 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/justfile b/justfile index 938d946..8b9c260 100644 --- a/justfile +++ b/justfile @@ -70,3 +70,27 @@ preview-release-notes: ./scripts/get_release_notes.py "$VERSION" | less -R release: create-release + +build-pyapp: + export PYAPP_PROJECT_NAME=krayt + export PYAPP_PROJECT_VERSION=`hatch version` + export PYAPP_DISTRIBUTION_SOURCE=~/git/krayt/dist/krayt-${PYAPP_PROJECT_VERSION}.tar.gz + export PYAPP_DISTRIBUTION_EMBED=true + + + echo "linting" + hatch run lint-format + + echo "Building pyapp" + hatch build + + echo "Uploading pyapp" + hatch publish + + cd ~/git/pyapp + cargo build --release --quiet + + + echo "Done" + + diff --git a/krayt/__about__.py b/krayt/__about__.py index 493f741..7380a2a 100644 --- a/krayt/__about__.py +++ b/krayt/__about__.py @@ -1 +1 @@ -__version__ = "0.3.0" +__version__ = "0.3.0b0" From 09f68faded75690ccf99f319ca72d43ff3375fb1 Mon Sep 17 00:00:00 2001 From: WaylonWalker Date: Thu, 17 Apr 2025 15:01:32 +0000 Subject: [PATCH 21/50] =?UTF-8?q?Bump=20version:=200.3.0b0=20=E2=86=92=200?= =?UTF-8?q?.3.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- krayt/__about__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/krayt/__about__.py b/krayt/__about__.py index 7380a2a..493f741 100644 --- a/krayt/__about__.py +++ b/krayt/__about__.py @@ -1 +1 @@ -__version__ = "0.3.0b0" +__version__ = "0.3.0" From b3500c917099f2ae4c84e068686da6e2921f85e0 Mon Sep 17 00:00:00 2001 From: "Waylon S. Walker" Date: Thu, 17 Apr 2025 10:15:01 -0500 Subject: [PATCH 22/50] remove pypi env var --- .github/workflows/release-pypi.yaml | 3 --- krayt/__about__.py | 2 +- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/.github/workflows/release-pypi.yaml b/.github/workflows/release-pypi.yaml index 52ceb50..bd6c6ab 100644 --- a/.github/workflows/release-pypi.yaml +++ b/.github/workflows/release-pypi.yaml @@ -11,9 +11,6 @@ permissions: issues: read packages: none id-token: write -env: - HATCH_INDEX_USER: __token__ - HATCH_INDEX_AUTH: ${{ secrets.pypi_password }} jobs: release-krayt: runs-on: ubuntu-latest diff --git a/krayt/__about__.py b/krayt/__about__.py index 493f741..7380a2a 100644 --- a/krayt/__about__.py +++ b/krayt/__about__.py @@ -1 +1 @@ -__version__ = "0.3.0" +__version__ = "0.3.0b0" From 10f7f61cec222f2bb2032cf855878330ec7eecec Mon Sep 17 00:00:00 2001 From: WaylonWalker Date: Thu, 17 Apr 2025 15:16:00 +0000 Subject: [PATCH 23/50] =?UTF-8?q?Bump=20version:=200.3.0b0=20=E2=86=92=200?= =?UTF-8?q?.3.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- krayt/__about__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/krayt/__about__.py b/krayt/__about__.py index 7380a2a..493f741 100644 --- a/krayt/__about__.py +++ b/krayt/__about__.py @@ -1 +1 @@ -__version__ = "0.3.0b0" +__version__ = "0.3.0" From 2f049f1f99d89eeb325c68ddf6ff12a3a10a4da8 Mon Sep 17 00:00:00 2001 From: "Waylon S. Walker" Date: Thu, 17 Apr 2025 11:12:39 -0500 Subject: [PATCH 24/50] release 0.3.0 --- krayt/__about__.py | 2 +- krayt/cli/pod.py | 2 +- pyproject.toml | 1 + 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/krayt/__about__.py b/krayt/__about__.py index 493f741..7380a2a 100644 --- a/krayt/__about__.py +++ b/krayt/__about__.py @@ -1 +1 @@ -__version__ = "0.3.0" +__version__ = "0.3.0b0" diff --git a/krayt/cli/pod.py b/krayt/cli/pod.py index 591a98e..6cf9aa3 100644 --- a/krayt/cli/pod.py +++ b/krayt/cli/pod.py @@ -98,7 +98,7 @@ def fuzzy_select(items): selected = iterfzf.iterfzf( formatted_items, prompt="Select a pod to clone:", - preview='''kubectl describe pod "$(echo {} | awk -F'[(|)]' '{gsub(/\x1b\[[0-9;]*m/, "", $1); print $1}' | xargs)" -n "$(echo {} | awk -F'[(|)]' '{gsub(/\x1b\[[0-9;]*m/, "", $2); print $2}' | xargs)"''', + # preview='''kubectl describe pod "$(echo {} | awk -F'[(|)]' '{gsub(/\x1b\[[0-9;]*m/, "", $1); print $1}' | xargs)" -n "$(echo {} | awk -F'[(|)]' '{gsub(/\x1b\[[0-9;]*m/, "", $2); print $2}' | xargs)"''', ) if not selected: return None, None diff --git a/pyproject.toml b/pyproject.toml index c2ec4f8..5ef96cd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -35,6 +35,7 @@ dependencies = [ "jinja2", "iterfzf", "pydantic", + "more-itertools", ] [[project.authors]] From 0025cea94776e852017e2ab71b686f4e4e187869 Mon Sep 17 00:00:00 2001 From: WaylonWalker Date: Thu, 17 Apr 2025 16:13:34 +0000 Subject: [PATCH 25/50] =?UTF-8?q?Bump=20version:=200.3.0b0=20=E2=86=92=200?= =?UTF-8?q?.3.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- krayt/__about__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/krayt/__about__.py b/krayt/__about__.py index 7380a2a..493f741 100644 --- a/krayt/__about__.py +++ b/krayt/__about__.py @@ -1 +1 @@ -__version__ = "0.3.0b0" +__version__ = "0.3.0" From 1959e1a39b94fd349e64a54f47a5ac1c41f9c2b0 Mon Sep 17 00:00:00 2001 From: "Waylon S. Walker" Date: Fri, 18 Apr 2025 13:36:05 -0500 Subject: [PATCH 26/50] replace nvim-manager with krayt --- scripts/install.sh.template | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/scripts/install.sh.template b/scripts/install.sh.template index 5fc0fb5..8e930c9 100644 --- a/scripts/install.sh.template +++ b/scripts/install.sh.template @@ -148,11 +148,11 @@ function install { FTYPE="" case "${OS}_${ARCH}" in "linux_amd64") - URL="https://github.com/WaylonWalker/nvim-manager/releases/download/v${RELEASE}/nvim-manager-${RELEASE}-x86_64-unknown-linux-gnu.tar.gz" + URL="https://github.com/WaylonWalker/krayt/releases/download/v${RELEASE}/krayt-${RELEASE}-x86_64-unknown-linux-gnu.tar.gz" FTYPE=".tar.gz" ;; "linux_arm64") - URL="https://github.com/WaylonWalker/nvim-manager/releases/download/v${RELEASE}/nvim-manager-${RELEASE}-aarch64-unknown-linux-gnu.tar.gz" + URL="https://github.com/WaylonWalker/krayt/releases/download/v${RELEASE}/krayt-${RELEASE}-aarch64-unknown-linux-gnu.tar.gz" FTYPE=".tar.gz" ;; *) fail "No asset for platform ${OS}-${ARCH}" ;; @@ -193,7 +193,7 @@ function install { unzip -o -qq tmp.zip || fail "unzip failed" rm tmp.zip || fail "cleanup failed" elif [[ $FTYPE = ".bin" ]]; then - bash -c "$GET $URL" >"nvim-manager_${OS}_${ARCH}" || fail "download failed" + bash -c "$GET $URL" >"krayt_${OS}_${ARCH}" || fail "download failed" else fail "unknown file type: $FTYPE" fi From 82102c4adff49c6decd0a1e274ff87b3ecac03e9 Mon Sep 17 00:00:00 2001 From: "Waylon S. Walker" Date: Fri, 18 Apr 2025 13:36:23 -0500 Subject: [PATCH 27/50] update release notes --- scripts/get_release_notes.py | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/scripts/get_release_notes.py b/scripts/get_release_notes.py index d782a15..f564136 100755 --- a/scripts/get_release_notes.py +++ b/scripts/get_release_notes.py @@ -21,7 +21,11 @@ def get_release_notes(version): You can install krayt using one of these methods: -> !krayt requires [uv](https://docs.astral.sh/uv/getting-started/installation/) to be installed +## pypi + +``` bash +pip install krayt +``` ### Using i.jpillora.com (recommended) @@ -37,8 +41,8 @@ curl -fsSL https://github.com/waylonwalker/krayt/releases/download/v{version}/in ### Manual download You can also manually download the archive for your platform from the releases page: -- [x86_64-unknown-linux-gnu](https://github.com/waylonwalker/krayt/releases/download/v{version}/krayt-{version}-x86_64-unknown-linux-gnu.tar.gz) -- [aarch64-unknown-linux-gnu](https://github.com/waylonwalker/krayt/releases/download/v{version}/krayt-{version}-aarch64-unknown-linux-gnu.tar.gz)""" +- [x86_64-unknown-linux-gnu](https://github.com/waylonwalker/krayt/releases/download/v{version}/krayt-{version}-x86_64-unknown-linux-gnu) +- [aarch64-unknown-linux-gnu](https://github.com/waylonwalker/krayt/releases/download/v{version}/krayt-{version}-aarch64-unknown-linux-gnu)""" # Get help output for main command and all subcommands try: @@ -46,17 +50,23 @@ You can also manually download the archive for your platform from the releases p # Get main help output main_help = subprocess.check_output( - ["./krayt.py", "--help"], + ["krayt", "--help"], stderr=subprocess.STDOUT, universal_newlines=True, ) help_outputs.append(("Main Command", main_help)) # Get help for each subcommand - subcommands = ["create", "exec", "clean", "version"] + subcommands = [ + "create", + "exec", + "clean", + "version", + "pod", + ] for cmd in subcommands: cmd_help = subprocess.check_output( - ["./krayt.py", cmd, "--help"], + ["krayt", cmd, "--help"], stderr=subprocess.STDOUT, universal_newlines=True, ) From 2f16036c8e29f97299dfc34e4b89ef8f09b1ca2d Mon Sep 17 00:00:00 2001 From: "Waylon S. Walker" Date: Fri, 18 Apr 2025 13:36:55 -0500 Subject: [PATCH 28/50] use hatch version --- justfile | 85 +++++++++++++++++++++++--------------------------------- 1 file changed, 34 insertions(+), 51 deletions(-) diff --git a/justfile b/justfile index 8b9c260..49279c2 100644 --- a/justfile +++ b/justfile @@ -3,7 +3,7 @@ delete-tag: set -euo pipefail # Get the version - VERSION=$(cat version) + VERSION=$(hatch version) # Delete the tag git tag -d "v$VERSION" @@ -14,83 +14,66 @@ delete-release: set -euo pipefail # Get the version - VERSION=$(cat version) + VERSION=$(hatch version) # Delete the release gh release delete "v$VERSION" create-tag: #!/usr/bin/env bash - VERSION=$(cat version) + VERSION=$(hatch version) git tag -a "v$VERSION" -m "Release v$VERSION" git push origin "v$VERSION" create-archives: #!/usr/bin/env bash - VERSION=$(cat version) + VERSION=$(hatch version) rm -rf dist build - mkdir -p dist + hatch build -t binary + + krayt_bin=dist/binary/krayt-${VERSION} # Create the binary for each platform for platform in "x86_64-unknown-linux-gnu" "aarch64-unknown-linux-gnu"; do - outdir="krayt-${VERSION}-${platform}" - mkdir -p "dist/${outdir}" - + outbin="krayt-${VERSION}-${platform}" # Copy the Python script and update version - cp krayt.py "dist/${outdir}/krayt.py" - sed -i "s/NIGHTLY/${VERSION}/" "dist/${outdir}/krayt.py" - - cd dist - tar czf "${outdir}.tar.gz" "${outdir}" - sha256sum "${outdir}.tar.gz" > "${outdir}.tar.gz.sha256" - cd .. + cp ${krayt_bin} "dist/binary/${outbin}" done # Generate install.sh - ./scripts/generate_install_script.py "$VERSION" - chmod +x dist/install.sh + # ./scripts/generate_install_script.py "$VERSION" + # chmod +x dist/install.sh -create-release: create-tag create-archives +# create-release: create-archives +create-release: #!/usr/bin/env bash - VERSION=$(cat version) + VERSION=$(hatch version) ./scripts/get_release_notes.py "$VERSION" > release_notes.tmp - gh release create "v$VERSION" \ - --title "v$VERSION" \ - --notes-file release_notes.tmp \ - dist/krayt-${VERSION}-x86_64-unknown-linux-gnu.tar.gz \ - dist/krayt-${VERSION}-x86_64-unknown-linux-gnu.tar.gz.sha256 \ - dist/krayt-${VERSION}-aarch64-unknown-linux-gnu.tar.gz \ - dist/krayt-${VERSION}-aarch64-unknown-linux-gnu.tar.gz.sha256 \ - dist/install.sh + + # Check if release already exists + if gh release view "v$VERSION" &>/dev/null; then + echo "Release v$VERSION already exists. Uploading binaries..." + # Upload binaries to existing release + gh release upload "v$VERSION" \ + dist/binary/krayt-${VERSION} \ + dist/binary/krayt-${VERSION}-aarch64-unknown-linux-gnu \ + dist/binary/krayt-${VERSION}-x86_64-unknown-linux-gnu || true + else + echo "Creating new release v$VERSION" + # Create new release with binaries + gh release create "v$VERSION" \ + --title "v$VERSION" \ + --notes-file release_notes.tmp \ + dist/binary/krayt-${VERSION} \ + dist/binary/krayt-${VERSION}-aarch64-unknown-linux-gnu \ + dist/binary/krayt-${VERSION}-x86_64-unknown-linux-gnu + fi rm release_notes.tmp preview-release-notes: #!/usr/bin/env bash - VERSION=$(cat version) + VERSION=$(hatch version) ./scripts/get_release_notes.py "$VERSION" | less -R release: create-release -build-pyapp: - export PYAPP_PROJECT_NAME=krayt - export PYAPP_PROJECT_VERSION=`hatch version` - export PYAPP_DISTRIBUTION_SOURCE=~/git/krayt/dist/krayt-${PYAPP_PROJECT_VERSION}.tar.gz - export PYAPP_DISTRIBUTION_EMBED=true - - - echo "linting" - hatch run lint-format - - echo "Building pyapp" - hatch build - - echo "Uploading pyapp" - hatch publish - - cd ~/git/pyapp - cargo build --release --quiet - - - echo "Done" - - From ae918bf5f27124ce4658a7055a7cbcbe312a6cf5 Mon Sep 17 00:00:00 2001 From: "Waylon S. Walker" Date: Fri, 18 Apr 2025 13:37:11 -0500 Subject: [PATCH 29/50] changelog for 0.3.0 --- CHANGELOG.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4f04f65..40950cf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +## 0.3.0 + +- created pypi release +- updated releases to use pyapp +- all new package +- port forward support +- additional_packages support + ## 0.2.0 ### Added From cc425cf812bb58437418fc5a8813855f14730237 Mon Sep 17 00:00:00 2001 From: "Waylon S. Walker" Date: Fri, 18 Apr 2025 13:38:05 -0500 Subject: [PATCH 30/50] add --apply flag for create --- CHANGELOG.md | 4 +++ krayt/cli/pod.py | 65 ++++++++++++++++++++++++++++++++++++++++++------ 2 files changed, 62 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 40950cf..b32f5e9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.4.0 + +- create now has --apply to apply the generated manifest to the cluster + ## 0.3.0 - created pypi release diff --git a/krayt/cli/pod.py b/krayt/cli/pod.py index 6cf9aa3..1417247 100644 --- a/krayt/cli/pod.py +++ b/krayt/cli/pod.py @@ -144,6 +144,17 @@ def get_pods( raise typer.Exit(1) +def get_namespaces( + namespace=None, + label_selector: str = "app=krayt", +): + config.load_kube_config() + api = client.CoreV1Api() + + all_namespaces = [n.metadata.name for n in api.list_namespace().items] + return all_namespaces + + def get_pod_spec(pod_name, namespace): config.load_kube_config() v1 = client.CoreV1Api() @@ -640,6 +651,11 @@ def create( "--post-init-hooks", help="additional hooks to execute at the start of container initialization", ), + apply: bool = typer.Option( + False, + "--apply", + help="Automatically apply the changes instead of just echoing them.", + ), ): """ Krack open a Krayt dragon! Create an inspector pod to explore what's inside your volumes. @@ -647,16 +663,15 @@ def create( The inspector will be created in the same namespace as the selected pod. """ # For create, we want to list all pods, not just Krayt pods - selected_namespace = None - selected_pod = None + selected_namespace = namespace + selected_pod = clone if namespace is None and clone is not None and "/" in clone: selected_namespace, selected_pod = clone.split("/", 1) - elif namespace is not None and clone is not None: - selected_namespace = namespace - selected_pod = clone - pods = get_pods(namespace, label_selector=None) + namepaces = get_namespaces(namespace) + pods = get_pods(namespace, label_selector="app!=krayt") + if not pods: typer.echo("No pods found.") raise typer.Exit(1) @@ -694,7 +709,43 @@ def create( ) # Output the job manifest - typer.echo(yaml.dump(clean_dict(inspector_job), sort_keys=False)) + job_yaml = yaml.dump(clean_dict(inspector_job), sort_keys=False) + + if apply: + # # Apply the job to the cluster + # import tempfile + # import subprocess + # + # with tempfile.NamedTemporaryFile(mode="w", suffix=".yaml") as temp_file: + # temp_file.write(job_yaml) + # temp_file.flush() + # + # try: + # typer.echo( + # f"Applying job {job_name} to namespace {selected_namespace}..." + # ) + # result = subprocess.run( + # ["kubectl", "apply", "-f", temp_file.name], + # capture_output=True, + # text=True, + # check=True, + # ) + # typer.echo(result.stdout) + # typer.echo(f"Successfully created inspector job {job_name}") + # except subprocess.CalledProcessError as e: + # typer.echo(f"Error applying job: {e.stderr}", err=True) + # raise typer.Exit(1) + # + batch_api = client.BatchV1Api() + job = batch_api.create_namespaced_job( + namespace=selected_namespace, + body=inspector_job, + ) + print(f"Job {job.metadata.name} created.") + return job + else: + # Just echo the YAML + typer.echo(job_yaml) @app.command() From 7daa9a3874780640f6ada8d6c3fd570ddc0676a5 Mon Sep 17 00:00:00 2001 From: "Waylon S. Walker" Date: Fri, 18 Apr 2025 14:05:48 -0500 Subject: [PATCH 31/50] python native exec --- krayt/cli/pod.py | 281 ++++++++++++++++++++++++++++------------------- 1 file changed, 170 insertions(+), 111 deletions(-) diff --git a/krayt/cli/pod.py b/krayt/cli/pod.py index 1417247..9107b7b 100644 --- a/krayt/cli/pod.py +++ b/krayt/cli/pod.py @@ -1,5 +1,6 @@ import iterfzf from krayt.templates import env +from kubernetes.stream import stream from kubernetes import client, config import logging import os @@ -8,6 +9,10 @@ import typer from typing import Any, List, Optional import yaml from krayt.__about__ import __version__ +import sys +import tty +import termios +import select logging.basicConfig(level=logging.WARNING) @@ -35,8 +40,8 @@ def format_volume_mount(vm: client.V1VolumeMount) -> dict[str, Any]: return clean_dict( { "name": vm.name, - "mountPath": vm.mount_path, - "readOnly": vm.read_only if vm.read_only else None, + "mount_path": vm.mount_path, + "read_only": vm.read_only if vm.read_only else None, } ) @@ -271,92 +276,81 @@ def create_inspector_job( pre_init_hooks: Optional[List[str]] = None, post_init_hooks: Optional[List[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, - } + secret_mounts = [ + client.V1VolumeMount( + name=vol.name, + mount_path=f"/mnt/secrets/{vol.secret.secret_name}", + read_only=True, ) + for vol in secret_volumes + ] - # Convert volume mounts to dictionaries formatted_mounts = [format_volume_mount(vm) for vm in volume_mounts] + formatted_mounts = [client.V1VolumeMount(**vm) for vm in formatted_mounts if vm] 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 = [ + f"{v.name}:{v.persistent_volume_claim.claim_name}" + for v in volumes + if hasattr(v, "persistent_volume_claim") and v.persistent_volume_claim + ] - 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) - pvcs = None - pre_init_scripts = None - post_init_scripts = None - pre_init_hooks = None - post_init_hooks = None + template = env.get_template("base.sh") command = template.render( volumes=volumes, - pvcs=pvcs, + pvcs=None, 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, + pre_init_scripts=None, + post_init_scripts=None, + pre_init_hooks=None, + post_init_hooks=None, ) - 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": { - "ttlSecondsAfterFinished": 600, - "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 + container = client.V1Container( + name="inspector", + image=image, + command=["sh", "-c", command], + env=env_vars, + volume_mounts=formatted_mounts, + ) + + spec = client.V1PodSpec( + containers=[container], + volumes=[format_volume(v) for v in volumes if format_volume(v)], + restart_policy="Never", + image_pull_secrets=[client.V1LocalObjectReference(name=imagepullsecret)] + if imagepullsecret + else None, + ) + + template = client.V1PodTemplateSpec( + metadata=client.V1ObjectMeta(labels={"app": "krayt"}), spec=spec + ) + + job_spec = client.V1JobSpec( + template=template, + ttl_seconds_after_finished=600, + ) + + job = client.V1Job( + api_version="batch/v1", + kind="Job", + metadata=client.V1ObjectMeta( + name=job_name, + namespace=namespace, + labels={"app": "krayt"}, + annotations={"pvcs": ",".join(pvc_info) if pvc_info else "none"}, + ), + spec=job_spec, + ) + + return job PROTECTED_NAMESPACES = { @@ -449,6 +443,89 @@ def get_pod(namespace: Optional[str] = None): return pod_name, pod_namespace +# @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. +# """ +# +# pod_name, pod_namespace = get_pod(namespace) +# exec_command = [ +# "kubectl", +# "exec", +# "-it", +# "-n", +# pod_namespace, +# pod_name, +# "--", +# "/bin/bash", +# "-l", +# ] +# +# os.execvp("kubectl", exec_command) + + +def interactive_exec(pod_name: str, namespace: str): + # Load kubeconfig from local context (or use load_incluster_config if running inside the cluster) + config.load_kube_config() + + core_v1 = client.CoreV1Api() + command = ["/bin/bash", "-i"] + + # Save the current terminal settings + oldtty = termios.tcgetattr(sys.stdin) + try: + # Put terminal into raw mode but don't handle local echo ourselves + # Let the remote terminal handle echoing and control characters + tty.setraw(sys.stdin.fileno()) + + # Create a TTY-enabled exec connection to the pod + resp = stream( + core_v1.connect_get_namespaced_pod_exec, + pod_name, + namespace, + command=command, + stderr=True, + stdin=True, + stdout=True, + tty=True, + _preload_content=False, + ) + + # Set up a simple select-based event loop to handle I/O + while resp.is_open(): + # Update the websocket connection + resp.update(timeout=0.1) + + # Handle output from the pod + if resp.peek_stdout(): + sys.stdout.write(resp.read_stdout()) + sys.stdout.flush() + if resp.peek_stderr(): + sys.stderr.write(resp.read_stderr()) + sys.stderr.flush() + + # Check for input from the user + rlist, _, _ = select.select([sys.stdin], [], [], 0.01) + if sys.stdin in rlist: + # Read input and forward it to the pod without local echo + data = os.read(sys.stdin.fileno(), 1024) + if data: + resp.write_stdin(data.decode()) + + except Exception as e: + print(f"\nError in interactive session: {e}", file=sys.stderr) + finally: + # Always restore terminal settings + termios.tcsetattr(sys.stdin, termios.TCSADRAIN, oldtty) + + @app.command() def exec( namespace: Optional[str] = typer.Option( @@ -460,21 +537,28 @@ def exec( 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() # or config.load_incluster_config() if running inside a pod + core_v1 = client.CoreV1Api() pod_name, pod_namespace = get_pod(namespace) - exec_command = [ - "kubectl", - "exec", - "-it", - "-n", - pod_namespace, - pod_name, - "--", - "/bin/bash", - "-l", - ] + interactive_exec(pod_name, pod_namespace) - os.execvp("kubectl", exec_command) + # command = ["/bin/bash", "-l"] + # print(f"kubectl exec -it -n {pod_namespace} {pod_name} -- {' '.join(command)}") + # print( + # f"execing into {pod_name} in {pod_namespace} with command {' '.join(command)}" + # ) + # resp = stream( + # core_v1.connect_get_namespaced_pod_exec, + # pod_name, + # pod_namespace, + # command=command, + # stderr=True, + # stdin=True, + # stdout=True, + # tty=True, + # ) + # print(resp) @app.command() @@ -687,9 +771,6 @@ def create( typer.echo("No pod selected.") raise typer.Exit(1) - # typer.echo(f"Selected pod exists: {selected_pod in (p[0] for p in pods)}") - # typer.echo(f"Selected pod: {selected_pod} ({selected_namespace})") - pod_spec = get_pod_spec(selected_pod, selected_namespace) volume_mounts, volumes = get_pod_volumes_and_mounts(pod_spec) @@ -709,33 +790,11 @@ def create( ) # Output the job manifest - job_yaml = yaml.dump(clean_dict(inspector_job), sort_keys=False) + api_client = client.ApiClient() + job_dict = api_client.sanitize_for_serialization(inspector_job) + job_yaml = yaml.dump(job_dict, sort_keys=False) if apply: - # # Apply the job to the cluster - # import tempfile - # import subprocess - # - # with tempfile.NamedTemporaryFile(mode="w", suffix=".yaml") as temp_file: - # temp_file.write(job_yaml) - # temp_file.flush() - # - # try: - # typer.echo( - # f"Applying job {job_name} to namespace {selected_namespace}..." - # ) - # result = subprocess.run( - # ["kubectl", "apply", "-f", temp_file.name], - # capture_output=True, - # text=True, - # check=True, - # ) - # typer.echo(result.stdout) - # typer.echo(f"Successfully created inspector job {job_name}") - # except subprocess.CalledProcessError as e: - # typer.echo(f"Error applying job: {e.stderr}", err=True) - # raise typer.Exit(1) - # batch_api = client.BatchV1Api() job = batch_api.create_namespaced_job( namespace=selected_namespace, From 3ce69baf261784d4d2ee11f918e369fb3778d129 Mon Sep 17 00:00:00 2001 From: "Waylon S. Walker" Date: Mon, 21 Apr 2025 09:46:03 -0500 Subject: [PATCH 32/50] generic templates endpoint for cli --- CHANGELOG.md | 1 + krayt/cli/__init__.py | 2 +- krayt/cli/templates.py | 72 +++++++++++++++++++++--------------------- 3 files changed, 38 insertions(+), 37 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b32f5e9..075d211 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ ## 0.4.0 - create now has --apply to apply the generated manifest to the cluster +- generic templates endpoint for cli ## 0.3.0 diff --git a/krayt/cli/__init__.py b/krayt/cli/__init__.py index d5c6f0c..035a6de 100644 --- a/krayt/cli/__init__.py +++ b/krayt/cli/__init__.py @@ -6,7 +6,7 @@ from typer import Typer app = Typer() -app.add_typer(templates_app, name="templates", no_args_is_help=True) +app.add_typer(templates_app, name="template", no_args_is_help=True) app.add_typer(pod_app, name="pod", no_args_is_help=True) app.command(name="create")(create) app.command(name="c")(create) diff --git a/krayt/cli/templates.py b/krayt/cli/templates.py index 841cd54..7416db6 100644 --- a/krayt/cli/templates.py +++ b/krayt/cli/templates.py @@ -13,7 +13,8 @@ def list(): @app.command() -def base( +def render( + template_name: Optional[str] = typer.Option("base.sh", "--template-name", "-t"), volumes: Optional[List[str]] = typer.Option( None, "--volume", @@ -48,7 +49,6 @@ def base( 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, @@ -62,37 +62,37 @@ def base( print(rendered) -@app.command() -def install( - additional_packages: Optional[List[str]] = typer.Option( - ..., "--additional-packages", "-ap" - ), -): - template_name = "install.sh" - template = env.get_template(template_name) - rendered = template.render(additional_packages=additional_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) +# @app.command() +# def install( +# additional_packages: Optional[List[str]] = typer.Option( +# ..., "--additional-packages", "-ap" +# ), +# ): +# template_name = "install.sh" +# template = env.get_template(template_name) +# rendered = template.render(additional_packages=additional_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) From db968536463aa1bf55a5a5c34eb14ff851263660 Mon Sep 17 00:00:00 2001 From: "Waylon S. Walker" Date: Mon, 21 Apr 2025 09:47:31 -0500 Subject: [PATCH 33/50] add python fallback for exec --- krayt/cli/pod.py | 196 +++++++++++++++++++++++++++++------------------ 1 file changed, 120 insertions(+), 76 deletions(-) diff --git a/krayt/cli/pod.py b/krayt/cli/pod.py index 9107b7b..4c81f19 100644 --- a/krayt/cli/pod.py +++ b/krayt/cli/pod.py @@ -13,6 +13,8 @@ import sys import tty import termios import select +import signal +import json logging.basicConfig(level=logging.WARNING) @@ -443,87 +445,121 @@ def get_pod(namespace: Optional[str] = None): return pod_name, pod_namespace -# @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. -# """ -# -# pod_name, pod_namespace = get_pod(namespace) -# exec_command = [ -# "kubectl", -# "exec", -# "-it", -# "-n", -# pod_namespace, -# pod_name, -# "--", -# "/bin/bash", -# "-l", -# ] -# -# os.execvp("kubectl", exec_command) - - def interactive_exec(pod_name: str, namespace: str): # Load kubeconfig from local context (or use load_incluster_config if running inside the cluster) - config.load_kube_config() + print(f"Connecting to pod {pod_name} in namespace {namespace}...") + try: + config.load_kube_config() + except Exception as e: + print(f"Error loading kubeconfig: {e}", file=sys.stderr) + return core_v1 = client.CoreV1Api() - command = ["/bin/bash", "-i"] + command = ["/bin/bash", "-l"] + resp = None # Save the current terminal settings oldtty = termios.tcgetattr(sys.stdin) + + # Function to handle window resize events + def handle_resize(signum, frame): + if resp and resp.is_open(): + # Get the current terminal size + cols, rows = os.get_terminal_size() + # Send terminal resize command via websocket + # Format matches kubectl's resize message format + resize_msg = json.dumps({"Width": cols, "Height": rows}) + resp.write_channel(4, resize_msg) + + # Function to handle exit signals + def handle_exit(signum, frame): + if resp and resp.is_open(): + # Send Ctrl+C to the remote process + resp.write_stdin("\x03") + try: # Put terminal into raw mode but don't handle local echo ourselves # Let the remote terminal handle echoing and control characters tty.setraw(sys.stdin.fileno()) - + + # Set up signal handlers + signal.signal(signal.SIGWINCH, handle_resize) # Window resize + signal.signal(signal.SIGINT, handle_exit) # Ctrl+C + # Create a TTY-enabled exec connection to the pod - resp = stream( - core_v1.connect_get_namespaced_pod_exec, - pod_name, - namespace, - command=command, - stderr=True, - stdin=True, - stdout=True, - tty=True, - _preload_content=False, - ) + try: + resp = stream( + core_v1.connect_get_namespaced_pod_exec, + pod_name, + namespace, + command=command, + stderr=True, + stdin=True, + stdout=True, + tty=True, + _preload_content=False, + ) + print(f"Connected to {pod_name}") + except Exception as e: + print(f"\nError connecting to pod: {e}", file=sys.stderr) + return + + # Wait for the connection to be ready + time.sleep(0.2) + + # Send initial terminal size + cols, rows = os.get_terminal_size() + resize_msg = json.dumps({"Width": cols, "Height": rows}) + resp.write_channel(4, resize_msg) + + # Make sure the size is set by sending a resize event + handle_resize(None, None) # Set up a simple select-based event loop to handle I/O - while resp.is_open(): - # Update the websocket connection - resp.update(timeout=0.1) - - # Handle output from the pod - if resp.peek_stdout(): - sys.stdout.write(resp.read_stdout()) - sys.stdout.flush() - if resp.peek_stderr(): - sys.stderr.write(resp.read_stderr()) - sys.stderr.flush() + try: + while resp and resp.is_open(): + # Update the websocket connection + resp.update(timeout=0.1) - # Check for input from the user - rlist, _, _ = select.select([sys.stdin], [], [], 0.01) - if sys.stdin in rlist: - # Read input and forward it to the pod without local echo - data = os.read(sys.stdin.fileno(), 1024) - if data: + # Handle output from the pod + if resp.peek_stdout(): + sys.stdout.write(resp.read_stdout()) + sys.stdout.flush() + if resp.peek_stderr(): + sys.stderr.write(resp.read_stderr()) + sys.stderr.flush() + + # Check for input from the user + rlist, _, _ = select.select([sys.stdin], [], [], 0.01) + if sys.stdin in rlist: + # Read input and forward it to the pod without local echo + data = os.read(sys.stdin.fileno(), 1024) + if not data: # EOF (e.g., user pressed Ctrl+D) + break resp.write_stdin(data.decode()) + except Exception as e: + print(f"\nConnection error: {e}", file=sys.stderr) + except KeyboardInterrupt: + # Handle Ctrl+C gracefully + print("\nSession terminated by user", file=sys.stderr) except Exception as e: print(f"\nError in interactive session: {e}", file=sys.stderr) finally: + # Reset signal handlers + signal.signal(signal.SIGWINCH, signal.SIG_DFL) + signal.signal(signal.SIGINT, signal.SIG_DFL) + + # Close the connection if it's still open + if resp and resp.is_open(): + try: + resp.close() + except: + pass + # Always restore terminal settings termios.tcsetattr(sys.stdin, termios.TCSADRAIN, oldtty) + print("\nConnection closed", file=sys.stderr) @app.command() @@ -532,6 +568,12 @@ def exec( None, help="Kubernetes namespace. If not specified, will search for inspectors across all namespaces.", ), + shell: Optional[str] = typer.Option( + "/bin/bash", + "--shell", + "-s", + help="Shell to use for the inspector pod", + ), ): """ Enter the Krayt dragon's lair! Connect to a running inspector pod. @@ -541,24 +583,26 @@ def exec( core_v1 = client.CoreV1Api() pod_name, pod_namespace = get_pod(namespace) - interactive_exec(pod_name, pod_namespace) - # command = ["/bin/bash", "-l"] - # print(f"kubectl exec -it -n {pod_namespace} {pod_name} -- {' '.join(command)}") - # print( - # f"execing into {pod_name} in {pod_namespace} with command {' '.join(command)}" - # ) - # resp = stream( - # core_v1.connect_get_namespaced_pod_exec, - # pod_name, - # pod_namespace, - # command=command, - # stderr=True, - # stdin=True, - # stdout=True, - # tty=True, - # ) - # print(resp) + try: + pod_name, pod_namespace = get_pod(namespace) + exec_command = [ + "kubectl", + "exec", + "-it", + "-n", + pod_namespace, + pod_name, + "--", + shell, + "-l", + ] + + os.execvp("kubectl", exec_command) + except Exception as e: + print(f"Error executing command with kubectl trying python api: {e}") + + interactive_exec(pod_name, pod_namespace) @app.command() From 225edce32d24f11fe53eb3712760a1190287a69b Mon Sep 17 00:00:00 2001 From: "Waylon S. Walker" Date: Wed, 23 Apr 2025 19:42:40 -0500 Subject: [PATCH 34/50] better motd volumes --- krayt/templates/motd.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/krayt/templates/motd.sh b/krayt/templates/motd.sh index 9de1e85..5838161 100644 --- a/krayt/templates/motd.sh +++ b/krayt/templates/motd.sh @@ -5,11 +5,11 @@ cat </etc/motd └───────────────────────────────────┘ "Inside every volume lies a pearl of wisdom waiting to be discovered." -{%- if volumes %} +{%- if mounts %} Mounted Volumes: -{%- for volume in volumes %} -- {{ volume }} +{%- for mount in mounts %} +- {{ mount.name }}:{{ mount.mount_path }} {%- endfor %} {%- endif %} From 17c088526b8c4d5abe4d7c1c6849639d748a6231 Mon Sep 17 00:00:00 2001 From: "Waylon S. Walker" Date: Wed, 23 Apr 2025 19:56:26 -0500 Subject: [PATCH 35/50] release 0.4.0 --- .github/workflows/release-pypi.yaml | 11 +++++++++++ CHANGELOG.md | 1 + justfile | 4 ++-- krayt/__about__.py | 2 +- 4 files changed, 15 insertions(+), 3 deletions(-) diff --git a/.github/workflows/release-pypi.yaml b/.github/workflows/release-pypi.yaml index bd6c6ab..162de46 100644 --- a/.github/workflows/release-pypi.yaml +++ b/.github/workflows/release-pypi.yaml @@ -22,3 +22,14 @@ jobs: env: # required for gh release GH_TOKEN: ${{ github.token }} + - name: Install just + run: | + curl --proto '=https' --tlsv1.2 -sSf https://just.systems/install.sh | bash -s -- --to DEST + shell: bash + - name: Install uv + run: | + curl -LsSf https://astral.sh/uv/0.6.16/install.sh | sh + shell: bash + - name: GitHub Release (just release) + run: just create-release + shell: bash diff --git a/CHANGELOG.md b/CHANGELOG.md index 075d211..e6265dc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ - create now has --apply to apply the generated manifest to the cluster - generic templates endpoint for cli +- better motd for volume mounts ## 0.3.0 diff --git a/justfile b/justfile index 49279c2..5e9b035 100644 --- a/justfile +++ b/justfile @@ -44,8 +44,8 @@ create-archives: # ./scripts/generate_install_script.py "$VERSION" # chmod +x dist/install.sh -# create-release: create-archives -create-release: +create-release: create-tag create-archives +# create-release: #!/usr/bin/env bash VERSION=$(hatch version) ./scripts/get_release_notes.py "$VERSION" > release_notes.tmp diff --git a/krayt/__about__.py b/krayt/__about__.py index 493f741..dab8f5a 100644 --- a/krayt/__about__.py +++ b/krayt/__about__.py @@ -1 +1 @@ -__version__ = "0.3.0" +__version__ = "0.4.0b0" From ed449034f70c30d65a462cd50636ea1c3458d623 Mon Sep 17 00:00:00 2001 From: "Waylon S. Walker" Date: Wed, 23 Apr 2025 19:59:15 -0500 Subject: [PATCH 36/50] formatting fix --- krayt/cli/pod.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/krayt/cli/pod.py b/krayt/cli/pod.py index 4c81f19..d8ac87a 100644 --- a/krayt/cli/pod.py +++ b/krayt/cli/pod.py @@ -554,7 +554,7 @@ def interactive_exec(pod_name: str, namespace: str): if resp and resp.is_open(): try: resp.close() - except: + except Exception: pass # Always restore terminal settings @@ -580,7 +580,7 @@ def exec( If multiple inspectors are found, you'll get to choose which one to explore. """ config.load_kube_config() # or config.load_incluster_config() if running inside a pod - core_v1 = client.CoreV1Api() + client.CoreV1Api() pod_name, pod_namespace = get_pod(namespace) @@ -797,7 +797,7 @@ def create( if namespace is None and clone is not None and "/" in clone: selected_namespace, selected_pod = clone.split("/", 1) - namepaces = get_namespaces(namespace) + get_namespaces(namespace) pods = get_pods(namespace, label_selector="app!=krayt") if not pods: @@ -916,10 +916,10 @@ def list_pods(): typer.echo(f"{pod} ({namespace})") -def main(): - setup_environment() - app() - - -if __name__ == "__main__": - main() +# def main(): +# setup_environment() +# app() +# +# +# if __name__ == "__main__": +# main() From 681194fc1f58612e89a765ae0a8fd58724ce68cb Mon Sep 17 00:00:00 2001 From: "Waylon S. Walker" Date: Wed, 23 Apr 2025 20:00:20 -0500 Subject: [PATCH 37/50] fix justfile format --- justfile | 1 - 1 file changed, 1 deletion(-) diff --git a/justfile b/justfile index 5e9b035..f62f5c9 100644 --- a/justfile +++ b/justfile @@ -45,7 +45,6 @@ create-archives: # chmod +x dist/install.sh create-release: create-tag create-archives -# create-release: #!/usr/bin/env bash VERSION=$(hatch version) ./scripts/get_release_notes.py "$VERSION" > release_notes.tmp From 64077d3ba2122ffbf3ebd495778072fa0088f3a4 Mon Sep 17 00:00:00 2001 From: WaylonWalker Date: Thu, 24 Apr 2025 01:01:32 +0000 Subject: [PATCH 38/50] =?UTF-8?q?Bump=20version:=200.4.0b0=20=E2=86=92=200?= =?UTF-8?q?.4.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- krayt/__about__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/krayt/__about__.py b/krayt/__about__.py index dab8f5a..6a9beea 100644 --- a/krayt/__about__.py +++ b/krayt/__about__.py @@ -1 +1 @@ -__version__ = "0.4.0b0" +__version__ = "0.4.0" From 0a6e19731d8db9f0ce432deb9dfd91798792c3c1 Mon Sep 17 00:00:00 2001 From: "Waylon S. Walker" Date: Wed, 23 Apr 2025 20:04:02 -0500 Subject: [PATCH 39/50] fix just install --- .github/workflows/release-pypi.yaml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/release-pypi.yaml b/.github/workflows/release-pypi.yaml index 162de46..8fbadca 100644 --- a/.github/workflows/release-pypi.yaml +++ b/.github/workflows/release-pypi.yaml @@ -16,15 +16,15 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: waylonwalker/hatch-action@v4 - with: - before-command: "lint-format" - env: - # required for gh release - GH_TOKEN: ${{ github.token }} + # - uses: waylonwalker/hatch-action@v4 + # with: + # before-command: "lint-format" + # env: + # # required for gh release + # GH_TOKEN: ${{ github.token }} - name: Install just run: | - curl --proto '=https' --tlsv1.2 -sSf https://just.systems/install.sh | bash -s -- --to DEST + curl --proto '=https' --tlsv1.2 -sSf https://just.systems/install.sh | bash -s -- --to /usr/local/bin shell: bash - name: Install uv run: | From 1138f695f47cf3bfbdedb32b6dcf97be00964002 Mon Sep 17 00:00:00 2001 From: "Waylon S. Walker" Date: Wed, 23 Apr 2025 20:04:59 -0500 Subject: [PATCH 40/50] bump action --- .github/workflows/release-pypi.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/release-pypi.yaml b/.github/workflows/release-pypi.yaml index 8fbadca..e7b47e7 100644 --- a/.github/workflows/release-pypi.yaml +++ b/.github/workflows/release-pypi.yaml @@ -22,6 +22,7 @@ jobs: # env: # # required for gh release # GH_TOKEN: ${{ github.token }} + # - name: Install just run: | curl --proto '=https' --tlsv1.2 -sSf https://just.systems/install.sh | bash -s -- --to /usr/local/bin From 7511cace421efe8c0a965439ed0efed0d81e683b Mon Sep 17 00:00:00 2001 From: "Waylon S. Walker" Date: Wed, 23 Apr 2025 20:05:43 -0500 Subject: [PATCH 41/50] bump build --- krayt/__about__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/krayt/__about__.py b/krayt/__about__.py index 6a9beea..6f9c9b3 100644 --- a/krayt/__about__.py +++ b/krayt/__about__.py @@ -1 +1 @@ -__version__ = "0.4.0" +__version__ = "0.4.1b0" From b64e635b714564c2ff3017fd2141f5036fe035b1 Mon Sep 17 00:00:00 2001 From: "Waylon S. Walker" Date: Wed, 23 Apr 2025 20:09:52 -0500 Subject: [PATCH 42/50] install hatch for release --- .github/workflows/release-pypi.yaml | 11 +++++++++++ krayt/__about__.py | 2 +- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/.github/workflows/release-pypi.yaml b/.github/workflows/release-pypi.yaml index e7b47e7..f1a6c3e 100644 --- a/.github/workflows/release-pypi.yaml +++ b/.github/workflows/release-pypi.yaml @@ -31,6 +31,17 @@ jobs: run: | curl -LsSf https://astral.sh/uv/0.6.16/install.sh | sh shell: bash + - name: Install hatch + run: | + uv pip install hatch --system + shell: bash + - name: Configure Git + run: | + git config --global user.name "github-actions[bot]" + git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com" + shell: bash - name: GitHub Release (just release) run: just create-release + env: + GH_TOKEN: ${{ github.token }} shell: bash diff --git a/krayt/__about__.py b/krayt/__about__.py index 6f9c9b3..ea0e418 100644 --- a/krayt/__about__.py +++ b/krayt/__about__.py @@ -1 +1 @@ -__version__ = "0.4.1b0" +__version__ = "0.4.1b1" From 2fbc15ae8492385b92caadff51c9ca211f832f9a Mon Sep 17 00:00:00 2001 From: "Waylon S. Walker" Date: Wed, 23 Apr 2025 20:11:13 -0500 Subject: [PATCH 43/50] use uv tool --- .github/workflows/release-pypi.yaml | 2 +- krayt/__about__.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release-pypi.yaml b/.github/workflows/release-pypi.yaml index f1a6c3e..78cba98 100644 --- a/.github/workflows/release-pypi.yaml +++ b/.github/workflows/release-pypi.yaml @@ -33,7 +33,7 @@ jobs: shell: bash - name: Install hatch run: | - uv pip install hatch --system + uv tool install hatch shell: bash - name: Configure Git run: | diff --git a/krayt/__about__.py b/krayt/__about__.py index ea0e418..04e30ea 100644 --- a/krayt/__about__.py +++ b/krayt/__about__.py @@ -1 +1 @@ -__version__ = "0.4.1b1" +__version__ = "0.4.1b2" From 1fee6e2c97be2fc1b8db531a8167b4678eb628ca Mon Sep 17 00:00:00 2001 From: "Waylon S. Walker" Date: Wed, 23 Apr 2025 20:19:57 -0500 Subject: [PATCH 44/50] fix deployment --- justfile | 2 +- krayt/__about__.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/justfile b/justfile index f62f5c9..35dfece 100644 --- a/justfile +++ b/justfile @@ -31,7 +31,7 @@ create-archives: rm -rf dist build hatch build -t binary - krayt_bin=dist/binary/krayt-${VERSION} + krayt_bin=dist/binary/krayt-${VERSION}/krayt_linux_amd64 # Create the binary for each platform for platform in "x86_64-unknown-linux-gnu" "aarch64-unknown-linux-gnu"; do diff --git a/krayt/__about__.py b/krayt/__about__.py index 04e30ea..7c5a130 100644 --- a/krayt/__about__.py +++ b/krayt/__about__.py @@ -1 +1 @@ -__version__ = "0.4.1b2" +__version__ = "0.4.1b3" From be6ee910981561226d56d404329bf25baad60bb6 Mon Sep 17 00:00:00 2001 From: "Waylon S. Walker" Date: Wed, 23 Apr 2025 20:28:57 -0500 Subject: [PATCH 45/50] release 0.4.1 --- .github/workflows/release-pypi.yaml | 13 ++++++------- CHANGELOG.md | 4 ++++ justfile | 2 +- krayt/__about__.py | 2 +- 4 files changed, 12 insertions(+), 9 deletions(-) diff --git a/.github/workflows/release-pypi.yaml b/.github/workflows/release-pypi.yaml index 78cba98..1e607ef 100644 --- a/.github/workflows/release-pypi.yaml +++ b/.github/workflows/release-pypi.yaml @@ -16,13 +16,12 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - # - uses: waylonwalker/hatch-action@v4 - # with: - # before-command: "lint-format" - # env: - # # required for gh release - # GH_TOKEN: ${{ github.token }} - # + - uses: waylonwalker/hatch-action@v4 + with: + before-command: "lint-format" + env: + # required for gh release + GH_TOKEN: ${{ github.token }} - name: Install just run: | curl --proto '=https' --tlsv1.2 -sSf https://just.systems/install.sh | bash -s -- --to /usr/local/bin diff --git a/CHANGELOG.md b/CHANGELOG.md index e6265dc..a158339 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.4.1 + +- Automated release for both pypi and github + ## 0.4.0 - create now has --apply to apply the generated manifest to the cluster diff --git a/justfile b/justfile index 35dfece..f62f5c9 100644 --- a/justfile +++ b/justfile @@ -31,7 +31,7 @@ create-archives: rm -rf dist build hatch build -t binary - krayt_bin=dist/binary/krayt-${VERSION}/krayt_linux_amd64 + krayt_bin=dist/binary/krayt-${VERSION} # Create the binary for each platform for platform in "x86_64-unknown-linux-gnu" "aarch64-unknown-linux-gnu"; do diff --git a/krayt/__about__.py b/krayt/__about__.py index 7c5a130..a79b352 100644 --- a/krayt/__about__.py +++ b/krayt/__about__.py @@ -1 +1 @@ -__version__ = "0.4.1b3" +__version__ = "0.4.1b4" From d6056cead2e8aa7275e66d85a0e3d265cfa27d73 Mon Sep 17 00:00:00 2001 From: WaylonWalker Date: Thu, 24 Apr 2025 01:36:32 +0000 Subject: [PATCH 46/50] =?UTF-8?q?Bump=20version:=200.4.1b4=20=E2=86=92=200?= =?UTF-8?q?.4.1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- krayt/__about__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/krayt/__about__.py b/krayt/__about__.py index a79b352..3d26edf 100644 --- a/krayt/__about__.py +++ b/krayt/__about__.py @@ -1 +1 @@ -__version__ = "0.4.1b4" +__version__ = "0.4.1" From eb54e314ff4f495ab839e42a0cce79a1d7e0f09f Mon Sep 17 00:00:00 2001 From: "Waylon S. Walker" Date: Wed, 23 Apr 2025 20:40:38 -0500 Subject: [PATCH 47/50] release 0.4.2 --- .github/workflows/release-pypi.yaml | 5 ++++- CHANGELOG.md | 4 ++++ krayt/__about__.py | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release-pypi.yaml b/.github/workflows/release-pypi.yaml index 1e607ef..8aead5b 100644 --- a/.github/workflows/release-pypi.yaml +++ b/.github/workflows/release-pypi.yaml @@ -12,7 +12,7 @@ permissions: packages: none id-token: write jobs: - release-krayt: + pypi-release-krayt: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -22,6 +22,9 @@ jobs: env: # required for gh release GH_TOKEN: ${{ github.token }} + gh-release-krayt: + runs-on: ubuntu-latest + steps: - name: Install just run: | curl --proto '=https' --tlsv1.2 -sSf https://just.systems/install.sh | bash -s -- --to /usr/local/bin diff --git a/CHANGELOG.md b/CHANGELOG.md index a158339..cc6a106 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.4.2 + +- working out binary release process + ## 0.4.1 - Automated release for both pypi and github diff --git a/krayt/__about__.py b/krayt/__about__.py index 3d26edf..d54ab1b 100644 --- a/krayt/__about__.py +++ b/krayt/__about__.py @@ -1 +1 @@ -__version__ = "0.4.1" +__version__ = "0.4.2b0" From 45e5dd74aaea0a1b1af19300ee1f6cdd112f9c86 Mon Sep 17 00:00:00 2001 From: WaylonWalker Date: Thu, 24 Apr 2025 01:42:32 +0000 Subject: [PATCH 48/50] =?UTF-8?q?Bump=20version:=200.4.2b0=20=E2=86=92=200?= =?UTF-8?q?.4.2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- krayt/__about__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/krayt/__about__.py b/krayt/__about__.py index d54ab1b..df12433 100644 --- a/krayt/__about__.py +++ b/krayt/__about__.py @@ -1 +1 @@ -__version__ = "0.4.2b0" +__version__ = "0.4.2" From f8c44999fe1f0d6e98e9c651d35c27477d48fba2 Mon Sep 17 00:00:00 2001 From: "Waylon S. Walker" Date: Wed, 23 Apr 2025 20:45:16 -0500 Subject: [PATCH 49/50] release 0.4.3 --- .github/workflows/release-pypi.yaml | 4 +--- CHANGELOG.md | 4 ++++ krayt/__about__.py | 2 +- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/.github/workflows/release-pypi.yaml b/.github/workflows/release-pypi.yaml index 8aead5b..16111e2 100644 --- a/.github/workflows/release-pypi.yaml +++ b/.github/workflows/release-pypi.yaml @@ -22,9 +22,7 @@ jobs: env: # required for gh release GH_TOKEN: ${{ github.token }} - gh-release-krayt: - runs-on: ubuntu-latest - steps: + - run: sudo rm -rf dist - name: Install just run: | curl --proto '=https' --tlsv1.2 -sSf https://just.systems/install.sh | bash -s -- --to /usr/local/bin diff --git a/CHANGELOG.md b/CHANGELOG.md index cc6a106..29ffba6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.4.3 + +- working out binary release process + ## 0.4.2 - working out binary release process diff --git a/krayt/__about__.py b/krayt/__about__.py index df12433..6d91c22 100644 --- a/krayt/__about__.py +++ b/krayt/__about__.py @@ -1 +1 @@ -__version__ = "0.4.2" +__version__ = "0.4.3b0" From 0f9e70267ecefbc0a9803e1232906971835a10e1 Mon Sep 17 00:00:00 2001 From: WaylonWalker Date: Thu, 24 Apr 2025 01:46:17 +0000 Subject: [PATCH 50/50] =?UTF-8?q?Bump=20version:=200.4.3b0=20=E2=86=92=200?= =?UTF-8?q?.4.3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- krayt/__about__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/krayt/__about__.py b/krayt/__about__.py index 6d91c22..f6b7e26 100644 --- a/krayt/__about__.py +++ b/krayt/__about__.py @@ -1 +1 @@ -__version__ = "0.4.3b0" +__version__ = "0.4.3"