Compare commits

..

No commits in common. "main" and "v0.3.0" have entirely different histories.
main ... v0.3.0

10 changed files with 201 additions and 396 deletions

View file

@ -12,7 +12,7 @@ permissions:
packages: none
id-token: write
jobs:
pypi-release-krayt:
release-krayt:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
@ -22,26 +22,3 @@ jobs:
env:
# required for gh release
GH_TOKEN: ${{ github.token }}
- 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
shell: bash
- name: Install uv
run: |
curl -LsSf https://astral.sh/uv/0.6.16/install.sh | sh
shell: bash
- name: Install hatch
run: |
uv tool install hatch
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

View file

@ -1,29 +1,3 @@
## 0.4.3
- working out binary release process
## 0.4.2
- working out binary release process
## 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
- generic templates endpoint for cli
- better motd for volume mounts
## 0.3.0
- created pypi release
- updated releases to use pyapp
- all new package
- port forward support
- additional_packages support
## 0.2.0
### Added

View file

@ -3,7 +3,7 @@ delete-tag:
set -euo pipefail
# Get the version
VERSION=$(hatch version)
VERSION=$(cat version)
# Delete the tag
git tag -d "v$VERSION"
@ -14,65 +14,83 @@ delete-release:
set -euo pipefail
# Get the version
VERSION=$(hatch version)
VERSION=$(cat version)
# Delete the release
gh release delete "v$VERSION"
create-tag:
#!/usr/bin/env bash
VERSION=$(hatch version)
VERSION=$(cat version)
git tag -a "v$VERSION" -m "Release v$VERSION"
git push origin "v$VERSION"
create-archives:
#!/usr/bin/env bash
VERSION=$(hatch version)
VERSION=$(cat version)
rm -rf dist build
hatch build -t binary
krayt_bin=dist/binary/krayt-${VERSION}
mkdir -p dist
# Create the binary for each platform
for platform in "x86_64-unknown-linux-gnu" "aarch64-unknown-linux-gnu"; do
outbin="krayt-${VERSION}-${platform}"
outdir="krayt-${VERSION}-${platform}"
mkdir -p "dist/${outdir}"
# Copy the Python script and update version
cp ${krayt_bin} "dist/binary/${outbin}"
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
./scripts/generate_install_script.py "$VERSION"
chmod +x dist/install.sh
create-release: create-tag create-archives
#!/usr/bin/env bash
VERSION=$(hatch version)
VERSION=$(cat version)
./scripts/get_release_notes.py "$VERSION" > release_notes.tmp
# 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
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=$(hatch version)
VERSION=$(cat 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"

View file

@ -1 +1 @@
__version__ = "0.4.3"
__version__ = "0.3.0"

View file

@ -6,7 +6,7 @@ from typer import Typer
app = Typer()
app.add_typer(templates_app, name="template", no_args_is_help=True)
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)

View file

@ -1,6 +1,5 @@
import iterfzf
from krayt.templates import env
from kubernetes.stream import stream
from kubernetes import client, config
import logging
import os
@ -9,12 +8,6 @@ import typer
from typing import Any, List, Optional
import yaml
from krayt.__about__ import __version__
import sys
import tty
import termios
import select
import signal
import json
logging.basicConfig(level=logging.WARNING)
@ -42,8 +35,8 @@ def format_volume_mount(vm: client.V1VolumeMount) -> dict[str, Any]:
return clean_dict(
{
"name": vm.name,
"mount_path": vm.mount_path,
"read_only": vm.read_only if vm.read_only else None,
"mountPath": vm.mount_path,
"readOnly": vm.read_only if vm.read_only else None,
}
)
@ -151,17 +144,6 @@ 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()
@ -278,81 +260,92 @@ 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)
secret_mounts = [
client.V1VolumeMount(
name=vol.name,
mount_path=f"/mnt/secrets/{vol.secret.secret_name}",
read_only=True,
# 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,
}
)
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)
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
]
# Format mount and PVC info for MOTD
mount_info = []
for vm in formatted_mounts:
if vm:
mount_info.append(f"{vm['name']}:{vm['mountPath']}")
template = env.get_template("base.sh")
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
command = template.render(
volumes=volumes,
pvcs=None,
pvcs=pvcs,
additional_packages=additional_packages,
pre_init_scripts=None,
post_init_scripts=None,
pre_init_hooks=None,
post_init_hooks=None,
pre_init_scripts=pre_init_scripts,
post_init_scripts=post_init_scripts,
pre_init_hooks=pre_init_hooks,
post_init_hooks=post_init_hooks,
)
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)]
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,
)
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
"restartPolicy": "Never",
},
},
},
}
return inspector_job
PROTECTED_NAMESPACES = {
@ -445,146 +438,18 @@ def get_pod(namespace: Optional[str] = None):
return pod_name, pod_namespace
def interactive_exec(pod_name: str, namespace: str):
# Load kubeconfig from local context (or use load_incluster_config if running inside the cluster)
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", "-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
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
try:
while resp and 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 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 Exception:
pass
# Always restore terminal settings
termios.tcsetattr(sys.stdin, termios.TCSADRAIN, oldtty)
print("\nConnection closed", file=sys.stderr)
@app.command()
def exec(
namespace: Optional[str] = typer.Option(
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.
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
client.CoreV1Api()
pod_name, pod_namespace = get_pod(namespace)
try:
pod_name, pod_namespace = get_pod(namespace)
exec_command = [
"kubectl",
@ -594,15 +459,11 @@ def exec(
pod_namespace,
pod_name,
"--",
shell,
"/bin/bash",
"-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()
@ -779,11 +640,6 @@ 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.
@ -791,15 +647,16 @@ 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 = namespace
selected_pod = clone
selected_namespace = None
selected_pod = None
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
get_namespaces(namespace)
pods = get_pods(namespace, label_selector="app!=krayt")
pods = get_pods(namespace, label_selector=None)
if not pods:
typer.echo("No pods found.")
raise typer.Exit(1)
@ -815,6 +672,9 @@ 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)
@ -834,21 +694,7 @@ def create(
)
# Output the job manifest
api_client = client.ApiClient()
job_dict = api_client.sanitize_for_serialization(inspector_job)
job_yaml = yaml.dump(job_dict, sort_keys=False)
if apply:
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)
typer.echo(yaml.dump(clean_dict(inspector_job), sort_keys=False))
@app.command()
@ -916,10 +762,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()

View file

@ -13,8 +13,7 @@ def list():
@app.command()
def render(
template_name: Optional[str] = typer.Option("base.sh", "--template-name", "-t"),
def base(
volumes: Optional[List[str]] = typer.Option(
None,
"--volume",
@ -49,6 +48,7 @@ def render(
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 render(
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)

View file

@ -5,11 +5,11 @@ cat <<EOF >/etc/motd
└───────────────────────────────────┘
"Inside every volume lies a pearl of wisdom waiting to be discovered."
{%- if mounts %}
{%- if volumes %}
Mounted Volumes:
{%- for mount in mounts %}
- {{ mount.name }}:{{ mount.mount_path }}
{%- for volume in volumes %}
- {{ volume }}
{%- endfor %}
{%- endif %}

View file

@ -21,11 +21,7 @@ def get_release_notes(version):
You can install krayt using one of these methods:
## pypi
``` bash
pip install krayt
```
> !krayt requires [uv](https://docs.astral.sh/uv/getting-started/installation/) to be installed
### Using i.jpillora.com (recommended)
@ -41,8 +37,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)
- [aarch64-unknown-linux-gnu](https://github.com/waylonwalker/krayt/releases/download/v{version}/krayt-{version}-aarch64-unknown-linux-gnu)"""
- [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:
@ -50,23 +46,17 @@ You can also manually download the archive for your platform from the releases p
# Get main help output
main_help = subprocess.check_output(
["krayt", "--help"],
["./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",
"pod",
]
subcommands = ["create", "exec", "clean", "version"]
for cmd in subcommands:
cmd_help = subprocess.check_output(
["krayt", cmd, "--help"],
["./krayt.py", cmd, "--help"],
stderr=subprocess.STDOUT,
universal_newlines=True,
)

View file

@ -148,11 +148,11 @@ function install {
FTYPE=""
case "${OS}_${ARCH}" in
"linux_amd64")
URL="https://github.com/WaylonWalker/krayt/releases/download/v${RELEASE}/krayt-${RELEASE}-x86_64-unknown-linux-gnu.tar.gz"
URL="https://github.com/WaylonWalker/nvim-manager/releases/download/v${RELEASE}/nvim-manager-${RELEASE}-x86_64-unknown-linux-gnu.tar.gz"
FTYPE=".tar.gz"
;;
"linux_arm64")
URL="https://github.com/WaylonWalker/krayt/releases/download/v${RELEASE}/krayt-${RELEASE}-aarch64-unknown-linux-gnu.tar.gz"
URL="https://github.com/WaylonWalker/nvim-manager/releases/download/v${RELEASE}/nvim-manager-${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" >"krayt_${OS}_${ARCH}" || fail "download failed"
bash -c "$GET $URL" >"nvim-manager_${OS}_${ARCH}" || fail "download failed"
else
fail "unknown file type: $FTYPE"
fi