Compare commits
25 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0f9e70267e | ||
|
|
f8c44999fe | ||
|
|
45e5dd74aa | ||
|
|
eb54e314ff | ||
|
|
d6056cead2 | ||
|
|
be6ee91098 | ||
|
|
1fee6e2c97 | ||
|
|
2fbc15ae84 | ||
|
|
b64e635b71 | ||
|
|
7511cace42 | ||
|
|
1138f695f4 | ||
|
|
0a6e19731d | ||
|
|
64077d3ba2 | ||
|
|
681194fc1f | ||
|
|
ed449034f7 | ||
|
|
17c088526b | ||
|
|
225edce32d | ||
|
|
db96853646 | ||
|
|
3ce69baf26 | ||
|
|
7daa9a3874 | ||
|
|
cc425cf812 | ||
|
|
ae918bf5f2 | ||
|
|
2f16036c8e | ||
|
|
82102c4adf | ||
|
|
1959e1a39b |
10 changed files with 396 additions and 201 deletions
25
.github/workflows/release-pypi.yaml
vendored
25
.github/workflows/release-pypi.yaml
vendored
|
|
@ -12,7 +12,7 @@ permissions:
|
||||||
packages: none
|
packages: none
|
||||||
id-token: write
|
id-token: write
|
||||||
jobs:
|
jobs:
|
||||||
release-krayt:
|
pypi-release-krayt:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
@ -22,3 +22,26 @@ jobs:
|
||||||
env:
|
env:
|
||||||
# required for gh release
|
# required for gh release
|
||||||
GH_TOKEN: ${{ github.token }}
|
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
|
||||||
|
|
|
||||||
26
CHANGELOG.md
26
CHANGELOG.md
|
|
@ -1,3 +1,29 @@
|
||||||
|
## 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
|
## 0.2.0
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
|
||||||
76
justfile
76
justfile
|
|
@ -3,7 +3,7 @@ delete-tag:
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
# Get the version
|
# Get the version
|
||||||
VERSION=$(cat version)
|
VERSION=$(hatch version)
|
||||||
|
|
||||||
# Delete the tag
|
# Delete the tag
|
||||||
git tag -d "v$VERSION"
|
git tag -d "v$VERSION"
|
||||||
|
|
@ -14,83 +14,65 @@ delete-release:
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
# Get the version
|
# Get the version
|
||||||
VERSION=$(cat version)
|
VERSION=$(hatch version)
|
||||||
|
|
||||||
# Delete the release
|
# Delete the release
|
||||||
gh release delete "v$VERSION"
|
gh release delete "v$VERSION"
|
||||||
|
|
||||||
create-tag:
|
create-tag:
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
VERSION=$(cat version)
|
VERSION=$(hatch version)
|
||||||
git tag -a "v$VERSION" -m "Release v$VERSION"
|
git tag -a "v$VERSION" -m "Release v$VERSION"
|
||||||
git push origin "v$VERSION"
|
git push origin "v$VERSION"
|
||||||
|
|
||||||
create-archives:
|
create-archives:
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
VERSION=$(cat version)
|
VERSION=$(hatch version)
|
||||||
rm -rf dist build
|
rm -rf dist build
|
||||||
mkdir -p dist
|
hatch build -t binary
|
||||||
|
|
||||||
|
krayt_bin=dist/binary/krayt-${VERSION}
|
||||||
|
|
||||||
# Create the binary for each platform
|
# Create the binary for each platform
|
||||||
for platform in "x86_64-unknown-linux-gnu" "aarch64-unknown-linux-gnu"; do
|
for platform in "x86_64-unknown-linux-gnu" "aarch64-unknown-linux-gnu"; do
|
||||||
outdir="krayt-${VERSION}-${platform}"
|
outbin="krayt-${VERSION}-${platform}"
|
||||||
mkdir -p "dist/${outdir}"
|
|
||||||
|
|
||||||
# Copy the Python script and update version
|
# Copy the Python script and update version
|
||||||
cp krayt.py "dist/${outdir}/krayt.py"
|
cp ${krayt_bin} "dist/binary/${outbin}"
|
||||||
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
|
done
|
||||||
|
|
||||||
# Generate install.sh
|
# Generate install.sh
|
||||||
./scripts/generate_install_script.py "$VERSION"
|
# ./scripts/generate_install_script.py "$VERSION"
|
||||||
chmod +x dist/install.sh
|
# chmod +x dist/install.sh
|
||||||
|
|
||||||
create-release: create-tag create-archives
|
create-release: create-tag create-archives
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
VERSION=$(cat version)
|
VERSION=$(hatch version)
|
||||||
./scripts/get_release_notes.py "$VERSION" > release_notes.tmp
|
./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" \
|
gh release create "v$VERSION" \
|
||||||
--title "v$VERSION" \
|
--title "v$VERSION" \
|
||||||
--notes-file release_notes.tmp \
|
--notes-file release_notes.tmp \
|
||||||
dist/krayt-${VERSION}-x86_64-unknown-linux-gnu.tar.gz \
|
dist/binary/krayt-${VERSION} \
|
||||||
dist/krayt-${VERSION}-x86_64-unknown-linux-gnu.tar.gz.sha256 \
|
dist/binary/krayt-${VERSION}-aarch64-unknown-linux-gnu \
|
||||||
dist/krayt-${VERSION}-aarch64-unknown-linux-gnu.tar.gz \
|
dist/binary/krayt-${VERSION}-x86_64-unknown-linux-gnu
|
||||||
dist/krayt-${VERSION}-aarch64-unknown-linux-gnu.tar.gz.sha256 \
|
fi
|
||||||
dist/install.sh
|
|
||||||
rm release_notes.tmp
|
rm release_notes.tmp
|
||||||
|
|
||||||
preview-release-notes:
|
preview-release-notes:
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
VERSION=$(cat version)
|
VERSION=$(hatch version)
|
||||||
./scripts/get_release_notes.py "$VERSION" | less -R
|
./scripts/get_release_notes.py "$VERSION" | less -R
|
||||||
|
|
||||||
release: create-release
|
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"
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1 +1 @@
|
||||||
__version__ = "0.3.0"
|
__version__ = "0.4.3"
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ from typer import Typer
|
||||||
|
|
||||||
app = 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.add_typer(pod_app, name="pod", no_args_is_help=True)
|
||||||
app.command(name="create")(create)
|
app.command(name="create")(create)
|
||||||
app.command(name="c")(create)
|
app.command(name="c")(create)
|
||||||
|
|
|
||||||
330
krayt/cli/pod.py
330
krayt/cli/pod.py
|
|
@ -1,5 +1,6 @@
|
||||||
import iterfzf
|
import iterfzf
|
||||||
from krayt.templates import env
|
from krayt.templates import env
|
||||||
|
from kubernetes.stream import stream
|
||||||
from kubernetes import client, config
|
from kubernetes import client, config
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
|
|
@ -8,6 +9,12 @@ import typer
|
||||||
from typing import Any, List, Optional
|
from typing import Any, List, Optional
|
||||||
import yaml
|
import yaml
|
||||||
from krayt.__about__ import __version__
|
from krayt.__about__ import __version__
|
||||||
|
import sys
|
||||||
|
import tty
|
||||||
|
import termios
|
||||||
|
import select
|
||||||
|
import signal
|
||||||
|
import json
|
||||||
|
|
||||||
|
|
||||||
logging.basicConfig(level=logging.WARNING)
|
logging.basicConfig(level=logging.WARNING)
|
||||||
|
|
@ -35,8 +42,8 @@ def format_volume_mount(vm: client.V1VolumeMount) -> dict[str, Any]:
|
||||||
return clean_dict(
|
return clean_dict(
|
||||||
{
|
{
|
||||||
"name": vm.name,
|
"name": vm.name,
|
||||||
"mountPath": vm.mount_path,
|
"mount_path": vm.mount_path,
|
||||||
"readOnly": vm.read_only if vm.read_only else None,
|
"read_only": vm.read_only if vm.read_only else None,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -144,6 +151,17 @@ def get_pods(
|
||||||
raise typer.Exit(1)
|
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):
|
def get_pod_spec(pod_name, namespace):
|
||||||
config.load_kube_config()
|
config.load_kube_config()
|
||||||
v1 = client.CoreV1Api()
|
v1 = client.CoreV1Api()
|
||||||
|
|
@ -260,92 +278,81 @@ def create_inspector_job(
|
||||||
pre_init_hooks: Optional[List[str]] = None,
|
pre_init_hooks: Optional[List[str]] = None,
|
||||||
post_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())
|
timestamp = int(time.time())
|
||||||
job_name = f"{pod_name}-krayt-{timestamp}"
|
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)
|
env_vars, secret_volumes = get_env_vars_and_secret_volumes(api, namespace)
|
||||||
|
|
||||||
# Add secret volumes to our volumes list
|
|
||||||
volumes.extend(secret_volumes)
|
volumes.extend(secret_volumes)
|
||||||
|
|
||||||
# Create corresponding volume mounts for secrets
|
secret_mounts = [
|
||||||
secret_mounts = []
|
client.V1VolumeMount(
|
||||||
for vol in secret_volumes:
|
name=vol.name,
|
||||||
secret_mounts.append(
|
mount_path=f"/mnt/secrets/{vol.secret.secret_name}",
|
||||||
{
|
read_only=True,
|
||||||
"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 = [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)
|
formatted_mounts.extend(secret_mounts)
|
||||||
|
|
||||||
# Format mount and PVC info for MOTD
|
pvc_info = [
|
||||||
mount_info = []
|
f"{v.name}:{v.persistent_volume_claim.claim_name}"
|
||||||
for vm in formatted_mounts:
|
for v in volumes
|
||||||
if vm:
|
if hasattr(v, "persistent_volume_claim") and v.persistent_volume_claim
|
||||||
mount_info.append(f"{vm['name']}:{vm['mountPath']}")
|
]
|
||||||
|
|
||||||
pvc_info = []
|
template = env.get_template("base.sh")
|
||||||
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(
|
command = template.render(
|
||||||
volumes=volumes,
|
volumes=volumes,
|
||||||
pvcs=pvcs,
|
pvcs=None,
|
||||||
additional_packages=additional_packages,
|
additional_packages=additional_packages,
|
||||||
pre_init_scripts=pre_init_scripts,
|
pre_init_scripts=None,
|
||||||
post_init_scripts=post_init_scripts,
|
post_init_scripts=None,
|
||||||
pre_init_hooks=pre_init_hooks,
|
pre_init_hooks=None,
|
||||||
post_init_hooks=post_init_hooks,
|
post_init_hooks=None,
|
||||||
)
|
)
|
||||||
|
|
||||||
inspector_job = {
|
container = client.V1Container(
|
||||||
"apiVersion": "batch/v1",
|
name="inspector",
|
||||||
"kind": "Job",
|
image=image,
|
||||||
"metadata": {
|
command=["sh", "-c", command],
|
||||||
"name": job_name,
|
env=env_vars,
|
||||||
"namespace": namespace,
|
volume_mounts=formatted_mounts,
|
||||||
"labels": {"app": "krayt"},
|
)
|
||||||
"annotations": {"pvcs": ",".join(pvc_info) if pvc_info else "none"},
|
|
||||||
},
|
spec = client.V1PodSpec(
|
||||||
"spec": {
|
containers=[container],
|
||||||
"ttlSecondsAfterFinished": 600,
|
volumes=[format_volume(v) for v in volumes if format_volume(v)],
|
||||||
"template": {
|
restart_policy="Never",
|
||||||
"metadata": {"labels": {"app": "krayt"}},
|
image_pull_secrets=[client.V1LocalObjectReference(name=imagepullsecret)]
|
||||||
"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
|
if imagepullsecret
|
||||||
else None,
|
else None,
|
||||||
"restartPolicy": "Never",
|
)
|
||||||
},
|
|
||||||
},
|
template = client.V1PodTemplateSpec(
|
||||||
},
|
metadata=client.V1ObjectMeta(labels={"app": "krayt"}), spec=spec
|
||||||
}
|
)
|
||||||
return inspector_job
|
|
||||||
|
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 = {
|
PROTECTED_NAMESPACES = {
|
||||||
|
|
@ -438,18 +445,146 @@ def get_pod(namespace: Optional[str] = None):
|
||||||
return pod_name, pod_namespace
|
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()
|
@app.command()
|
||||||
def exec(
|
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.",
|
||||||
),
|
),
|
||||||
|
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.
|
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 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)
|
pod_name, pod_namespace = get_pod(namespace)
|
||||||
exec_command = [
|
exec_command = [
|
||||||
"kubectl",
|
"kubectl",
|
||||||
|
|
@ -459,11 +594,15 @@ def exec(
|
||||||
pod_namespace,
|
pod_namespace,
|
||||||
pod_name,
|
pod_name,
|
||||||
"--",
|
"--",
|
||||||
"/bin/bash",
|
shell,
|
||||||
"-l",
|
"-l",
|
||||||
]
|
]
|
||||||
|
|
||||||
os.execvp("kubectl", exec_command)
|
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()
|
@app.command()
|
||||||
|
|
@ -640,6 +779,11 @@ def create(
|
||||||
"--post-init-hooks",
|
"--post-init-hooks",
|
||||||
help="additional hooks to execute at the start of container initialization",
|
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.
|
Krack open a Krayt dragon! Create an inspector pod to explore what's inside your volumes.
|
||||||
|
|
@ -647,16 +791,15 @@ def create(
|
||||||
The inspector will be created in the same namespace as the selected pod.
|
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
|
# For create, we want to list all pods, not just Krayt pods
|
||||||
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_namespace = namespace
|
||||||
selected_pod = clone
|
selected_pod = clone
|
||||||
|
|
||||||
pods = get_pods(namespace, label_selector=None)
|
if namespace is None and clone is not None and "/" in clone:
|
||||||
|
selected_namespace, selected_pod = clone.split("/", 1)
|
||||||
|
|
||||||
|
get_namespaces(namespace)
|
||||||
|
pods = get_pods(namespace, label_selector="app!=krayt")
|
||||||
|
|
||||||
if not pods:
|
if not pods:
|
||||||
typer.echo("No pods found.")
|
typer.echo("No pods found.")
|
||||||
raise typer.Exit(1)
|
raise typer.Exit(1)
|
||||||
|
|
@ -672,9 +815,6 @@ def create(
|
||||||
typer.echo("No pod selected.")
|
typer.echo("No pod selected.")
|
||||||
raise typer.Exit(1)
|
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)
|
pod_spec = get_pod_spec(selected_pod, selected_namespace)
|
||||||
volume_mounts, volumes = get_pod_volumes_and_mounts(pod_spec)
|
volume_mounts, volumes = get_pod_volumes_and_mounts(pod_spec)
|
||||||
|
|
||||||
|
|
@ -694,7 +834,21 @@ def create(
|
||||||
)
|
)
|
||||||
|
|
||||||
# Output the job manifest
|
# Output the job manifest
|
||||||
typer.echo(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:
|
||||||
|
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()
|
@app.command()
|
||||||
|
|
@ -762,10 +916,10 @@ def list_pods():
|
||||||
typer.echo(f"{pod} ({namespace})")
|
typer.echo(f"{pod} ({namespace})")
|
||||||
|
|
||||||
|
|
||||||
def main():
|
# def main():
|
||||||
setup_environment()
|
# setup_environment()
|
||||||
app()
|
# app()
|
||||||
|
#
|
||||||
|
#
|
||||||
if __name__ == "__main__":
|
# if __name__ == "__main__":
|
||||||
main()
|
# main()
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,8 @@ def list():
|
||||||
|
|
||||||
|
|
||||||
@app.command()
|
@app.command()
|
||||||
def base(
|
def render(
|
||||||
|
template_name: Optional[str] = typer.Option("base.sh", "--template-name", "-t"),
|
||||||
volumes: Optional[List[str]] = typer.Option(
|
volumes: Optional[List[str]] = typer.Option(
|
||||||
None,
|
None,
|
||||||
"--volume",
|
"--volume",
|
||||||
|
|
@ -48,7 +49,6 @@ def base(
|
||||||
help="additional hooks to execute at the start of container initialization",
|
help="additional hooks to execute at the start of container initialization",
|
||||||
),
|
),
|
||||||
):
|
):
|
||||||
template_name = "base.sh"
|
|
||||||
template = env.get_template(template_name)
|
template = env.get_template(template_name)
|
||||||
rendered = template.render(
|
rendered = template.render(
|
||||||
volumes=volumes,
|
volumes=volumes,
|
||||||
|
|
@ -62,37 +62,37 @@ def base(
|
||||||
print(rendered)
|
print(rendered)
|
||||||
|
|
||||||
|
|
||||||
@app.command()
|
# @app.command()
|
||||||
def install(
|
# def install(
|
||||||
additional_packages: Optional[List[str]] = typer.Option(
|
# additional_packages: Optional[List[str]] = typer.Option(
|
||||||
..., "--additional-packages", "-ap"
|
# ..., "--additional-packages", "-ap"
|
||||||
),
|
# ),
|
||||||
):
|
# ):
|
||||||
template_name = "install.sh"
|
# template_name = "install.sh"
|
||||||
template = env.get_template(template_name)
|
# template = env.get_template(template_name)
|
||||||
rendered = template.render(additional_packages=additional_packages)
|
# rendered = template.render(additional_packages=additional_packages)
|
||||||
print(rendered)
|
# print(rendered)
|
||||||
|
#
|
||||||
|
#
|
||||||
@app.command()
|
# @app.command()
|
||||||
def motd(
|
# def motd(
|
||||||
volumes: Optional[List[str]] = typer.Option(
|
# volumes: Optional[List[str]] = typer.Option(
|
||||||
None,
|
# None,
|
||||||
"--volume",
|
# "--volume",
|
||||||
),
|
# ),
|
||||||
pvcs: Optional[List[str]] = typer.Option(
|
# pvcs: Optional[List[str]] = typer.Option(
|
||||||
None,
|
# None,
|
||||||
"--pvc",
|
# "--pvc",
|
||||||
),
|
# ),
|
||||||
additional_packages: Optional[List[str]] = typer.Option(
|
# additional_packages: Optional[List[str]] = typer.Option(
|
||||||
..., "--additional-packages", "-ap"
|
# ..., "--additional-packages", "-ap"
|
||||||
),
|
# ),
|
||||||
):
|
# ):
|
||||||
template_name = "motd.sh"
|
# template_name = "motd.sh"
|
||||||
template = env.get_template(template_name)
|
# template = env.get_template(template_name)
|
||||||
rendered = template.render(
|
# rendered = template.render(
|
||||||
volumes=volumes,
|
# volumes=volumes,
|
||||||
pvcs=pvcs,
|
# pvcs=pvcs,
|
||||||
additional_packages=additional_packages,
|
# additional_packages=additional_packages,
|
||||||
)
|
# )
|
||||||
print(rendered)
|
# print(rendered)
|
||||||
|
|
|
||||||
|
|
@ -5,11 +5,11 @@ cat <<EOF >/etc/motd
|
||||||
└───────────────────────────────────┘
|
└───────────────────────────────────┘
|
||||||
|
|
||||||
"Inside every volume lies a pearl of wisdom waiting to be discovered."
|
"Inside every volume lies a pearl of wisdom waiting to be discovered."
|
||||||
{%- if volumes %}
|
{%- if mounts %}
|
||||||
|
|
||||||
Mounted Volumes:
|
Mounted Volumes:
|
||||||
{%- for volume in volumes %}
|
{%- for mount in mounts %}
|
||||||
- {{ volume }}
|
- {{ mount.name }}:{{ mount.mount_path }}
|
||||||
{%- endfor %}
|
{%- endfor %}
|
||||||
{%- endif %}
|
{%- endif %}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,11 @@ def get_release_notes(version):
|
||||||
|
|
||||||
You can install krayt using one of these methods:
|
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)
|
### Using i.jpillora.com (recommended)
|
||||||
|
|
||||||
|
|
@ -37,8 +41,8 @@ curl -fsSL https://github.com/waylonwalker/krayt/releases/download/v{version}/in
|
||||||
|
|
||||||
### Manual download
|
### Manual download
|
||||||
You can also manually download the archive for your platform from the releases page:
|
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)
|
- [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.tar.gz)"""
|
- [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
|
# Get help output for main command and all subcommands
|
||||||
try:
|
try:
|
||||||
|
|
@ -46,17 +50,23 @@ You can also manually download the archive for your platform from the releases p
|
||||||
|
|
||||||
# Get main help output
|
# Get main help output
|
||||||
main_help = subprocess.check_output(
|
main_help = subprocess.check_output(
|
||||||
["./krayt.py", "--help"],
|
["krayt", "--help"],
|
||||||
stderr=subprocess.STDOUT,
|
stderr=subprocess.STDOUT,
|
||||||
universal_newlines=True,
|
universal_newlines=True,
|
||||||
)
|
)
|
||||||
help_outputs.append(("Main Command", main_help))
|
help_outputs.append(("Main Command", main_help))
|
||||||
|
|
||||||
# Get help for each subcommand
|
# Get help for each subcommand
|
||||||
subcommands = ["create", "exec", "clean", "version"]
|
subcommands = [
|
||||||
|
"create",
|
||||||
|
"exec",
|
||||||
|
"clean",
|
||||||
|
"version",
|
||||||
|
"pod",
|
||||||
|
]
|
||||||
for cmd in subcommands:
|
for cmd in subcommands:
|
||||||
cmd_help = subprocess.check_output(
|
cmd_help = subprocess.check_output(
|
||||||
["./krayt.py", cmd, "--help"],
|
["krayt", cmd, "--help"],
|
||||||
stderr=subprocess.STDOUT,
|
stderr=subprocess.STDOUT,
|
||||||
universal_newlines=True,
|
universal_newlines=True,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -148,11 +148,11 @@ function install {
|
||||||
FTYPE=""
|
FTYPE=""
|
||||||
case "${OS}_${ARCH}" in
|
case "${OS}_${ARCH}" in
|
||||||
"linux_amd64")
|
"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"
|
FTYPE=".tar.gz"
|
||||||
;;
|
;;
|
||||||
"linux_arm64")
|
"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"
|
FTYPE=".tar.gz"
|
||||||
;;
|
;;
|
||||||
*) fail "No asset for platform ${OS}-${ARCH}" ;;
|
*) fail "No asset for platform ${OS}-${ARCH}" ;;
|
||||||
|
|
@ -193,7 +193,7 @@ function install {
|
||||||
unzip -o -qq tmp.zip || fail "unzip failed"
|
unzip -o -qq tmp.zip || fail "unzip failed"
|
||||||
rm tmp.zip || fail "cleanup failed"
|
rm tmp.zip || fail "cleanup failed"
|
||||||
elif [[ $FTYPE = ".bin" ]]; then
|
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
|
else
|
||||||
fail "unknown file type: $FTYPE"
|
fail "unknown file type: $FTYPE"
|
||||||
fi
|
fi
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue