release 0.0.0
This commit is contained in:
parent
24256b767d
commit
778f7fbd7c
8 changed files with 513 additions and 57 deletions
3
CHANGELOG.md
Normal file
3
CHANGELOG.md
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
## 0.0.0
|
||||||
|
|
||||||
|
- Initial release
|
||||||
80
README.md
Normal file
80
README.md
Normal file
|
|
@ -0,0 +1,80 @@
|
||||||
|
# Krayt - The Kubernetes Volume Inspector
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- 🔍 Create inspector pods with all the tools you need
|
||||||
|
- 📦 Access volumes and device mounts from any pod
|
||||||
|
- 🔎 Fuzzy search across all namespaces
|
||||||
|
- 🛠️ Built-in tools for file exploration and analysis
|
||||||
|
- 🧹 Automatic cleanup of inspector pods
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
### Quick Install (Linux)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Install latest version
|
||||||
|
curl -sSL https://github.com/waylonwalker/krayt/releases/latest/download/install.sh | sudo bash
|
||||||
|
|
||||||
|
# Install specific version
|
||||||
|
curl -sSL https://github.com/waylonwalker/krayt/releases/download/v0.1.0/install.sh | sudo bash
|
||||||
|
```
|
||||||
|
|
||||||
|
This will install the `krayt` command to `/usr/local/bin`.
|
||||||
|
|
||||||
|
### Manual Installation
|
||||||
|
|
||||||
|
1. Download the latest release for your platform from the [releases page](https://github.com/waylonwalker/krayt/releases)
|
||||||
|
2. Extract the archive: `tar xzf krayt-*.tar.gz`
|
||||||
|
3. Move the binary: `sudo mv krayt-*/krayt /usr/local/bin/krayt`
|
||||||
|
4. Make it executable: `sudo chmod +x /usr/local/bin/krayt`
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Create a new inspector and apply it directly
|
||||||
|
krayt create | kubectl apply -f -
|
||||||
|
|
||||||
|
# Or review the manifest first
|
||||||
|
krayt create > inspector.yaml
|
||||||
|
kubectl apply -f inspector.yaml
|
||||||
|
|
||||||
|
# Connect to a running inspector
|
||||||
|
krayt exec
|
||||||
|
|
||||||
|
# Clean up inspectors
|
||||||
|
krayt clean
|
||||||
|
|
||||||
|
# Show version
|
||||||
|
krayt version
|
||||||
|
```
|
||||||
|
|
||||||
|
### Available Tools
|
||||||
|
|
||||||
|
Your inspector pod comes equipped with a full arsenal of tools:
|
||||||
|
|
||||||
|
- **File Navigation**: `lf`, `exa`, `fd`
|
||||||
|
- **Search & Analysis**: `ripgrep`, `bat`, `hexyl`
|
||||||
|
- **Disk Usage**: `ncdu`, `dust`
|
||||||
|
- **File Comparison**: `difftastic`
|
||||||
|
- **System Monitoring**: `bottom`, `htop`
|
||||||
|
- **JSON/YAML Tools**: `jq`, `yq`
|
||||||
|
- **Network Tools**: `mtr`, `dig`
|
||||||
|
- **Cloud & Database**: `aws-cli`, `sqlite3`
|
||||||
|
|
||||||
|
## Quotes from the Field
|
||||||
|
|
||||||
|
> "Inside every volume lies a pearl of wisdom waiting to be discovered."
|
||||||
|
>
|
||||||
|
> -- Ancient Tatooine proverb
|
||||||
|
|
||||||
|
> "The path to understanding your storage is through exploration."
|
||||||
|
>
|
||||||
|
> -- Krayt dragon hunter's manual
|
||||||
|
|
||||||
|
## May the Force be with your volumes!
|
||||||
|
|
||||||
|
Remember: A Krayt dragon's pearl is valuable not just for what it is, but for what it reveals about the dragon that created it. Similarly, your volumes tell a story about your application's data journey.
|
||||||
72
justfile
Normal file
72
justfile
Normal file
|
|
@ -0,0 +1,72 @@
|
||||||
|
delete-tag:
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# Get the version
|
||||||
|
VERSION=$(cat version)
|
||||||
|
|
||||||
|
# Delete the tag
|
||||||
|
git tag -d "v$VERSION"
|
||||||
|
git push origin ":refs/tags/v$VERSION"
|
||||||
|
|
||||||
|
delete-release:
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# Get the version
|
||||||
|
VERSION=$(cat version)
|
||||||
|
|
||||||
|
# Delete the release
|
||||||
|
gh release delete "v$VERSION"
|
||||||
|
|
||||||
|
create-tag:
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
VERSION=$(cat version)
|
||||||
|
git tag -a "v$VERSION" -m "Release v$VERSION"
|
||||||
|
git push origin "v$VERSION"
|
||||||
|
|
||||||
|
create-archives:
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
VERSION=$(cat version)
|
||||||
|
rm -rf dist build
|
||||||
|
mkdir -p dist
|
||||||
|
|
||||||
|
# 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}"
|
||||||
|
|
||||||
|
# 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 ..
|
||||||
|
done
|
||||||
|
|
||||||
|
# Generate install.sh
|
||||||
|
./scripts/generate_install_script.py "$VERSION"
|
||||||
|
chmod +x dist/install.sh
|
||||||
|
|
||||||
|
create-release: create-tag create-archives
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
VERSION=$(cat 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
|
||||||
|
rm release_notes.tmp
|
||||||
|
|
||||||
|
preview-release-notes:
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
VERSION=$(cat version)
|
||||||
|
./scripts/get_release_notes.py "$VERSION" | less -R
|
||||||
|
|
||||||
|
release: create-release
|
||||||
|
|
@ -1,21 +1,38 @@
|
||||||
#!/usr/bin/env -S uv run --quiet --script
|
#!/usr/bin/env -S uv run --quiet --script
|
||||||
# /// script
|
# /// script
|
||||||
# requires-python = ">=3.12"
|
# requires-python = ">=3.10"
|
||||||
# dependencies = [
|
# dependencies = [
|
||||||
# "typer",
|
# "typer",
|
||||||
# "kubernetes",
|
# "kubernetes",
|
||||||
# "iterfzf"
|
# "iterfzf"
|
||||||
# ]
|
# ]
|
||||||
# ///
|
# ///
|
||||||
|
"""
|
||||||
|
Krayt - The Kubernetes Volume Inspector
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
Features:
|
||||||
|
- Create inspector pods with all the tools you need
|
||||||
|
- Access volumes and device mounts from any pod
|
||||||
|
- Fuzzy search across all namespaces
|
||||||
|
- Built-in tools for file exploration and analysis
|
||||||
|
- Automatic cleanup of inspector pods
|
||||||
|
|
||||||
|
May the Force be with your volumes!
|
||||||
|
"""
|
||||||
|
|
||||||
from iterfzf import iterfzf
|
from iterfzf import iterfzf
|
||||||
from kubernetes import client, config
|
from kubernetes import client, config
|
||||||
import logging
|
import logging
|
||||||
|
import os
|
||||||
import time
|
import time
|
||||||
import typer
|
import typer
|
||||||
from typing import Any, Optional
|
from typing import Any, Optional
|
||||||
import yaml
|
import yaml
|
||||||
import os
|
|
||||||
|
KRAYT_VERSION = "NIGHTLY"
|
||||||
|
|
||||||
logging.basicConfig(level=logging.WARNING)
|
logging.basicConfig(level=logging.WARNING)
|
||||||
|
|
||||||
|
|
@ -57,9 +74,7 @@ def format_volume(v: client.V1Volume) -> dict[str, Any]:
|
||||||
volume_source = None
|
volume_source = None
|
||||||
if v.persistent_volume_claim:
|
if v.persistent_volume_claim:
|
||||||
volume_source = {
|
volume_source = {
|
||||||
"persistentVolumeClaim": {
|
"persistentVolumeClaim": {"claimName": v.persistent_volume_claim.claim_name}
|
||||||
"claimName": v.persistent_volume_claim.claim_name
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
elif v.config_map:
|
elif v.config_map:
|
||||||
volume_source = {"configMap": {"name": v.config_map.name}}
|
volume_source = {"configMap": {"name": v.config_map.name}}
|
||||||
|
|
@ -69,14 +84,14 @@ def format_volume(v: client.V1Volume) -> dict[str, Any]:
|
||||||
volume_source = {
|
volume_source = {
|
||||||
"hostPath": {
|
"hostPath": {
|
||||||
"path": v.host_path.path,
|
"path": v.host_path.path,
|
||||||
"type": v.host_path.type if v.host_path.type else None
|
"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)
|
elif v.empty_dir: # Add support for emptyDir volumes (used for /dev/shm)
|
||||||
volume_source = {
|
volume_source = {
|
||||||
"emptyDir": {
|
"emptyDir": {
|
||||||
"medium": v.empty_dir.medium if v.empty_dir.medium else None,
|
"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
|
"sizeLimit": v.empty_dir.size_limit if v.empty_dir.size_limit else None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -157,28 +172,30 @@ def get_pod_volumes_and_mounts(pod_spec):
|
||||||
for v in pod_spec.spec.volumes:
|
for v in pod_spec.spec.volumes:
|
||||||
# Handle device mounts
|
# Handle device mounts
|
||||||
if v.name in ["cache-volume"]:
|
if v.name in ["cache-volume"]:
|
||||||
volumes.append(client.V1Volume(
|
volumes.append(
|
||||||
name=v.name,
|
client.V1Volume(
|
||||||
empty_dir=client.V1EmptyDirVolumeSource(
|
name=v.name,
|
||||||
medium="Memory"
|
empty_dir=client.V1EmptyDirVolumeSource(medium="Memory"),
|
||||||
)
|
)
|
||||||
))
|
)
|
||||||
elif v.name in ["coral-device"]:
|
elif v.name in ["coral-device"]:
|
||||||
volumes.append(client.V1Volume(
|
volumes.append(
|
||||||
name=v.name,
|
client.V1Volume(
|
||||||
host_path=client.V1HostPathVolumeSource(
|
name=v.name,
|
||||||
path="/dev/apex_0",
|
host_path=client.V1HostPathVolumeSource(
|
||||||
type="CharDevice"
|
path="/dev/apex_0", type="CharDevice"
|
||||||
|
),
|
||||||
)
|
)
|
||||||
))
|
)
|
||||||
elif v.name in ["qsv-device"]:
|
elif v.name in ["qsv-device"]:
|
||||||
volumes.append(client.V1Volume(
|
volumes.append(
|
||||||
name=v.name,
|
client.V1Volume(
|
||||||
host_path=client.V1HostPathVolumeSource(
|
name=v.name,
|
||||||
path="/dev/dri",
|
host_path=client.V1HostPathVolumeSource(
|
||||||
type="Directory"
|
path="/dev/dri", type="Directory"
|
||||||
|
),
|
||||||
)
|
)
|
||||||
))
|
)
|
||||||
else:
|
else:
|
||||||
volumes.append(v)
|
volumes.append(v)
|
||||||
|
|
||||||
|
|
@ -230,9 +247,12 @@ def get_pod_env_and_secrets(api, namespace, pod_name):
|
||||||
return env_vars, secret_volumes
|
return env_vars, secret_volumes
|
||||||
|
|
||||||
|
|
||||||
def create_inspector_job(api, namespace, pod_name, volume_mounts, volumes):
|
def create_inspector_job(
|
||||||
|
api, namespace: str, pod_name: str, volume_mounts: list, volumes: list
|
||||||
|
):
|
||||||
|
"""Create a Krayt inspector job with the given mounts"""
|
||||||
timestamp = int(time.time())
|
timestamp = int(time.time())
|
||||||
job_name = f"{pod_name}-inspector-{timestamp}"
|
job_name = f"{pod_name}-krayt-{timestamp}"
|
||||||
|
|
||||||
# Get environment variables and secrets from the target pod
|
# Get environment variables and secrets from the target pod
|
||||||
env_vars, secret_volumes = get_pod_env_and_secrets(api, namespace, pod_name)
|
env_vars, secret_volumes = get_pod_env_and_secrets(api, namespace, pod_name)
|
||||||
|
|
@ -258,7 +278,8 @@ def create_inspector_job(api, namespace, pod_name, volume_mounts, volumes):
|
||||||
# Format mount and PVC info for MOTD
|
# Format mount and PVC info for MOTD
|
||||||
mount_info = []
|
mount_info = []
|
||||||
for vm in formatted_mounts:
|
for vm in formatted_mounts:
|
||||||
mount_info.append(f"{vm['name']}:{vm['mountPath']}")
|
if vm:
|
||||||
|
mount_info.append(f"{vm['name']}:{vm['mountPath']}")
|
||||||
|
|
||||||
pvc_info = []
|
pvc_info = []
|
||||||
for v in volumes:
|
for v in volumes:
|
||||||
|
|
@ -271,16 +292,16 @@ def create_inspector_job(api, namespace, pod_name, volume_mounts, volumes):
|
||||||
"metadata": {
|
"metadata": {
|
||||||
"name": job_name,
|
"name": job_name,
|
||||||
"namespace": namespace,
|
"namespace": namespace,
|
||||||
"labels": {"app": "pvc-inspector"},
|
"labels": {"app": "krayt"},
|
||||||
},
|
},
|
||||||
"spec": {
|
"spec": {
|
||||||
"ttlSecondsAfterFinished": 0, # Delete immediately after completion
|
"ttlSecondsAfterFinished": 0, # Delete immediately after completion
|
||||||
"template": {
|
"template": {
|
||||||
"metadata": {"labels": {"app": "pvc-inspector"}},
|
"metadata": {"labels": {"app": "krayt"}},
|
||||||
"spec": {
|
"spec": {
|
||||||
"containers": [
|
"containers": [
|
||||||
{
|
{
|
||||||
"name": "inspector",
|
"name": "krayt",
|
||||||
"image": "alpine:latest", # Use Alpine as base for package management
|
"image": "alpine:latest", # Use Alpine as base for package management
|
||||||
"command": [
|
"command": [
|
||||||
"sh",
|
"sh",
|
||||||
|
|
@ -304,8 +325,10 @@ apk add ripgrep exa ncdu dust \
|
||||||
update_motd() {
|
update_motd() {
|
||||||
cat << EOF > /etc/motd
|
cat << EOF > /etc/motd
|
||||||
====================================
|
====================================
|
||||||
PVC Inspector Pod
|
Krayt Dragon's Lair
|
||||||
====================================
|
====================================
|
||||||
|
"Inside every volume lies a pearl of wisdom waiting to be discovered."
|
||||||
|
|
||||||
Mounted Volumes:
|
Mounted Volumes:
|
||||||
$(echo "$MOUNTS" | tr ',' '\\n' | sed 's/^/- /')
|
$(echo "$MOUNTS" | tr ',' '\\n' | sed 's/^/- /')
|
||||||
|
|
||||||
|
|
@ -318,7 +341,7 @@ $(for d in /mnt/secrets/*; do if [ -d "$d" ]; then echo "- $(basename $d)"; fi;
|
||||||
Environment Variables:
|
Environment Variables:
|
||||||
$(env | sort | sed 's/^/- /')
|
$(env | sort | sed 's/^/- /')
|
||||||
|
|
||||||
Available Tools:
|
Your Hunting Tools:
|
||||||
File Navigation:
|
File Navigation:
|
||||||
- lf: Terminal file manager (run 'lf')
|
- lf: Terminal file manager (run 'lf')
|
||||||
- exa: Modern ls (run 'ls', 'll', or 'tree')
|
- exa: Modern ls (run 'ls', 'll', or 'tree')
|
||||||
|
|
@ -377,7 +400,7 @@ alias cat='bat --paging=never'
|
||||||
|
|
||||||
# Function to show detailed tool help
|
# Function to show detailed tool help
|
||||||
tools-help() {
|
tools-help() {
|
||||||
echo "PVC Inspector Tools Guide:"
|
echo "Krayt Dragon Hunter's Guide:"
|
||||||
echo
|
echo
|
||||||
echo "File Navigation:"
|
echo "File Navigation:"
|
||||||
echo " lf : Navigate with arrow keys, q to quit, h for help"
|
echo " lf : Navigate with arrow keys, q to quit, h for help"
|
||||||
|
|
@ -475,16 +498,37 @@ PROTECTED_NAMESPACES = {
|
||||||
"linkerd",
|
"linkerd",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def version_callback(value: bool):
|
||||||
|
if value:
|
||||||
|
typer.echo(f"Version: {KRAYT_VERSION}")
|
||||||
|
raise typer.Exit()
|
||||||
|
|
||||||
|
|
||||||
|
@app.callback(invoke_without_command=True)
|
||||||
|
def main(
|
||||||
|
ctx: typer.Context,
|
||||||
|
version: bool = typer.Option(
|
||||||
|
False, "--version", "-v", help="Show version", callback=version_callback
|
||||||
|
),
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Krack open a Krayt dragon!
|
||||||
|
"""
|
||||||
|
if ctx.invoked_subcommand is None:
|
||||||
|
ctx.get_help()
|
||||||
|
|
||||||
|
|
||||||
@app.command()
|
@app.command()
|
||||||
def exec_inspector(
|
def exec(
|
||||||
namespace: Optional[str] = typer.Option(
|
namespace: Optional[str] = typer.Option(
|
||||||
None,
|
None,
|
||||||
help="Kubernetes namespace. If not specified, will search for inspectors across all namespaces.",
|
help="Kubernetes namespace. If not specified, will search for inspectors across all namespaces.",
|
||||||
),
|
),
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Execute a shell in a running inspector pod. If multiple inspectors are found,
|
Enter the Krayt dragon's lair! Connect to a running inspector pod.
|
||||||
presents a fuzzy finder to select one.
|
If multiple inspectors are found, you'll get to choose which one to explore.
|
||||||
"""
|
"""
|
||||||
config.load_kube_config()
|
config.load_kube_config()
|
||||||
batch_api = client.BatchV1Api()
|
batch_api = client.BatchV1Api()
|
||||||
|
|
@ -493,13 +537,11 @@ def exec_inspector(
|
||||||
if namespace:
|
if namespace:
|
||||||
logging.debug(f"Listing jobs in namespace {namespace}")
|
logging.debug(f"Listing jobs in namespace {namespace}")
|
||||||
jobs = batch_api.list_namespaced_job(
|
jobs = batch_api.list_namespaced_job(
|
||||||
namespace=namespace, label_selector="app=pvc-inspector"
|
namespace=namespace, label_selector="app=krayt"
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
logging.debug("Listing jobs in all namespaces")
|
logging.debug("Listing jobs in all namespaces")
|
||||||
jobs = batch_api.list_job_for_all_namespaces(
|
jobs = batch_api.list_job_for_all_namespaces(label_selector="app=krayt")
|
||||||
label_selector="app=pvc-inspector"
|
|
||||||
)
|
|
||||||
|
|
||||||
running_inspectors = []
|
running_inspectors = []
|
||||||
for job in jobs.items:
|
for job in jobs.items:
|
||||||
|
|
@ -507,11 +549,13 @@ def exec_inspector(
|
||||||
v1 = client.CoreV1Api()
|
v1 = client.CoreV1Api()
|
||||||
pods = v1.list_namespaced_pod(
|
pods = v1.list_namespaced_pod(
|
||||||
namespace=job.metadata.namespace,
|
namespace=job.metadata.namespace,
|
||||||
label_selector=f"job-name={job.metadata.name}"
|
label_selector=f"job-name={job.metadata.name}",
|
||||||
)
|
)
|
||||||
for pod in pods.items:
|
for pod in pods.items:
|
||||||
if pod.status.phase == "Running":
|
if pod.status.phase == "Running":
|
||||||
running_inspectors.append((pod.metadata.name, pod.metadata.namespace))
|
running_inspectors.append(
|
||||||
|
(pod.metadata.name, pod.metadata.namespace)
|
||||||
|
)
|
||||||
|
|
||||||
if not running_inspectors:
|
if not running_inspectors:
|
||||||
typer.echo("No running inspector pods found.")
|
typer.echo("No running inspector pods found.")
|
||||||
|
|
@ -527,7 +571,10 @@ def exec_inspector(
|
||||||
|
|
||||||
# Execute the shell
|
# Execute the shell
|
||||||
typer.echo(f"Connecting to inspector {pod_namespace}/{pod_name}...")
|
typer.echo(f"Connecting to inspector {pod_namespace}/{pod_name}...")
|
||||||
os.execvp("kubectl", ["kubectl", "exec", "-it", "-n", pod_namespace, pod_name, "--", "sh", "-l"])
|
os.execvp(
|
||||||
|
"kubectl",
|
||||||
|
["kubectl", "exec", "-it", "-n", pod_namespace, pod_name, "--", "sh", "-l"],
|
||||||
|
)
|
||||||
|
|
||||||
except client.exceptions.ApiException as e:
|
except client.exceptions.ApiException as e:
|
||||||
logging.error(f"Failed to list jobs: {e}")
|
logging.error(f"Failed to list jobs: {e}")
|
||||||
|
|
@ -536,19 +583,21 @@ def exec_inspector(
|
||||||
|
|
||||||
|
|
||||||
@app.command()
|
@app.command()
|
||||||
def cleanup_inspectors(
|
def clean(
|
||||||
namespace: Optional[str] = typer.Option(
|
namespace: Optional[str] = typer.Option(
|
||||||
None,
|
None,
|
||||||
help="Kubernetes namespace. If not specified, will cleanup in all namespaces.",
|
help="Kubernetes namespace. If not specified, will cleanup in all namespaces.",
|
||||||
),
|
),
|
||||||
yes: bool = typer.Option(
|
yes: bool = typer.Option(
|
||||||
False,
|
False,
|
||||||
"--yes", "-y",
|
"--yes",
|
||||||
|
"-y",
|
||||||
help="Skip confirmation prompt.",
|
help="Skip confirmation prompt.",
|
||||||
),
|
),
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Delete all PVC inspector jobs in the specified namespace or all namespaces
|
Clean up after your hunt! Remove all Krayt inspector jobs.
|
||||||
|
Use --yes/-y to skip confirmation and clean up immediately.
|
||||||
"""
|
"""
|
||||||
config.load_kube_config()
|
config.load_kube_config()
|
||||||
batch_api = client.BatchV1Api()
|
batch_api = client.BatchV1Api()
|
||||||
|
|
@ -560,24 +609,28 @@ def cleanup_inspectors(
|
||||||
raise typer.Exit(1)
|
raise typer.Exit(1)
|
||||||
logging.debug(f"Listing jobs in namespace {namespace}")
|
logging.debug(f"Listing jobs in namespace {namespace}")
|
||||||
jobs = batch_api.list_namespaced_job(
|
jobs = batch_api.list_namespaced_job(
|
||||||
namespace=namespace, label_selector="app=pvc-inspector"
|
namespace=namespace, label_selector="app=krayt"
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
logging.debug("Listing jobs in all namespaces")
|
logging.debug("Listing jobs in all namespaces")
|
||||||
jobs = batch_api.list_job_for_all_namespaces(
|
jobs = batch_api.list_job_for_all_namespaces(label_selector="app=krayt")
|
||||||
label_selector="app=pvc-inspector"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Filter out jobs in protected namespaces
|
# Filter out jobs in protected namespaces
|
||||||
jobs.items = [job for job in jobs.items if job.metadata.namespace not in PROTECTED_NAMESPACES]
|
jobs.items = [
|
||||||
|
job
|
||||||
|
for job in jobs.items
|
||||||
|
if job.metadata.namespace not in PROTECTED_NAMESPACES
|
||||||
|
]
|
||||||
|
|
||||||
if not jobs.items:
|
if not jobs.items:
|
||||||
typer.echo("No PVC inspector jobs found.")
|
typer.echo("No Krayt inspector jobs found.")
|
||||||
return
|
return
|
||||||
|
|
||||||
# Show confirmation prompt
|
# Show confirmation prompt
|
||||||
if not yes:
|
if not yes:
|
||||||
job_list = "\n".join(f" {job.metadata.namespace}/{job.metadata.name}" for job in jobs.items)
|
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}")
|
typer.echo(f"The following inspector jobs will be deleted:\n{job_list}")
|
||||||
if not typer.confirm("Are you sure you want to continue?"):
|
if not typer.confirm("Are you sure you want to continue?"):
|
||||||
typer.echo("Operation cancelled.")
|
typer.echo("Operation cancelled.")
|
||||||
|
|
@ -611,15 +664,16 @@ def cleanup_inspectors(
|
||||||
|
|
||||||
|
|
||||||
@app.command()
|
@app.command()
|
||||||
def create_inspector(
|
def create(
|
||||||
namespace: Optional[str] = typer.Option(
|
namespace: Optional[str] = typer.Option(
|
||||||
None,
|
None,
|
||||||
help="Kubernetes namespace. If not specified, will search for pods across all namespaces.",
|
help="Kubernetes namespace. If not specified, will search for pods across all namespaces.",
|
||||||
),
|
),
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Create a PVC inspector job. If namespace is not specified, will search for pods across all namespaces.
|
Krack open a Krayt dragon! Create an inspector pod to explore what's inside your volumes.
|
||||||
The inspector job will be created in the same namespace as the selected pod.
|
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.
|
||||||
"""
|
"""
|
||||||
pods = get_pods(namespace)
|
pods = get_pods(namespace)
|
||||||
if not pods:
|
if not pods:
|
||||||
|
|
@ -642,5 +696,11 @@ def create_inspector(
|
||||||
typer.echo(yaml.dump(clean_dict(inspector_job), sort_keys=False))
|
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}")
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
app()
|
app()
|
||||||
25
scripts/generate_install_script.py
Executable file
25
scripts/generate_install_script.py
Executable file
|
|
@ -0,0 +1,25 @@
|
||||||
|
#!/usr/bin/env -S uv run --quiet --script
|
||||||
|
# /// script
|
||||||
|
# requires-python = ">=3.10"
|
||||||
|
# ///
|
||||||
|
|
||||||
|
import sys
|
||||||
|
|
||||||
|
|
||||||
|
def generate_install_script(version):
|
||||||
|
with open("scripts/install.sh.template", "r") as f:
|
||||||
|
template = f.read()
|
||||||
|
|
||||||
|
script = template.replace("{{VERSION}}", version)
|
||||||
|
|
||||||
|
with open("dist/install.sh", "w") as f:
|
||||||
|
f.write(script)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
if len(sys.argv) != 2:
|
||||||
|
print("Usage: generate_install_script.py VERSION", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
version = sys.argv[1]
|
||||||
|
generate_install_script(version)
|
||||||
89
scripts/get_release_notes.py
Executable file
89
scripts/get_release_notes.py
Executable file
|
|
@ -0,0 +1,89 @@
|
||||||
|
#!/usr/bin/env -S uv run --quiet --script
|
||||||
|
# /// script
|
||||||
|
# requires-python = ">=3.10"
|
||||||
|
# ///
|
||||||
|
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
|
||||||
|
|
||||||
|
def get_release_notes(version):
|
||||||
|
with open("CHANGELOG.md", "r") as f:
|
||||||
|
content = f.read()
|
||||||
|
|
||||||
|
sections = content.split("\n## ")
|
||||||
|
# First section won't start with ## since it's split on that
|
||||||
|
sections = ["## " + s if i > 0 else s for i, s in enumerate(sections)]
|
||||||
|
|
||||||
|
for section in sections:
|
||||||
|
if section.startswith(f"## {version}"):
|
||||||
|
install_instructions = f"""## Installation
|
||||||
|
|
||||||
|
You can install krayt using one of these methods:
|
||||||
|
|
||||||
|
> !krayt requires [uv](https://docs.astral.sh/uv/getting-started/installation/) to be installed
|
||||||
|
|
||||||
|
### Using i.jpillora.com (recommended)
|
||||||
|
|
||||||
|
``` bash
|
||||||
|
curl https://i.jpillora.com/waylonwalker/krayt | bash
|
||||||
|
```
|
||||||
|
|
||||||
|
### Direct install script
|
||||||
|
|
||||||
|
``` bash
|
||||||
|
curl -fsSL https://github.com/waylonwalker/krayt/releases/download/v{version}/install.sh | bash
|
||||||
|
```
|
||||||
|
|
||||||
|
### 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)"""
|
||||||
|
|
||||||
|
# Get help output for main command and all subcommands
|
||||||
|
try:
|
||||||
|
help_outputs = []
|
||||||
|
|
||||||
|
# Get main help output
|
||||||
|
main_help = subprocess.check_output(
|
||||||
|
["./krayt.py", "--help"],
|
||||||
|
stderr=subprocess.STDOUT,
|
||||||
|
universal_newlines=True,
|
||||||
|
)
|
||||||
|
help_outputs.append(("Main Command", main_help))
|
||||||
|
|
||||||
|
# Get help for each subcommand
|
||||||
|
subcommands = ["create", "exec", "clean", "version"]
|
||||||
|
for cmd in subcommands:
|
||||||
|
cmd_help = subprocess.check_output(
|
||||||
|
["./krayt.py", cmd, "--help"],
|
||||||
|
stderr=subprocess.STDOUT,
|
||||||
|
universal_newlines=True,
|
||||||
|
)
|
||||||
|
help_outputs.append((f"Subcommand: {cmd}", cmd_help))
|
||||||
|
|
||||||
|
# Format all help outputs
|
||||||
|
help_text = "\n\n".join(
|
||||||
|
f"### {title}\n\n``` bash\n{output}```"
|
||||||
|
for title, output in help_outputs
|
||||||
|
)
|
||||||
|
|
||||||
|
return f"{section.strip()}\n\n{install_instructions.format(version=version)}\n\n## Command Line Usage\n\n{help_text}"
|
||||||
|
except subprocess.CalledProcessError as e:
|
||||||
|
return f"{section.strip()}\n\n{install_instructions.format(version=version)}\n\n## Command Line Usage\n\nError getting help: {e.output}"
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
if len(sys.argv) != 2:
|
||||||
|
print("Usage: get_release_notes.py VERSION", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
version = sys.argv[1]
|
||||||
|
notes = get_release_notes(version)
|
||||||
|
if notes:
|
||||||
|
print(notes)
|
||||||
|
else:
|
||||||
|
print(f"Error: No release notes found for version {version}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
126
scripts/install.sh.template
Normal file
126
scripts/install.sh.template
Normal file
|
|
@ -0,0 +1,126 @@
|
||||||
|
#!/bin/bash
|
||||||
|
if [ "$DEBUG" == "1" ]; then
|
||||||
|
set -x
|
||||||
|
fi
|
||||||
|
TMP_DIR=$(mktemp -d -t krayt-installer-XXXXXXXXXX)
|
||||||
|
function cleanup {
|
||||||
|
rm -rf $TMP_DIR >/dev/null
|
||||||
|
}
|
||||||
|
function fail {
|
||||||
|
cleanup
|
||||||
|
msg=$1
|
||||||
|
echo "============"
|
||||||
|
echo "Error: $msg" 1>&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
function check_deps {
|
||||||
|
if ! command -v uv &>/dev/null; then
|
||||||
|
echo " Error: uv is not installed"
|
||||||
|
echo "krayt requires uv to run. You can install it with:"
|
||||||
|
echo " curl -LsSf https://astral.sh/uv/install.sh | sh"
|
||||||
|
echo ""
|
||||||
|
echo "Or visit: https://github.com/astral/uv for more installation options"
|
||||||
|
echo ""
|
||||||
|
fail "uv not found"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
function install {
|
||||||
|
#settings
|
||||||
|
USER="waylonwalker"
|
||||||
|
PROG="krayt"
|
||||||
|
ASPROG="krayt"
|
||||||
|
MOVE="true"
|
||||||
|
RELEASE="{{VERSION}}"
|
||||||
|
INSECURE="false"
|
||||||
|
OUT_DIR="/usr/local/bin"
|
||||||
|
GH="https://github.com"
|
||||||
|
#bash check
|
||||||
|
[ ! "$BASH_VERSION" ] && fail "Please use bash instead"
|
||||||
|
[ ! -d $OUT_DIR ] && fail "output directory missing: $OUT_DIR"
|
||||||
|
#dependency check, assume we are a standard POISX machine
|
||||||
|
which find >/dev/null || fail "find not installed"
|
||||||
|
which xargs >/dev/null || fail "xargs not installed"
|
||||||
|
which sort >/dev/null || fail "sort not installed"
|
||||||
|
which tail >/dev/null || fail "tail not installed"
|
||||||
|
which cut >/dev/null || fail "cut not installed"
|
||||||
|
which du >/dev/null || fail "du not installed"
|
||||||
|
#choose an HTTP client
|
||||||
|
GET=""
|
||||||
|
if which curl >/dev/null; then
|
||||||
|
GET="curl"
|
||||||
|
if [[ $INSECURE = "true" ]]; then GET="$GET --insecure"; fi
|
||||||
|
GET="$GET --fail -# -L"
|
||||||
|
elif which wget >/dev/null; then
|
||||||
|
GET="wget"
|
||||||
|
if [[ $INSECURE = "true" ]]; then GET="$GET --no-check-certificate"; fi
|
||||||
|
GET="$GET -qO-"
|
||||||
|
else
|
||||||
|
fail "neither wget/curl are installed"
|
||||||
|
fi
|
||||||
|
#find OS
|
||||||
|
case $(uname -s) in
|
||||||
|
Darwin) OS="darwin" ;;
|
||||||
|
Linux) OS="linux" ;;
|
||||||
|
*) fail "unknown os: $(uname -s)" ;;
|
||||||
|
esac
|
||||||
|
#find ARCH
|
||||||
|
if uname -m | grep -E '(arm|aarch)64' >/dev/null; then
|
||||||
|
ARCH="aarch64"
|
||||||
|
elif uname -m | grep 64 >/dev/null; then
|
||||||
|
ARCH="x86_64"
|
||||||
|
else
|
||||||
|
fail "unknown arch: $(uname -m)"
|
||||||
|
fi
|
||||||
|
#choose from asset list
|
||||||
|
URL=""
|
||||||
|
FTYPE=""
|
||||||
|
VERSION=${RELEASE#v}
|
||||||
|
if [[ $VERSION == "" ]]; then
|
||||||
|
VERSION=$(curl -s https://api.github.com/repos/$USER/$PROG/releases/latest | grep -o '"tag_name": "[^"]*' | cut -d'"' -f4)
|
||||||
|
fi
|
||||||
|
if [[ $VERSION == "" ]]; then
|
||||||
|
fail "cannot find latest version"
|
||||||
|
fi
|
||||||
|
VERSION=${VERSION#v}
|
||||||
|
ASSET_URL="$GH/$USER/$PROG/releases/download/v$VERSION/${PROG}-${VERSION}-${ARCH}-unknown-${OS}-gnu.tar.gz"
|
||||||
|
echo "Installing $PROG v$VERSION..."
|
||||||
|
echo "Downloading binary from $ASSET_URL"
|
||||||
|
#enter tempdir
|
||||||
|
cd $TMP_DIR
|
||||||
|
#download and unpack
|
||||||
|
if [[ $ASSET_URL =~ \.gz$ ]]; then
|
||||||
|
which tar >/dev/null || fail "tar not installed"
|
||||||
|
if [[ $GET =~ ^curl ]]; then
|
||||||
|
curl -s ${ASSET_URL} | tar zx || fail "download failed"
|
||||||
|
else
|
||||||
|
wget -qO- ${ASSET_URL} | tar zx || fail "download failed"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
fail "unknown file type: $ASSET_URL"
|
||||||
|
fi
|
||||||
|
#check for error
|
||||||
|
cd ${PROG}-${VERSION}-${ARCH}-unknown-${OS}-gnu
|
||||||
|
#move binary
|
||||||
|
if [[ -f "${PROG}.py" ]]; then
|
||||||
|
chmod +x "${PROG}.py"
|
||||||
|
if [[ $MOVE == "true" ]]; then
|
||||||
|
echo "Moving binary to $OUT_DIR/$ASPROG"
|
||||||
|
# Create a wrapper script to ensure uv is used
|
||||||
|
cat > "$OUT_DIR/$ASPROG" << EOF
|
||||||
|
#!/bin/bash
|
||||||
|
exec uv run --quiet --script "$OUT_DIR/${ASPROG}.py" "\$@"
|
||||||
|
EOF
|
||||||
|
chmod +x "$OUT_DIR/$ASPROG"
|
||||||
|
mv "${PROG}.py" "$OUT_DIR/${ASPROG}.py" || fail "Cannot move binary to $OUT_DIR"
|
||||||
|
else
|
||||||
|
echo "Moving binary to $OUT_DIR/${ASPROG}.py"
|
||||||
|
mv "${PROG}.py" "$OUT_DIR/${ASPROG}.py" || fail "Cannot move binary to $OUT_DIR"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
fail "cannot find binary"
|
||||||
|
fi
|
||||||
|
echo "Installation complete!"
|
||||||
|
cleanup
|
||||||
|
}
|
||||||
|
check_deps
|
||||||
|
install
|
||||||
1
version
Normal file
1
version
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
0.0.0
|
||||||
Loading…
Add table
Add a link
Reference in a new issue