Compare commits

...

57 commits
v0.0.0 ... main

Author SHA1 Message Date
WaylonWalker
0f9e70267e Bump version: 0.4.3b0 → 0.4.3
Some checks failed
Release Krayt / pypi-release-krayt (push) Failing after 1m16s
2025-04-24 01:46:17 +00:00
Waylon S. Walker
f8c44999fe release 0.4.3 2025-04-23 20:45:16 -05:00
WaylonWalker
45e5dd74aa Bump version: 0.4.2b0 → 0.4.2
Some checks failed
Release Krayt / gh-release-krayt (push) Failing after 24s
Release Krayt / pypi-release-krayt (push) Failing after 56s
2025-04-24 01:42:32 +00:00
Waylon S. Walker
eb54e314ff release 0.4.2 2025-04-23 20:41:14 -05:00
WaylonWalker
d6056cead2 Bump version: 0.4.1b4 → 0.4.1
Some checks failed
Release Krayt / release-krayt (push) Failing after 58s
2025-04-24 01:36:32 +00:00
Waylon S. Walker
be6ee91098 release 0.4.1 2025-04-23 20:35:29 -05:00
Waylon S. Walker
1fee6e2c97 fix deployment
Some checks failed
Release Krayt / release-krayt (push) Failing after 49s
2025-04-23 20:19:57 -05:00
Waylon S. Walker
2fbc15ae84 use uv tool
Some checks failed
Release Krayt / release-krayt (push) Failing after 1m2s
2025-04-23 20:11:13 -05:00
Waylon S. Walker
b64e635b71 install hatch for release 2025-04-23 20:09:52 -05:00
Waylon S. Walker
7511cace42 bump build 2025-04-23 20:05:43 -05:00
Waylon S. Walker
1138f695f4 bump action 2025-04-23 20:04:59 -05:00
Waylon S. Walker
0a6e19731d fix just install 2025-04-23 20:04:21 -05:00
WaylonWalker
64077d3ba2 Bump version: 0.4.0b0 → 0.4.0
Some checks failed
Release Krayt / release-krayt (push) Failing after 34s
2025-04-24 01:01:32 +00:00
Waylon S. Walker
681194fc1f fix justfile format 2025-04-23 20:00:20 -05:00
Waylon S. Walker
ed449034f7 formatting fix 2025-04-23 19:59:15 -05:00
Waylon S. Walker
17c088526b release 0.4.0 2025-04-23 19:56:26 -05:00
Waylon S. Walker
225edce32d better motd volumes 2025-04-23 19:42:40 -05:00
Waylon S. Walker
db96853646 add python fallback for exec 2025-04-21 09:47:31 -05:00
Waylon S. Walker
3ce69baf26 generic templates endpoint for cli 2025-04-21 09:46:18 -05:00
Waylon S. Walker
7daa9a3874 python native exec 2025-04-18 14:05:48 -05:00
Waylon S. Walker
cc425cf812 add --apply flag for create 2025-04-18 13:38:05 -05:00
Waylon S. Walker
ae918bf5f2 changelog for 0.3.0 2025-04-18 13:37:11 -05:00
Waylon S. Walker
2f16036c8e use hatch version 2025-04-18 13:36:55 -05:00
Waylon S. Walker
82102c4adf update release notes 2025-04-18 13:36:23 -05:00
Waylon S. Walker
1959e1a39b replace nvim-manager with krayt 2025-04-18 13:36:05 -05:00
WaylonWalker
0025cea947 Bump version: 0.3.0b0 → 0.3.0
Some checks failed
Release Krayt / release-krayt (push) Failing after 1m13s
2025-04-17 16:13:34 +00:00
Waylon S. Walker
2f049f1f99 release 0.3.0 2025-04-17 11:12:39 -05:00
WaylonWalker
10f7f61cec Bump version: 0.3.0b0 → 0.3.0 2025-04-17 15:16:00 +00:00
Waylon S. Walker
b3500c9170 remove pypi env var 2025-04-17 10:15:01 -05:00
WaylonWalker
09f68faded Bump version: 0.3.0b0 → 0.3.0 2025-04-17 15:01:32 +00:00
Waylon S. Walker
0bd055f881 bump for build 2025-04-17 10:00:36 -05:00
Waylon S. Walker
2e4eb8ee5f update workflow permissions 2025-04-17 09:59:38 -05:00
WaylonWalker
caa07ac771 Bump version: 0.3.0b1 → 0.3.0 2025-04-17 14:49:58 +00:00
Waylon S. Walker
575c70ee9f bump for build 2025-04-17 09:49:02 -05:00
Waylon S. Walker
a9669afff8 give write permissions 2025-04-17 09:47:50 -05:00
Waylon S. Walker
96788a412b bump for build 2025-04-17 09:45:39 -05:00
Waylon S. Walker
9b6efdc7fe rename job 2025-04-17 09:45:04 -05:00
Waylon Walker
abb7dacc86
release 0.3.0
release 0.3.0
2025-04-17 09:42:09 -05:00
Waylon S. Walker
74fba4f23e add content 2025-04-17 09:41:18 -05:00
Waylon S. Walker
301b628d39 add release workflow 2025-04-17 09:37:32 -05:00
Waylon S. Walker
61fbec4e31 bump version 2025-04-17 09:37:14 -05:00
Waylon S. Walker
a6ecb8240c add lint-format 2025-04-17 09:36:57 -05:00
Waylon S. Walker
e8686a2c66 lint 2025-04-17 09:19:16 -05:00
Waylon S. Walker
47e7f1cb5e implement port forward 2025-04-17 09:09:40 -05:00
Waylon S. Walker
2899ee23eb its working! 2025-04-11 22:09:04 -05:00
Waylon S. Walker
0d913f7656 brew is gonna take some work 2025-04-10 20:08:34 -05:00
Waylon S. Walker
378744632f wip 2025-04-10 09:21:51 -05:00
Waylon S. Walker
9737746923 more package support 2025-04-09 09:15:00 -05:00
Waylon S. Walker
a60562b7fc wip 2025-04-08 21:09:10 -05:00
Waylon S. Walker
90491d17bf release 0.2.0 2025-03-25 10:14:19 -05:00
Waylon S. Walker
75c7b30056 release 0.1.0 2025-03-25 10:00:37 -05:00
Waylon S. Walker
4c8e66aa46 fix init scripts 2025-03-25 09:58:00 -05:00
Waylon S. Walker
256e62377e logs 2025-03-25 09:42:49 -05:00
Waylon S. Walker
5238ef5f6b add ability to select image 2025-03-24 22:40:18 -05:00
Waylon S. Walker
eb90496cbc still working 2025-03-24 21:47:05 -05:00
Waylon Walker
86985a62a9
Update README.md 2025-03-24 15:01:30 -05:00
Waylon S. Walker
08c70cc18f add artwork 2025-03-24 14:59:08 -05:00
34 changed files with 3402 additions and 333 deletions

47
.github/workflows/release-pypi.yaml vendored Normal file
View file

@ -0,0 +1,47 @@
name: Release Krayt
on:
workflow_dispatch:
push:
paths:
- "krayt/**"
- "pyproject.toml"
permissions:
contents: write
pull-requests: write
issues: read
packages: none
id-token: write
jobs:
pypi-release-krayt:
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 }}
- 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

1
.gitignore vendored
View file

@ -962,3 +962,4 @@ FodyWeavers.xsd
# Additional files built by Visual Studio # 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 # End of https://www.toptal.com/developers/gitignore/api/vim,node,data,emacs,python,pycharm,executable,sublimetext,visualstudio,visualstudiocode
*.null-ls*

View file

@ -1,3 +1,54 @@
## 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
- Support for imagepullsecret flag on krayt create command
- Allows pulling from private container registries by specifying an image pull secret
## 0.1.0
### Added
- Support for initialization scripts in `~/.config/krayt/init.d/`
- Scripts run before package installation
- Support for proxy configuration
- Custom package repositories setup
- Environment variable configuration
- Example initialization scripts:
- `00_proxy.sh` for proxy configuration
- `10_install_git.sh` for git installation and configuration
- Improved binary installation process
- Better platform detection
- Support for multiple archive formats (.tar.gz, .gz, .bz2, .zip, .bin)
- Improved error handling and user feedback
- Automatic sudo elevation when needed for binary installation
## 0.0.0 ## 0.0.0
- Initial release - Initial release

21
LICENSE Normal file
View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2021-present Waylon S. Walker <waylon@waylonwalker.com>
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.

View file

@ -1,5 +1,7 @@
# Krayt - The Kubernetes Volume Inspector # 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. 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. Hunt down storage issues and explore your persistent data like a true Tatooine dragon hunter.
@ -38,6 +40,12 @@ This will install the `krayt` command to `/usr/local/bin`.
# Create a new inspector and apply it directly # Create a new inspector and apply it directly
krayt create | kubectl apply -f - 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 # Or review the manifest first
krayt create > inspector.yaml krayt create > inspector.yaml
kubectl apply -f inspector.yaml kubectl apply -f inspector.yaml
@ -65,6 +73,80 @@ Your inspector pod comes equipped with a full arsenal of tools:
- **Network Tools**: `mtr`, `dig` - **Network Tools**: `mtr`, `dig`
- **Cloud & Database**: `aws-cli`, `sqlite3` - **Cloud & Database**: `aws-cli`, `sqlite3`
## Customization
### Init Scripts
Krayt supports initialization scripts that run in the inspector pod before any packages are installed. These scripts are useful for:
- Setting up proxy configurations
- Installing additional tools
- Configuring custom package repositories
- Setting environment variables
Place your scripts in `~/.config/krayt/init.d/` with a `.sh` extension. Scripts are executed in alphabetical order, so you can control the execution sequence using numerical prefixes.
Example init scripts:
1. Install additional tools (`~/.config/krayt/init.d/10_install_git.sh`):
```bash
#!/bin/sh
echo "Installing additional tools..."
# Install git for source control
apk add git
# Configure git
git config --global init.defaultBranch main
git config --global core.editor vi
```
2. Set up custom repositories (`~/.config/krayt/init.d/20_custom_repos.sh`):
```bash
#!/bin/sh
echo "Adding custom package repositories..."
# Add testing repository for newer packages
echo "@testing http://dl-cdn.alpinelinux.org/alpine/edge/testing" >> /etc/apk/repositories
# Update package list
apk update
```
### Proxy Configuration
If your environment requires a proxy, you have two options:
1. **Environment Variables** (Recommended):
```bash
# Add to your shell's rc file (e.g., ~/.bashrc, ~/.zshrc)
export HTTP_PROXY="http://proxy.example.com:8080"
export HTTPS_PROXY="http://proxy.example.com:8080"
export NO_PROXY="localhost,127.0.0.1,.internal.example.com"
```
2. **Init Script** (`~/.config/krayt/init.d/00_proxy.sh`):
```bash
#!/bin/sh
echo "Configuring proxy settings..."
# Set proxy for Alpine package manager
mkdir -p /etc/apk
cat > /etc/apk/repositories << EOF
http://dl-cdn.alpinelinux.org/alpine/latest-stable/main
http://dl-cdn.alpinelinux.org/alpine/latest-stable/community
# Configure proxy
proxy=http://proxy.example.com:8080
EOF
# Set proxy for other tools
export HTTP_PROXY="http://proxy.example.com:8080"
export HTTPS_PROXY="http://proxy.example.com:8080"
export NO_PROXY="localhost,127.0.0.1,.internal.example.com"
```
The proxy configuration will be applied before any packages are installed, ensuring that all package installations and network operations work correctly through your proxy.
## Quotes from the Field ## Quotes from the Field
> "Inside every volume lies a pearl of wisdom waiting to be discovered." > "Inside every volume lies a pearl of wisdom waiting to be discovered."

View file

@ -0,0 +1,25 @@
#!/bin/sh
echo "Configuring proxy settings..."
# Set proxy for Alpine package manager
mkdir -p /etc/apk
cat > /etc/apk/repositories << EOF
http://dl-cdn.alpinelinux.org/alpine/latest-stable/main
http://dl-cdn.alpinelinux.org/alpine/latest-stable/community
# Configure proxy
proxy=http://proxy.example.com:8080
EOF
# Set proxy for other tools
export HTTP_PROXY="http://proxy.example.com:8080"
export HTTPS_PROXY="http://proxy.example.com:8080"
export NO_PROXY="localhost,127.0.0.1,.internal.example.com"
# Test proxy configuration
echo "Testing proxy configuration..."
if curl -s -m 5 https://www.google.com > /dev/null; then
echo "Proxy configuration successful!"
else
echo "Warning: Proxy test failed. Check your proxy settings."
fi

View file

@ -0,0 +1,20 @@
#!/bin/sh
echo "Installing additional development tools..."
# Install git and related tools
apk add git git-lfs
# Configure git defaults
git config --global init.defaultBranch main
git config --global core.editor vi
git config --global pull.rebase false
# Add some helpful git aliases
git config --global alias.st status
git config --global alias.co checkout
git config --global alias.br branch
git config --global alias.ci commit
git config --global alias.unstage 'reset HEAD --'
git config --global alias.last 'log -1 HEAD'
echo "Git configuration complete."

View file

@ -0,0 +1,19 @@
#!/bin/sh
echo "Setting up additional package repositories..."
# Add testing repository for newer packages
echo "@testing http://dl-cdn.alpinelinux.org/alpine/edge/testing" >> /etc/apk/repositories
# Add community repository
echo "@community http://dl-cdn.alpinelinux.org/alpine/edge/community" >> /etc/apk/repositories
# Update package list
apk update
# Install some useful tools from testing/community
apk add \
@testing golang \
@community rust \
@community cargo
echo "Additional repositories configured and packages installed."

View file

@ -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,59 +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

BIN
krayt-squooshed.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 511 KiB

BIN
krayt.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 MiB

BIN
krayt.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 567 KiB

1
krayt/__about__.py Normal file
View file

@ -0,0 +1 @@
__version__ = "0.4.3"

5
krayt/__init__.py Normal file
View file

@ -0,0 +1,5 @@
from krayt.__about__ import __version__
__all__ = [
"__version__",
]

88
krayt/bundles.py Normal file
View file

@ -0,0 +1,88 @@
"""
Bundles of packages available in most package managers.
"""
basics = [
"curl",
"wget",
"jq",
"yq",
"bash",
"coreutils",
]
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",
],
}
bundles["all"] = list(
set(
[
*bundles["basics"],
*bundles["pretty"],
*bundles["networking"],
*bundles["database"],
*bundles["storage"],
*bundles["search"],
*bundles["monitoring"],
]
)
)

25
krayt/cli/__init__.py Normal file
View file

@ -0,0 +1,25 @@
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, clean
from krayt.cli.templates import app as templates_app
from typer import Typer
app = Typer()
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)
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)
@app.command()
def version():
print(__version__)
def main():
app()

25
krayt/cli/bundles.py Normal file
View file

@ -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}")

925
krayt/cli/pod.py Normal file
View file

@ -0,0 +1,925 @@
import iterfzf
from krayt.templates import env
from kubernetes.stream import stream
from kubernetes import client, config
import logging
import os
import time
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)
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,
"mount_path": vm.mount_path,
"read_only": 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 = 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
# 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_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()
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 create_inspector_job(
api,
namespace: str,
pod_name: str,
volume_mounts: list,
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,
):
timestamp = int(time.time())
job_name = f"{pod_name}-krayt-{timestamp}"
env_vars, secret_volumes = get_env_vars_and_secret_volumes(api, namespace)
volumes.extend(secret_volumes)
secret_mounts = [
client.V1VolumeMount(
name=vol.name,
mount_path=f"/mnt/secrets/{vol.secret.secret_name}",
read_only=True,
)
for vol in secret_volumes
]
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
]
template = env.get_template("base.sh")
command = template.render(
volumes=volumes,
pvcs=None,
additional_packages=additional_packages,
pre_init_scripts=None,
post_init_scripts=None,
pre_init_hooks=None,
post_init_hooks=None,
)
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 = {
"kube-system",
"kube-public",
"kube-node-lease",
"argo-events",
"argo-rollouts",
"argo-workflows",
"argocd",
"cert-manager",
"ingress-nginx",
"monitoring",
"prometheus",
"istio-system",
"linkerd",
}
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: {__version__}")
raise typer.Exit()
def get_pod(namespace: Optional[str] = None):
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)
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
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",
"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()
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(
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,
"--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",
"-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",
),
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",
),
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.
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
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)
get_namespaces(namespace)
pods = get_pods(namespace, label_selector="app!=krayt")
if not pods:
typer.echo("No pods found.")
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)
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,
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
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()
def version():
"""Show the version of Krayt."""
typer.echo(f"Version: {__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)
@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()
# app()
#
#
# if __name__ == "__main__":
# main()

98
krayt/cli/templates.py Normal file
View file

@ -0,0 +1,98 @@
from krayt.templates import env
import typer
from typing import List, Optional
app = typer.Typer()
@app.command()
def list():
typer.echo("Available templates:")
for template in env.list_templates():
typer.echo(template)
@app.command()
def render(
template_name: Optional[str] = typer.Option("base.sh", "--template-name", "-t"),
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 = 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"
# 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)

191
krayt/package.py Normal file
View file

@ -0,0 +1,191 @@
from krayt.bundles import bundles
from more_itertools import unique_everseen
from pydantic import BaseModel, BeforeValidator
from typing import Annotated, List, Literal, Optional, Union
SUPPORTED_KINDS = {
"system",
"uv",
"installer",
"i",
"curlbash",
"curlsh",
"cargo",
"pipx",
"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"],
}
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, etc.
"""
kind: Annotated[
Literal[*SUPPORTED_KINDS],
BeforeValidator(validate_kind),
] = "system"
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":
"""
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
# 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"
def install_command(self) -> str:
"""
Generate the bash install command snippet for this package.
"""
cmd = ""
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"installer {self.value}"
elif self.kind == "curlsh":
cmd = f"curl -fsSL {self.value} | sh"
elif self.kind == "curlbash":
cmd = f"curl -fsSL {self.value} | bash"
elif self.kind == "cargo":
cmd = f"cargo install {self.value}"
elif self.kind == "pipx":
cmd = f"pipx install {self.value}"
elif self.kind == "npm":
cmd = f"npm install -g {self.value}"
elif self.kind == "go":
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
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.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([*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))

13
krayt/templates.py Normal file
View file

@ -0,0 +1,13 @@
from jinja2 import Environment, FileSystemLoader
from krayt.package import get_install_script
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]))
env.globals["get_install_script"] = get_install_script

3
krayt/templates/.kraytrc Normal file
View file

@ -0,0 +1,3 @@
if [ -t 1 ] && [ -f /etc/motd ]; then
cat /etc/motd
fi

25
krayt/templates/base.sh Normal file
View file

@ -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

View file

@ -0,0 +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 <package1> [package2] [...]"
return 1
fi
FAILED_PKGS=""
for pkg in "$@"; do
echo "Installing package: $pkg"
if ! $INSTALL_CMD $pkg; then
echo "⚠️ Warning: Failed to install package: $pkg"
FAILED_PKGS="$FAILED_PKGS $pkg"
fi
done
{% raw %}
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
{% endraw %}
}
installer() {
if [ $# -eq 0 ]; then
echo "Usage: installer <package1> [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 %}

116
krayt/templates/kraytrc.sh Normal file
View file

@ -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 <<EOF >/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

40
krayt/templates/motd.sh Normal file
View file

@ -0,0 +1,40 @@
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."
{%- if mounts %}
Mounted Volumes:
{%- for mount in mounts %}
- {{ mount.name }}:{{ mount.mount_path }}
{%- 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

View file

@ -13,13 +13,6 @@ Krayt - The Kubernetes Volume Inspector
Like cracking open a Krayt dragon pearl, this tool helps you inspect what's inside your Kubernetes volumes. 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. 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! May the Force be with your volumes!
""" """
@ -27,6 +20,7 @@ from iterfzf import iterfzf
from kubernetes import client, config from kubernetes import client, config
import logging import logging
import os import os
from pathlib import Path
import time import time
import typer import typer
from typing import Any, Optional from typing import Any, Optional
@ -106,47 +100,57 @@ def fuzzy_select(items):
if not items: if not items:
return None, None return None, None
# Format items as "namespace/name" for display # If there's only one item, return it without prompting
formatted_items = [f"{ns}/{name}" for name, ns in items] if len(items) == 1:
logging.debug(f"Found {len(formatted_items)} pods") return items[0]
# Format items for display
formatted_items = [f"{name} ({namespace})" for name, namespace in items]
# Use fzf for selection
try: try:
# Use iterfzf for selection
selected = iterfzf(formatted_items) selected = iterfzf(formatted_items)
if not selected:
if selected:
namespace, name = selected.split("/")
logging.debug(f"Selected pod {name} in namespace {namespace}")
return name, namespace
else:
logging.debug("No selection made")
return None, None 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: except Exception as e:
logging.error(f"Error during selection: {e}", exc_info=True) typer.echo(f"Error during selection: {e}")
typer.echo(f"Error during selection: {e}", err=True) return None, None
raise typer.Exit(1)
def get_pods(namespace=None): def get_pods(
namespace=None,
label_selector: str = "app=krayt",
):
"""Get list of pods in the specified namespace or all namespaces""" """Get list of pods in the specified namespace or all namespaces"""
config.load_kube_config()
v1 = client.CoreV1Api()
try: try:
config.load_kube_config()
api = client.CoreV1Api()
if namespace: if namespace:
logging.debug(f"Listing pods in namespace {namespace}") pods = api.list_namespaced_pod(
pod_list = v1.list_namespaced_pod(namespace=namespace) namespace=namespace,
label_selector=label_selector,
)
else: else:
logging.debug("Listing pods in all namespaces") pods = api.list_pod_for_all_namespaces(
pod_list = v1.list_pod_for_all_namespaces() label_selector=label_selector,
)
pods = [(pod.metadata.name, pod.metadata.namespace) for pod in pod_list.items] # Convert to list of (name, namespace) tuples
logging.debug(f"Found {len(pods)} pods") pod_list = []
return pods for pod in pods.items:
except client.exceptions.ApiException as e: if pod.metadata.namespace not in PROTECTED_NAMESPACES:
logging.error(f"Error listing pods: {e}") pod_list.append((pod.metadata.name, pod.metadata.namespace))
typer.echo(f"Error listing pods: {e}", err=True) return pod_list
except client.rest.ApiException as e:
typer.echo(f"Error listing pods: {e}")
raise typer.Exit(1) raise typer.Exit(1)
@ -205,57 +209,141 @@ def get_pod_volumes_and_mounts(pod_spec):
return volume_mounts, volumes return volume_mounts, volumes
def get_pod_env_and_secrets(api, namespace, pod_name): def get_env_vars_and_secret_volumes(api, namespace: str):
pod = api.read_namespaced_pod(pod_name, namespace) """Get environment variables and secret volumes for the inspector pod"""
# Get environment variables from the pod
env_vars = [] env_vars = []
for container in pod.spec.containers: volumes = []
if container.env:
for env in container.env:
env_dict = {"name": env.name}
if env.value:
env_dict["value"] = env.value
elif env.value_from:
if env.value_from.config_map_key_ref:
env_dict["valueFrom"] = {
"configMapKeyRef": {
"name": env.value_from.config_map_key_ref.name,
"key": env.value_from.config_map_key_ref.key,
}
}
elif env.value_from.secret_key_ref:
env_dict["valueFrom"] = {
"secretKeyRef": {
"name": env.value_from.secret_key_ref.name,
"key": env.value_from.secret_key_ref.key,
}
}
elif env.value_from.field_ref:
env_dict["valueFrom"] = {
"fieldRef": {
"fieldPath": env.value_from.field_ref.field_path
}
}
env_vars.append(env_dict)
# Get all volume mounts that are secrets # Add proxy environment variables if they exist in the host environment
secret_volumes = [] proxy_vars = [
if pod.spec.volumes: "HTTP_PROXY",
secret_volumes = [v for v in pod.spec.volumes if v.secret] "HTTPS_PROXY",
"NO_PROXY",
"http_proxy",
"https_proxy",
"no_proxy",
]
return env_vars, secret_volumes 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( def create_inspector_job(
api, namespace: str, pod_name: str, volume_mounts: list, volumes: list 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""" """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 secrets from the target pod # Get environment variables and secret volumes from the target pod
env_vars, secret_volumes = get_pod_env_and_secrets(api, namespace, pod_name) env_vars, secret_volumes = get_env_vars_and_secret_volumes(api, namespace)
# Add secret volumes to our volumes list # Add secret volumes to our volumes list
volumes.extend(secret_volumes) volumes.extend(secret_volumes)
@ -286,6 +374,142 @@ def create_inspector_job(
if hasattr(v, "persistent_volume_claim") and v.persistent_volume_claim: if hasattr(v, "persistent_volume_claim") and v.persistent_volume_claim:
pvc_info.append(f"{v.name}:{v.persistent_volume_claim.claim_name}") pvc_info.append(f"{v.name}:{v.persistent_volume_claim.claim_name}")
init_scripts = get_init_scripts()
# Build the command script
command_parts = []
# Configure apk proxy settings BEFORE any package installation
command_parts.extend(
[
"# Configure apk proxy settings",
"mkdir -p /etc/apk",
"cat > /etc/apk/repositories << 'EOF'",
"https://dl-cdn.alpinelinux.org/alpine/latest-stable/main",
"https://dl-cdn.alpinelinux.org/alpine/latest-stable/community",
"EOF",
"",
'if [ ! -z "$HTTP_PROXY" ]; then',
' echo "Setting up apk proxy configuration..."',
" mkdir -p /etc/apk/",
" cat > /etc/apk/repositories << EOF",
"#/media/cdrom/apks",
"http://dl-cdn.alpinelinux.org/alpine/latest-stable/main",
"http://dl-cdn.alpinelinux.org/alpine/latest-stable/community",
"",
"# Configure proxy",
"proxy=$HTTP_PROXY",
"EOF",
"fi",
"",
"# Install basic tools first",
"apk update",
"apk add curl",
"",
"# Install uv CLI",
"echo 'Installing uv CLI...'",
"curl -LsSf https://astral.sh/uv/install.sh | sh",
"echo 'uv version:'",
"uv --version",
"",
"echo 'Installing starship...'",
"curl -sS https://starship.rs/install.sh | sh -s -- -y",
"echo 'starship version:'",
"starship --version",
"",
"",
"# Install additional tools",
"apk add "
+ " ".join(
[
"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",
]
),
"",
]
)
# Add init scripts if present
if init_scripts:
command_parts.extend(
[
"# Set up init script environment",
"mkdir -p /tmp/init.d",
"",
"# Write and run init scripts",
"cat > /tmp/init.sh << 'EOFSCRIPT'",
init_scripts,
"EOFSCRIPT",
"",
"# Make init script executable and run it",
"chmod +x /tmp/init.sh",
"bash /tmp/init.sh",
"",
]
)
# Add shell setup and MOTD
command_parts.extend(
[
"# Create .ashrc with MOTD",
"cat > /root/.ashrc << 'EOF'",
"# Display MOTD on login",
"[ -f /etc/motd ] && cat /etc/motd",
"# Set up shell environment",
"export EDITOR=vi",
"export PAGER=less",
"# Set up aliases",
"alias ll='ls -la'",
"alias l='ls -la'",
"alias la='ls -la'",
"alias vi='vim'",
"# Set up PATH",
"export PATH=/root/.local/bin:$PATH",
'eval "$(starship init bash)"',
"EOF",
"",
"",
"# Set up environment to always source our RC file",
"echo 'export ENV=/root/.ashrc' > /etc/profile",
"echo 'export ENV=/root/.ashrc' > /etc/environment",
"",
"# Make RC file available to all shells",
"mkdir -p /etc/profile.d",
"cp /root/.ashrc /etc/profile.d/motd.sh",
"ln -sf /root/.ashrc /root/.profile",
"ln -sf /root/.ashrc /root/.bashrc",
"ln -sf /root/.ashrc /root/.mkshrc",
"ln -sf /root/.ashrc /etc/shinit",
"",
"# Update MOTD",
get_motd_script(mount_info, pvc_info),
"",
"# Keep container running",
"tail -f /dev/null",
]
)
inspector_job = { inspector_job = {
"apiVersion": "batch/v1", "apiVersion": "batch/v1",
"kind": "Job", "kind": "Job",
@ -293,187 +517,23 @@ def create_inspector_job(
"name": job_name, "name": job_name,
"namespace": namespace, "namespace": namespace,
"labels": {"app": "krayt"}, "labels": {"app": "krayt"},
"annotations": {"pvcs": ",".join(pvc_info) if pvc_info else "none"},
}, },
"spec": { "spec": {
"ttlSecondsAfterFinished": 0, # Delete immediately after completion
"template": { "template": {
"metadata": {"labels": {"app": "krayt"}}, "metadata": {"labels": {"app": "krayt"}},
"spec": { "spec": {
"containers": [ "containers": [
{ {
"name": "krayt", "name": "inspector",
"image": "alpine:latest", # Use Alpine as base for package management "image": image,
"command": [ "command": ["sh", "-c", "\n".join(command_parts)],
"sh", "env": env_vars,
"-c",
"""
# Install basic tools first
apk update
apk add curl
# Install lf (terminal file manager)
curl -L https://github.com/gokcehan/lf/releases/download/r31/lf-linux-amd64.tar.gz | tar xzf - -C /usr/local/bin
# Install the rest of the tools
apk add ripgrep exa ncdu dust \
file hexyl jq yq bat fd fzf \
htop bottom difftastic \
mtr bind-tools \
aws-cli sqlite sqlite-dev sqlite-libs
# Function to update MOTD
update_motd() {
cat << EOF > /etc/motd
====================================
Krayt Dragon's Lair
====================================
"Inside every volume lies a pearl of wisdom waiting to be discovered."
Mounted Volumes:
$(echo "$MOUNTS" | tr ',' '\\n' | sed 's/^/- /')
Persistent Volume Claims:
$(echo "$PVCS" | tr ',' '\\n' | sed 's/^/- /')
Mounted Secrets:
$(for d in /mnt/secrets/*; do if [ -d "$d" ]; then echo "- $(basename $d)"; fi; done)
Environment Variables:
$(env | sort | sed 's/^/- /')
Your Hunting Tools:
File Navigation:
- lf: Terminal file manager (run 'lf')
- exa: Modern ls (run 'ls', 'll', or 'tree')
- fd: Modern find (run 'fd pattern')
Search & Analysis:
- rg (ripgrep): Fast search (run 'rg pattern')
- bat: Better cat with syntax highlighting
- hexyl: Hex viewer (run 'hexyl file')
- file: File type detection
Disk Usage:
- ncdu: Interactive disk usage analyzer
- dust: Disk usage analyzer
- du: Standard disk usage tool
File Comparison:
- difft: Modern diff tool (alias 'diff')
System Monitoring:
- btm: Modern system monitor (alias 'top')
- htop: Interactive process viewer
JSON/YAML Tools:
- jq: JSON processor
- yq: YAML processor
Network Tools:
- dig: DNS lookup
- mtr: Network diagnostics
Cloud & Database:
- aws: AWS CLI
- sqlite3: SQLite database tool
Type 'tools-help' for detailed usage information
====================================
EOF
}
# Create helpful aliases and functions
cat << 'EOF' > /root/.ashrc
if [ "$PS1" ]; then
cat /etc/motd
fi
# Aliases for better file navigation
alias ls='exa'
alias ll='exa -l'
alias la='exa -la'
alias tree='exa --tree'
alias find='fd'
alias top='btm'
alias diff='difft'
alias cat='bat --paging=never'
# Function to show detailed tool help
tools-help() {
echo "Krayt Dragon Hunter's Guide:"
echo
echo "File Navigation:"
echo " lf : Navigate with arrow keys, q to quit, h for help"
echo " ls, ll, la : List files (exa with different options)"
echo " tree : Show directory structure"
echo " fd pattern : Find files matching pattern"
echo
echo "Search & Analysis:"
echo " rg pattern : Search file contents"
echo " bat file : View file with syntax highlighting"
echo " hexyl file : View file in hex format"
echo " file path : Determine file type"
echo
echo "Disk Usage:"
echo " ncdu : Interactive disk usage analyzer (navigate with arrows)"
echo " dust path : Tree-based disk usage"
echo " du -sh * : Summarize disk usage"
echo
echo "File Comparison:"
echo " diff file1 file2 : Compare files with syntax highlighting"
echo
echo "System Monitoring:"
echo " top (btm) : Modern system monitor"
echo " htop : Process viewer"
echo
echo "JSON/YAML Tools:"
echo " jq . file.json : Format and query JSON"
echo " yq . file.yaml : Format and query YAML"
echo
echo "Network Tools:"
echo " dig domain : DNS lookup"
echo " mtr host : Network diagnostics"
echo
echo "Cloud & Database:"
echo " aws : AWS CLI tool"
echo " sqlite3 : SQLite database tool"
echo
echo "Secrets:"
echo " ls /mnt/secrets : List mounted secrets"
}
# Set some helpful environment variables
export EDITOR=vi
export PAGER=less
EOF
# Set up environment to always source our RC file
echo "export ENV=/root/.ashrc" > /etc/profile
echo "export ENV=/root/.ashrc" > /etc/environment
# Make RC file available to all shells
cp /root/.ashrc /etc/profile.d/motd.sh
ln -sf /root/.ashrc /root/.profile
ln -sf /root/.ashrc /root/.bashrc
ln -sf /root/.ashrc /root/.mkshrc
ln -sf /root/.ashrc /etc/shinit
# Create initial MOTD
update_motd
sleep 3600
""",
],
"env": env_vars
+ [
{"name": "MOUNTS", "value": ",".join(mount_info)},
{"name": "PVCS", "value": ",".join(pvc_info)},
{"name": "ENV", "value": "/root/.ashrc"},
],
"volumeMounts": formatted_mounts, "volumeMounts": formatted_mounts,
} }
], ],
"volumes": [format_volume(v) for v in volumes if format_volume(v)], "volumes": [format_volume(v) for v in volumes if format_volume(v)],
"imagePullSecrets": [{"name": imagepullsecret}] if imagepullsecret else None,
"restartPolicy": "Never", "restartPolicy": "Never",
}, },
}, },
@ -499,6 +559,43 @@ 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
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): def version_callback(value: bool):
if value: if value:
typer.echo(f"Version: {KRAYT_VERSION}") typer.echo(f"Version: {KRAYT_VERSION}")
@ -569,12 +666,19 @@ def exec(
typer.echo("No inspector selected.") typer.echo("No inspector selected.")
raise typer.Exit(1) raise typer.Exit(1)
# Execute the shell exec_command = [
typer.echo(f"Connecting to inspector {pod_namespace}/{pod_name}...")
os.execvp(
"kubectl", "kubectl",
["kubectl", "exec", "-it", "-n", pod_namespace, pod_name, "--", "sh", "-l"], "exec",
) "-it",
"-n",
pod_namespace,
pod_name,
"--",
"/bin/bash",
"-l",
]
os.execvp("kubectl", exec_command)
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}")
@ -669,13 +773,25 @@ def create(
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.",
), ),
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. 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. 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. The inspector will be created in the same namespace as the selected pod.
""" """
pods = get_pods(namespace) # For create, we want to list all pods, not just Krayt pods
pods = get_pods(namespace, label_selector=None)
if not pods: if not pods:
typer.echo("No pods found.") typer.echo("No pods found.")
raise typer.Exit(1) raise typer.Exit(1)
@ -689,7 +805,13 @@ def create(
volume_mounts, volumes = get_pod_volumes_and_mounts(pod_spec) volume_mounts, volumes = get_pod_volumes_and_mounts(pod_spec)
inspector_job = create_inspector_job( inspector_job = create_inspector_job(
client.CoreV1Api(), selected_namespace, selected_pod, volume_mounts, volumes client.CoreV1Api(),
selected_namespace,
selected_pod,
volume_mounts,
volumes,
image=image,
imagepullsecret=imagepullsecret,
) )
# Output the job manifest # Output the job manifest
@ -702,5 +824,59 @@ def version():
typer.echo(f"Version: {KRAYT_VERSION}") typer.echo(f"Version: {KRAYT_VERSION}")
if __name__ == "__main__": @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() app()
if __name__ == "__main__":
main()

337
krayt2.py Executable file
View file

@ -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 <package1> [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()

70
pyproject.toml Normal file
View file

@ -0,0 +1,70 @@
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[tool.hatch.build.targets.wheel]
[tool.hatch.build.targets.sdist]
exclude = ["/.github"]
[tool.hatch.build.targets.binary]
[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",
"inquirer",
"jinja2",
"iterfzf",
"pydantic",
"more-itertools",
]
[[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"
[tool.hatch.envs.default]
dependencies = [
"ruff",
"pyinstrument",
]
[tool.hatch.envs.default.scripts]
lint = "ruff check krayt"
format = "ruff format krayt"
lint-format = ['lint', 'format']

View file

@ -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,
) )

View file

@ -13,7 +13,7 @@ function fail {
echo "Error: $msg" 1>&2 echo "Error: $msg" 1>&2
exit 1 exit 1
} }
function check_deps { function check_uv {
if ! command -v uv &>/dev/null; then if ! command -v uv &>/dev/null; then
echo " Error: uv is not installed" echo " Error: uv is not installed"
echo "krayt requires uv to run. You can install it with:" echo "krayt requires uv to run. You can install it with:"
@ -24,15 +24,67 @@ function check_deps {
fail "uv not found" fail "uv not found"
fi fi
} }
function setup_config_dir {
# Create config directory
CONFIG_DIR="${HOME}/.config/krayt"
mkdir -p "${CONFIG_DIR}/init.d"
# Create example init script if it doesn't exist
EXAMPLE_SCRIPT="${CONFIG_DIR}/init.d/00_proxy.sh.example"
if [ ! -f "$EXAMPLE_SCRIPT" ]; then
cat > "$EXAMPLE_SCRIPT" << 'EOF'
#!/bin/sh
# Example initialization script for Krayt inspector pods
# This script runs before any packages are installed
# To use this script, rename it to remove the .example extension
# Example: Set up proxy configuration
setup_proxy() {
# Uncomment and modify these lines to set up your proxy
# export HTTP_PROXY="http://proxy.example.com:8080"
# export HTTPS_PROXY="http://proxy.example.com:8080"
# export NO_PROXY="localhost,127.0.0.1,.example.com"
# Set up proxy for apk if needed
if [ ! -z "$HTTP_PROXY" ]; then
echo "proxy = $HTTP_PROXY" >> /etc/apk/repositories
fi
}
# Example: Add custom Alpine repositories
setup_repos() {
# Uncomment to add custom repos
# echo "http://dl-cdn.alpinelinux.org/alpine/edge/testing" >> /etc/apk/repositories
:
}
# Run the setup functions
setup_proxy
setup_repos
# Log the configuration
echo "Krayt inspector pod initialization complete"
echo "Proxy settings:"
echo "HTTP_PROXY=$HTTP_PROXY"
echo "HTTPS_PROXY=$HTTPS_PROXY"
echo "NO_PROXY=$NO_PROXY"
EOF
fi
echo "Created config directory at ${CONFIG_DIR}"
echo "Example init script created at ${EXAMPLE_SCRIPT}"
}
function install { function install {
#settings #settings
USER="waylonwalker" USER="waylonwalker"
PROG="krayt" PROG="krayt"
ASPROG="krayt" ASPROG="krayt"
MOVE="true" MOVE="false"
RELEASE="{{VERSION}}" RELEASE="{{VERSION}}"
INSECURE="false" INSECURE="false"
OUT_DIR="/usr/local/bin" OUT_DIR="$(pwd)"
GH="https://github.com" GH="https://github.com"
#bash check #bash check
[ ! "$BASH_VERSION" ] && fail "Please use bash instead" [ ! "$BASH_VERSION" ] && fail "Please use bash instead"
@ -57,70 +109,126 @@ function install {
else else
fail "neither wget/curl are installed" fail "neither wget/curl are installed"
fi fi
#find OS #debug HTTP
if [ "$DEBUG" == "1" ]; then
GET="$GET -v"
fi
#optional auth to install from private repos
#NOTE: this also needs to be set on your instance of installer
AUTH="${GITHUB_TOKEN}"
if [ ! -z "$AUTH" ]; then
GET="$GET -H 'Authorization: $AUTH'"
fi
#find OS #TODO BSDs and other posixs
case $(uname -s) in case $(uname -s) in
Darwin) OS="darwin" ;; Darwin) OS="darwin" ;;
Linux) OS="linux" ;; Linux) OS="linux" ;;
*) fail "unknown os: $(uname -s)" ;; *) fail "unknown os: $(uname -s)" ;;
esac esac
#find ARCH #find ARCH
if uname -m | grep -E '(arm|aarch)64' >/dev/null; then if uname -m | grep -E '(arm|arch)64' >/dev/null; then
ARCH="aarch64" ARCH="arm64"
# no m1 assets. if on mac arm64, rosetta allows fallback to amd64
if [[ $OS = "darwin" ]]; then
ARCH="amd64"
fi
elif uname -m | grep 64 >/dev/null; then elif uname -m | grep 64 >/dev/null; then
ARCH="x86_64" ARCH="amd64"
elif uname -m | grep arm >/dev/null; then
ARCH="arm" #TODO armv6/v7
elif uname -m | grep 386 >/dev/null; then
ARCH="386"
else else
fail "unknown arch: $(uname -m)" fail "unknown arch: $(uname -m)"
fi fi
#choose from asset list #choose from asset list
URL="" URL=""
FTYPE="" FTYPE=""
VERSION=${RELEASE#v} case "${OS}_${ARCH}" in
if [[ $VERSION == "" ]]; then "linux_amd64")
VERSION=$(curl -s https://api.github.com/repos/$USER/$PROG/releases/latest | grep -o '"tag_name": "[^"]*' | cut -d'"' -f4) 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/krayt/releases/download/v${RELEASE}/krayt-${RELEASE}-aarch64-unknown-linux-gnu.tar.gz"
FTYPE=".tar.gz"
;;
*) fail "No asset for platform ${OS}-${ARCH}" ;;
esac
#got URL! download it...
echo -n "Downloading"
echo -n " $USER/$PROG"
if [ ! -z "$RELEASE" ]; then
echo -n " $RELEASE"
fi fi
if [[ $VERSION == "" ]]; then if [ ! -z "$ASPROG" ]; then
fail "cannot find latest version" echo -n " as $ASPROG"
fi fi
VERSION=${VERSION#v} echo -n " (${OS}/${ARCH})"
ASSET_URL="$GH/$USER/$PROG/releases/download/v$VERSION/${PROG}-${VERSION}-${ARCH}-unknown-${OS}-gnu.tar.gz"
echo "Installing $PROG v$VERSION..." echo "....."
echo "Downloading binary from $ASSET_URL"
#enter tempdir #enter tempdir
mkdir -p $TMP_DIR
cd $TMP_DIR cd $TMP_DIR
#download and unpack if [[ $FTYPE = ".gz" ]]; then
if [[ $ASSET_URL =~ \.gz$ ]]; then which gzip >/dev/null || fail "gzip is not installed"
which tar >/dev/null || fail "tar not installed" bash -c "$GET $URL" | gzip -d - >$PROG || fail "download failed"
if [[ $GET =~ ^curl ]]; then elif [[ $FTYPE = ".bz2" ]]; then
curl -s ${ASSET_URL} | tar zx || fail "download failed" which bzip2 >/dev/null || fail "bzip2 is not installed"
bash -c "$GET $URL" | bzip2 -d - >$PROG || fail "download failed"
elif [[ $FTYPE = ".tar.bz" ]] || [[ $FTYPE = ".tar.bz2" ]]; then
which tar >/dev/null || fail "tar is not installed"
which bzip2 >/dev/null || fail "bzip2 is not installed"
bash -c "$GET $URL" | tar jxf - || fail "download failed"
elif [[ $FTYPE = ".tar.gz" ]] || [[ $FTYPE = ".tgz" ]]; then
which tar >/dev/null || fail "tar is not installed"
which gzip >/dev/null || fail "gzip is not installed"
bash -c "$GET $URL" | tar zxf - || fail "download failed"
elif [[ $FTYPE = ".zip" ]]; then
which unzip >/dev/null || fail "unzip is not installed"
bash -c "$GET $URL" >tmp.zip || fail "download failed"
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"
else else
wget -qO- ${ASSET_URL} | tar zx || fail "download failed" fail "unknown file type: $FTYPE"
fi fi
#search subtree largest file (bin)
TMP_BIN=$(find . -type f | xargs du | sort -n | tail -n 1 | cut -f 2)
if [ ! -f "$TMP_BIN" ]; then
fail "could not find find binary (largest file)"
fi
#ensure its larger than 1MB
#TODO linux=elf/darwin=macho file detection?
if [[ $(du -m $TMP_BIN | cut -f1) -lt 1 ]]; then
fail "no binary found ($TMP_BIN is not larger than 1MB)"
fi
#move into PATH or cwd
chmod +x $TMP_BIN || fail "chmod +x failed"
DEST="$OUT_DIR/$PROG"
if [ ! -z "$ASPROG" ]; then
DEST="$OUT_DIR/$ASPROG"
fi
#move without sudo
OUT=$(mv $TMP_BIN $DEST 2>&1)
STATUS=$?
# failed and string contains "Permission denied"
if [ $STATUS -ne 0 ]; then
if [[ $OUT =~ "Permission denied" ]]; then
echo "mv with sudo..."
sudo mv $TMP_BIN $DEST || fail "sudo mv failed"
else else
fail "unknown file type: $ASSET_URL" fail "mv failed ($OUT)"
fi 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 fi
else echo "Downloaded to $DEST"
fail "cannot find binary" #done
fi
echo "Installation complete!"
cleanup cleanup
check_uv
} }
check_deps
install install
setup_config_dir

247
test.sh Normal file
View file

@ -0,0 +1,247 @@
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 <package1> [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="$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
}
installer() {
if [ $# -eq 0 ]; then
echo "Usage: installer <package1> [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 <<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."
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 <<EOF >/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

206
test.yaml Normal file
View file

@ -0,0 +1,206 @@
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 <package1> [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 <<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:
- 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 <<EOF >/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

View file

@ -1 +1 @@
0.0.0 0.2.0