release 0.3.0
release 0.3.0
This commit is contained in:
commit
abb7dacc86
31 changed files with 3202 additions and 359 deletions
21
.github/workflows/release-pypi.yaml
vendored
Normal file
21
.github/workflows/release-pypi.yaml
vendored
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
name: Release Krayt
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
paths:
|
||||
- "krayt/**"
|
||||
- "pyproject.toml"
|
||||
env:
|
||||
HATCH_INDEX_USER: __token__
|
||||
HATCH_INDEX_AUTH: ${{ secrets.pypi_password }}
|
||||
jobs:
|
||||
release-markata:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: waylonwalker/hatch-action@v4
|
||||
with:
|
||||
before-command: "lint-format"
|
||||
env:
|
||||
# required for gh release
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -962,4 +962,4 @@ FodyWeavers.xsd
|
|||
# Additional files built by Visual Studio
|
||||
|
||||
# End of https://www.toptal.com/developers/gitignore/api/vim,node,data,emacs,python,pycharm,executable,sublimetext,visualstudio,visualstudiocode
|
||||
krayt
|
||||
*.null-ls*
|
||||
|
|
|
|||
25
CHANGELOG.md
25
CHANGELOG.md
|
|
@ -1,3 +1,28 @@
|
|||
## 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
|
||||
|
||||
- Initial release
|
||||
|
|
|
|||
21
LICENSE
Normal file
21
LICENSE
Normal 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.
|
||||
80
README.md
80
README.md
|
|
@ -40,6 +40,12 @@ This will install the `krayt` command to `/usr/local/bin`.
|
|||
# Create a new inspector and apply it directly
|
||||
krayt create | kubectl apply -f -
|
||||
|
||||
# Use a custom image
|
||||
krayt create --image custom-image:latest | kubectl apply -f -
|
||||
|
||||
# Use a private image with pull secret
|
||||
krayt create --image private-registry.com/image:latest --imagepullsecret my-registry-secret | kubectl apply -f -
|
||||
|
||||
# Or review the manifest first
|
||||
krayt create > inspector.yaml
|
||||
kubectl apply -f inspector.yaml
|
||||
|
|
@ -67,6 +73,80 @@ Your inspector pod comes equipped with a full arsenal of tools:
|
|||
- **Network Tools**: `mtr`, `dig`
|
||||
- **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
|
||||
|
||||
> "Inside every volume lies a pearl of wisdom waiting to be discovered."
|
||||
|
|
|
|||
25
examples/init.d/00_proxy.sh
Normal file
25
examples/init.d/00_proxy.sh
Normal 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
|
||||
20
examples/init.d/10_install_git.sh
Normal file
20
examples/init.d/10_install_git.sh
Normal 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."
|
||||
19
examples/init.d/20_custom_repos.sh
Normal file
19
examples/init.d/20_custom_repos.sh
Normal 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."
|
||||
BIN
krayt-squooshed.png
Normal file
BIN
krayt-squooshed.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 511 KiB |
BIN
krayt.png
Normal file
BIN
krayt.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.8 MiB |
1
krayt/__about__.py
Normal file
1
krayt/__about__.py
Normal file
|
|
@ -0,0 +1 @@
|
|||
__version__ = "0.3.0"
|
||||
5
krayt/__init__.py
Normal file
5
krayt/__init__.py
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
from krayt.__about__ import __version__
|
||||
|
||||
__all__ = [
|
||||
"__version__",
|
||||
]
|
||||
88
krayt/bundles.py
Normal file
88
krayt/bundles.py
Normal 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
25
krayt/cli/__init__.py
Normal 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="templates", no_args_is_help=True)
|
||||
app.add_typer(pod_app, name="pod", no_args_is_help=True)
|
||||
app.command(name="create")(create)
|
||||
app.command(name="c")(create)
|
||||
app.command(name="clean")(clean)
|
||||
app.command(name="exec")(exec)
|
||||
app.command(name="logs")(logs)
|
||||
app.add_typer(bundles_app, name="bundles", no_args_is_help=True)
|
||||
|
||||
|
||||
@app.command()
|
||||
def version():
|
||||
print(__version__)
|
||||
|
||||
|
||||
def main():
|
||||
app()
|
||||
25
krayt/cli/bundles.py
Normal file
25
krayt/cli/bundles.py
Normal 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}")
|
||||
687
krayt.py → krayt/cli/pod.py
Executable file → Normal file
687
krayt.py → krayt/cli/pod.py
Executable file → Normal file
|
|
@ -1,38 +1,14 @@
|
|||
#!/usr/bin/env -S uv run --quiet --script
|
||||
# /// script
|
||||
# requires-python = ">=3.10"
|
||||
# dependencies = [
|
||||
# "typer",
|
||||
# "kubernetes",
|
||||
# "iterfzf"
|
||||
# ]
|
||||
# ///
|
||||
"""
|
||||
Krayt - The Kubernetes Volume Inspector
|
||||
|
||||
Like cracking open a Krayt dragon pearl, this tool helps you inspect what's inside your Kubernetes volumes.
|
||||
Hunt down storage issues and explore your persistent data like a true Tatooine dragon hunter.
|
||||
|
||||
Features:
|
||||
- Create inspector pods with all the tools you need
|
||||
- Access volumes and device mounts from any pod
|
||||
- Fuzzy search across all namespaces
|
||||
- Built-in tools for file exploration and analysis
|
||||
- Automatic cleanup of inspector pods
|
||||
|
||||
May the Force be with your volumes!
|
||||
"""
|
||||
|
||||
from iterfzf import iterfzf
|
||||
import iterfzf
|
||||
from krayt.templates import env
|
||||
from kubernetes import client, config
|
||||
import logging
|
||||
import os
|
||||
import time
|
||||
import typer
|
||||
from typing import Any, Optional
|
||||
from typing import Any, List, Optional
|
||||
import yaml
|
||||
from krayt.__about__ import __version__
|
||||
|
||||
KRAYT_VERSION = "NIGHTLY"
|
||||
|
||||
logging.basicConfig(level=logging.WARNING)
|
||||
|
||||
|
|
@ -106,47 +82,65 @@ def fuzzy_select(items):
|
|||
if not items:
|
||||
return None, None
|
||||
|
||||
# Format items as "namespace/name" for display
|
||||
formatted_items = [f"{ns}/{name}" for name, ns in items]
|
||||
logging.debug(f"Found {len(formatted_items)} pods")
|
||||
# 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:
|
||||
# Use iterfzf for selection
|
||||
selected = iterfzf(formatted_items)
|
||||
# selected = inquirer.fuzzy(
|
||||
# message="Select a pod to clone:", choices=formatted_items
|
||||
# ).execute()
|
||||
|
||||
if selected:
|
||||
namespace, name = selected.split("/")
|
||||
logging.debug(f"Selected pod {name} in namespace {namespace}")
|
||||
return name, namespace
|
||||
else:
|
||||
logging.debug("No selection made")
|
||||
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:
|
||||
logging.error(f"Error during selection: {e}", exc_info=True)
|
||||
typer.echo(f"Error during selection: {e}", err=True)
|
||||
raise typer.Exit(1)
|
||||
typer.echo(f"Error during selection: {e}")
|
||||
return None, None
|
||||
|
||||
|
||||
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"""
|
||||
config.load_kube_config()
|
||||
v1 = client.CoreV1Api()
|
||||
|
||||
try:
|
||||
config.load_kube_config()
|
||||
api = client.CoreV1Api()
|
||||
if namespace:
|
||||
logging.debug(f"Listing pods in namespace {namespace}")
|
||||
pod_list = v1.list_namespaced_pod(namespace=namespace)
|
||||
pods = api.list_namespaced_pod(
|
||||
namespace=namespace,
|
||||
label_selector=label_selector,
|
||||
)
|
||||
else:
|
||||
logging.debug("Listing pods in all namespaces")
|
||||
pod_list = v1.list_pod_for_all_namespaces()
|
||||
pods = api.list_pod_for_all_namespaces(
|
||||
label_selector=label_selector,
|
||||
)
|
||||
|
||||
pods = [(pod.metadata.name, pod.metadata.namespace) for pod in pod_list.items]
|
||||
logging.debug(f"Found {len(pods)} pods")
|
||||
return pods
|
||||
except client.exceptions.ApiException as e:
|
||||
logging.error(f"Error listing pods: {e}")
|
||||
typer.echo(f"Error listing pods: {e}", err=True)
|
||||
# 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)
|
||||
|
||||
|
||||
|
|
@ -205,57 +199,73 @@ def get_pod_volumes_and_mounts(pod_spec):
|
|||
return volume_mounts, volumes
|
||||
|
||||
|
||||
def get_pod_env_and_secrets(api, namespace, pod_name):
|
||||
pod = api.read_namespaced_pod(pod_name, namespace)
|
||||
|
||||
# Get environment variables from the pod
|
||||
def get_env_vars_and_secret_volumes(api, namespace: str):
|
||||
"""Get environment variables and secret volumes for the inspector pod"""
|
||||
env_vars = []
|
||||
for container in pod.spec.containers:
|
||||
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)
|
||||
volumes = []
|
||||
|
||||
# Get all volume mounts that are secrets
|
||||
secret_volumes = []
|
||||
if pod.spec.volumes:
|
||||
secret_volumes = [v for v in pod.spec.volumes if v.secret]
|
||||
# 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",
|
||||
]
|
||||
|
||||
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 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,
|
||||
additional_packages: Optional[List[str]] = None,
|
||||
pre_init_scripts: Optional[List[str]] = None,
|
||||
post_init_scripts: Optional[List[str]] = None,
|
||||
pre_init_hooks: Optional[List[str]] = None,
|
||||
post_init_hooks: Optional[List[str]] = None,
|
||||
):
|
||||
"""Create a Krayt inspector job with the given mounts"""
|
||||
timestamp = int(time.time())
|
||||
job_name = f"{pod_name}-krayt-{timestamp}"
|
||||
|
||||
# Get environment variables and secrets from the target pod
|
||||
env_vars, secret_volumes = get_pod_env_and_secrets(api, namespace, pod_name)
|
||||
# Get environment variables and secret volumes from the target pod
|
||||
env_vars, secret_volumes = get_env_vars_and_secret_volumes(api, namespace)
|
||||
|
||||
# Add secret volumes to our volumes list
|
||||
volumes.extend(secret_volumes)
|
||||
|
|
@ -286,6 +296,23 @@ def create_inspector_job(
|
|||
if hasattr(v, "persistent_volume_claim") and v.persistent_volume_claim:
|
||||
pvc_info.append(f"{v.name}:{v.persistent_volume_claim.claim_name}")
|
||||
|
||||
template_name = "base.sh"
|
||||
template = env.get_template(template_name)
|
||||
pvcs = None
|
||||
pre_init_scripts = None
|
||||
post_init_scripts = None
|
||||
pre_init_hooks = None
|
||||
post_init_hooks = None
|
||||
command = template.render(
|
||||
volumes=volumes,
|
||||
pvcs=pvcs,
|
||||
additional_packages=additional_packages,
|
||||
pre_init_scripts=pre_init_scripts,
|
||||
post_init_scripts=post_init_scripts,
|
||||
pre_init_hooks=pre_init_hooks,
|
||||
post_init_hooks=post_init_hooks,
|
||||
)
|
||||
|
||||
inspector_job = {
|
||||
"apiVersion": "batch/v1",
|
||||
"kind": "Job",
|
||||
|
|
@ -293,187 +320,26 @@ def create_inspector_job(
|
|||
"name": job_name,
|
||||
"namespace": namespace,
|
||||
"labels": {"app": "krayt"},
|
||||
"annotations": {"pvcs": ",".join(pvc_info) if pvc_info else "none"},
|
||||
},
|
||||
"spec": {
|
||||
"ttlSecondsAfterFinished": 0, # Delete immediately after completion
|
||||
"ttlSecondsAfterFinished": 600,
|
||||
"template": {
|
||||
"metadata": {"labels": {"app": "krayt"}},
|
||||
"spec": {
|
||||
"containers": [
|
||||
{
|
||||
"name": "krayt",
|
||||
"image": "alpine:latest", # Use Alpine as base for package management
|
||||
"command": [
|
||||
"sh",
|
||||
"-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"},
|
||||
],
|
||||
"name": "inspector",
|
||||
"image": image,
|
||||
"command": ["sh", "-c", command],
|
||||
"env": env_vars,
|
||||
"volumeMounts": formatted_mounts,
|
||||
}
|
||||
],
|
||||
"volumes": [format_volume(v) for v in volumes if format_volume(v)],
|
||||
"imagePullSecrets": [{"name": imagepullsecret}]
|
||||
if imagepullsecret
|
||||
else None,
|
||||
"restartPolicy": "Never",
|
||||
},
|
||||
},
|
||||
|
|
@ -499,37 +365,32 @@ PROTECTED_NAMESPACES = {
|
|||
}
|
||||
|
||||
|
||||
def setup_environment():
|
||||
"""Set up the environment with proxy settings and other configurations"""
|
||||
# Load environment variables for proxies
|
||||
proxy_vars = [
|
||||
"HTTP_PROXY",
|
||||
"HTTPS_PROXY",
|
||||
"NO_PROXY",
|
||||
"http_proxy",
|
||||
"https_proxy",
|
||||
"no_proxy",
|
||||
]
|
||||
|
||||
for var in proxy_vars:
|
||||
if var in os.environ:
|
||||
# Make both upper and lower case versions available
|
||||
os.environ[var.upper()] = os.environ[var]
|
||||
os.environ[var.lower()] = os.environ[var]
|
||||
|
||||
|
||||
def version_callback(value: bool):
|
||||
if value:
|
||||
typer.echo(f"Version: {KRAYT_VERSION}")
|
||||
typer.echo(f"Version: {__version__}")
|
||||
raise typer.Exit()
|
||||
|
||||
|
||||
@app.callback(invoke_without_command=True)
|
||||
def main(
|
||||
ctx: typer.Context,
|
||||
version: bool = typer.Option(
|
||||
False, "--version", "-v", help="Show version", callback=version_callback
|
||||
),
|
||||
):
|
||||
"""
|
||||
Krack open a Krayt dragon!
|
||||
"""
|
||||
if ctx.invoked_subcommand is None:
|
||||
ctx.get_help()
|
||||
|
||||
|
||||
@app.command()
|
||||
def exec(
|
||||
namespace: Optional[str] = typer.Option(
|
||||
None,
|
||||
help="Kubernetes namespace. If not specified, will search for inspectors across all namespaces.",
|
||||
),
|
||||
):
|
||||
"""
|
||||
Enter the Krayt dragon's lair! Connect to a running inspector pod.
|
||||
If multiple inspectors are found, you'll get to choose which one to explore.
|
||||
"""
|
||||
def get_pod(namespace: Optional[str] = None):
|
||||
config.load_kube_config()
|
||||
batch_api = client.BatchV1Api()
|
||||
|
||||
|
|
@ -569,18 +430,76 @@ def exec(
|
|||
typer.echo("No inspector selected.")
|
||||
raise typer.Exit(1)
|
||||
|
||||
# Execute the shell
|
||||
typer.echo(f"Connecting to inspector {pod_namespace}/{pod_name}...")
|
||||
os.execvp(
|
||||
"kubectl",
|
||||
["kubectl", "exec", "-it", "-n", pod_namespace, pod_name, "--", "sh", "-l"],
|
||||
)
|
||||
|
||||
except client.exceptions.ApiException as e:
|
||||
logging.error(f"Failed to list jobs: {e}")
|
||||
typer.echo(f"Failed to list jobs: {e}", err=True)
|
||||
raise typer.Exit(1)
|
||||
|
||||
return pod_name, pod_namespace
|
||||
|
||||
|
||||
@app.command()
|
||||
def exec(
|
||||
namespace: Optional[str] = typer.Option(
|
||||
None,
|
||||
help="Kubernetes namespace. If not specified, will search for inspectors across all namespaces.",
|
||||
),
|
||||
):
|
||||
"""
|
||||
Enter the Krayt dragon's lair! Connect to a running inspector pod.
|
||||
If multiple inspectors are found, you'll get to choose which one to explore.
|
||||
"""
|
||||
|
||||
pod_name, pod_namespace = get_pod(namespace)
|
||||
exec_command = [
|
||||
"kubectl",
|
||||
"exec",
|
||||
"-it",
|
||||
"-n",
|
||||
pod_namespace,
|
||||
pod_name,
|
||||
"--",
|
||||
"/bin/bash",
|
||||
"-l",
|
||||
]
|
||||
|
||||
os.execvp("kubectl", exec_command)
|
||||
|
||||
|
||||
@app.command()
|
||||
def port_forward(
|
||||
namespace: Optional[str] = typer.Option(
|
||||
None,
|
||||
help="Kubernetes namespace. If not specified, will search for inspectors across all namespaces.",
|
||||
),
|
||||
port: str = typer.Option(
|
||||
"8080:8080",
|
||||
"--port",
|
||||
"-p",
|
||||
help="Port to forward to the inspector pod",
|
||||
),
|
||||
):
|
||||
"""
|
||||
Enter the Krayt dragon's lair! Connect to a running inspector pod.
|
||||
If multiple inspectors are found, you'll get to choose which one to explore.
|
||||
"""
|
||||
if ":" not in port:
|
||||
# if port does not contain a ":" it should be an int
|
||||
port = int(port)
|
||||
port = f"{port}:{port}"
|
||||
|
||||
pod_name, pod_namespace = get_pod(namespace)
|
||||
port_forward_command = [
|
||||
"kubectl",
|
||||
"port-forward",
|
||||
"-n",
|
||||
pod_namespace,
|
||||
pod_name,
|
||||
port,
|
||||
]
|
||||
|
||||
os.execvp("kubectl", port_forward_command)
|
||||
|
||||
|
||||
@app.command()
|
||||
def clean(
|
||||
|
|
@ -667,14 +586,140 @@ def clean(
|
|||
def create(
|
||||
namespace: Optional[str] = typer.Option(
|
||||
None,
|
||||
"--namespace",
|
||||
"-n",
|
||||
help="Kubernetes namespace. If not specified, will search for pods across all namespaces.",
|
||||
),
|
||||
clone: Optional[str] = typer.Option(
|
||||
None,
|
||||
"--clone",
|
||||
"-c",
|
||||
help="Clone an existing pod",
|
||||
),
|
||||
image: str = typer.Option(
|
||||
"alpine:latest",
|
||||
"--image",
|
||||
"-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",
|
||||
),
|
||||
):
|
||||
"""
|
||||
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 = None
|
||||
selected_pod = None
|
||||
|
||||
if namespace is None and clone is not None and "/" in clone:
|
||||
selected_namespace, selected_pod = clone.split("/", 1)
|
||||
elif namespace is not None and clone is not None:
|
||||
selected_namespace = namespace
|
||||
selected_pod = clone
|
||||
|
||||
pods = get_pods(namespace, label_selector=None)
|
||||
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)
|
||||
|
||||
# typer.echo(f"Selected pod exists: {selected_pod in (p[0] for p in pods)}")
|
||||
# typer.echo(f"Selected pod: {selected_pod} ({selected_namespace})")
|
||||
|
||||
pod_spec = get_pod_spec(selected_pod, selected_namespace)
|
||||
volume_mounts, volumes = get_pod_volumes_and_mounts(pod_spec)
|
||||
|
||||
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
|
||||
typer.echo(yaml.dump(clean_dict(inspector_job), sort_keys=False))
|
||||
|
||||
|
||||
@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.")
|
||||
|
|
@ -685,22 +730,42 @@ def create(
|
|||
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)
|
||||
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,
|
||||
)
|
||||
|
||||
inspector_job = create_inspector_job(
|
||||
client.CoreV1Api(), selected_namespace, selected_pod, volume_mounts, volumes
|
||||
)
|
||||
if follow:
|
||||
for line in logs:
|
||||
typer.echo(line.decode("utf-8").strip())
|
||||
else:
|
||||
typer.echo(logs.read().decode("utf-8"))
|
||||
|
||||
# Output the job manifest
|
||||
typer.echo(yaml.dump(clean_dict(inspector_job), sort_keys=False))
|
||||
except client.rest.ApiException as e:
|
||||
typer.echo(f"Error reading logs: {e}")
|
||||
raise typer.Exit(1)
|
||||
|
||||
|
||||
@app.command()
|
||||
def version():
|
||||
"""Show the version of Krayt."""
|
||||
typer.echo(f"Version: {KRAYT_VERSION}")
|
||||
@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__":
|
||||
app()
|
||||
main()
|
||||
98
krayt/cli/templates.py
Normal file
98
krayt/cli/templates.py
Normal 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 base(
|
||||
volumes: Optional[List[str]] = typer.Option(
|
||||
None,
|
||||
"--volume",
|
||||
),
|
||||
pvcs: Optional[List[str]] = typer.Option(
|
||||
None,
|
||||
"--pvc",
|
||||
),
|
||||
additional_packages: Optional[List[str]] = typer.Option(
|
||||
None, "--additional-packages", "-ap"
|
||||
),
|
||||
pre_init_scripts: Optional[List[str]] = typer.Option(
|
||||
None,
|
||||
"--pre-init-scripts",
|
||||
help="additional scripts to execute at the end of container initialization",
|
||||
),
|
||||
post_init_scripts: Optional[List[str]] = typer.Option(
|
||||
None,
|
||||
"--post-init-scripts",
|
||||
"--init-scripts",
|
||||
help="additional scripts to execute at the start of container initialization",
|
||||
),
|
||||
pre_init_hooks: Optional[List[str]] = typer.Option(
|
||||
None,
|
||||
"--pre-init-hooks",
|
||||
help="additional hooks to execute at the end of container initialization",
|
||||
),
|
||||
post_init_hooks: Optional[List[str]] = typer.Option(
|
||||
None,
|
||||
"--post-init-hooks",
|
||||
"--init-hooks",
|
||||
help="additional hooks to execute at the start of container initialization",
|
||||
),
|
||||
):
|
||||
template_name = "base.sh"
|
||||
template = env.get_template(template_name)
|
||||
rendered = template.render(
|
||||
volumes=volumes,
|
||||
pvcs=pvcs,
|
||||
additional_packages=additional_packages,
|
||||
pre_init_scripts=pre_init_scripts,
|
||||
post_init_scripts=post_init_scripts,
|
||||
pre_init_hooks=pre_init_hooks,
|
||||
post_init_hooks=post_init_hooks,
|
||||
)
|
||||
print(rendered)
|
||||
|
||||
|
||||
@app.command()
|
||||
def install(
|
||||
additional_packages: Optional[List[str]] = typer.Option(
|
||||
..., "--additional-packages", "-ap"
|
||||
),
|
||||
):
|
||||
template_name = "install.sh"
|
||||
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
191
krayt/package.py
Normal 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
13
krayt/templates.py
Normal 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
3
krayt/templates/.kraytrc
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
if [ -t 1 ] && [ -f /etc/motd ]; then
|
||||
cat /etc/motd
|
||||
fi
|
||||
25
krayt/templates/base.sh
Normal file
25
krayt/templates/base.sh
Normal 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
|
||||
88
krayt/templates/install.sh
Normal file
88
krayt/templates/install.sh
Normal 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
116
krayt/templates/kraytrc.sh
Normal 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
40
krayt/templates/motd.sh
Normal 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 volumes %}
|
||||
|
||||
Mounted Volumes:
|
||||
{%- for volume in volumes %}
|
||||
- {{ volume }}
|
||||
{%- endfor %}
|
||||
{%- endif %}
|
||||
|
||||
{%- if pvcs %}
|
||||
|
||||
Persistent Volume Claims:
|
||||
{%- for pvc in pvcs %}
|
||||
- {{ pvc }}
|
||||
{%- endfor %}
|
||||
{%- endif %}
|
||||
|
||||
{%- if secrets %}
|
||||
|
||||
Mounted Secrets:
|
||||
{%- for secret in secrets %}
|
||||
- {{ secret }}
|
||||
{%- endfor %}
|
||||
{%- endif %}
|
||||
|
||||
{%- if additional_packages %}
|
||||
|
||||
Additional Packages:
|
||||
{%- for package in additional_packages %}
|
||||
- {{ package }}
|
||||
{%- endfor %}
|
||||
{%- endif %}
|
||||
|
||||
EOF
|
||||
882
krayt1.py
Executable file
882
krayt1.py
Executable file
|
|
@ -0,0 +1,882 @@
|
|||
#!/usr/bin/env -S uv run --quiet --script
|
||||
# /// script
|
||||
# requires-python = ">=3.10"
|
||||
# dependencies = [
|
||||
# "typer",
|
||||
# "kubernetes",
|
||||
# "iterfzf"
|
||||
# ]
|
||||
# ///
|
||||
"""
|
||||
Krayt - The Kubernetes Volume Inspector
|
||||
|
||||
Like cracking open a Krayt dragon pearl, this tool helps you inspect what's inside your Kubernetes volumes.
|
||||
Hunt down storage issues and explore your persistent data like a true Tatooine dragon hunter.
|
||||
|
||||
May the Force be with your volumes!
|
||||
"""
|
||||
|
||||
from iterfzf import iterfzf
|
||||
from kubernetes import client, config
|
||||
import logging
|
||||
import os
|
||||
from pathlib import Path
|
||||
import time
|
||||
import typer
|
||||
from typing import Any, Optional
|
||||
import yaml
|
||||
|
||||
KRAYT_VERSION = "NIGHTLY"
|
||||
|
||||
logging.basicConfig(level=logging.WARNING)
|
||||
|
||||
app = typer.Typer()
|
||||
|
||||
|
||||
def clean_dict(d: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Remove None values and empty dicts from a dictionary recursively."""
|
||||
if not isinstance(d, dict):
|
||||
return d
|
||||
return {
|
||||
k: clean_dict(v)
|
||||
for k, v in d.items()
|
||||
if v is not None and v != {} and not (isinstance(v, dict) and not clean_dict(v))
|
||||
}
|
||||
|
||||
|
||||
def format_volume_mount(vm: client.V1VolumeMount) -> dict[str, Any]:
|
||||
"""Format volume mount with only relevant fields."""
|
||||
# Skip Kubernetes service account mounts
|
||||
if vm.mount_path.startswith("/var/run/secrets/kubernetes.io/"):
|
||||
return None
|
||||
|
||||
return clean_dict(
|
||||
{
|
||||
"name": vm.name,
|
||||
"mountPath": vm.mount_path,
|
||||
"readOnly": vm.read_only if vm.read_only else None,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def format_volume(v: client.V1Volume) -> dict[str, Any]:
|
||||
"""Format volume into a dictionary, return None if it should be skipped"""
|
||||
# Skip Kubernetes service account volumes
|
||||
if v.name.startswith("kube-api-access-"):
|
||||
return None
|
||||
|
||||
volume_source = None
|
||||
if v.persistent_volume_claim:
|
||||
volume_source = {
|
||||
"persistentVolumeClaim": {"claimName": v.persistent_volume_claim.claim_name}
|
||||
}
|
||||
elif v.config_map:
|
||||
volume_source = {"configMap": {"name": v.config_map.name}}
|
||||
elif v.secret:
|
||||
volume_source = {"secret": {"secretName": v.secret.secret_name}}
|
||||
elif v.host_path: # Add support for hostPath volumes (used for device mounts)
|
||||
volume_source = {
|
||||
"hostPath": {
|
||||
"path": v.host_path.path,
|
||||
"type": v.host_path.type if v.host_path.type else None,
|
||||
}
|
||||
}
|
||||
elif v.empty_dir: # Add support for emptyDir volumes (used for /dev/shm)
|
||||
volume_source = {
|
||||
"emptyDir": {
|
||||
"medium": v.empty_dir.medium if v.empty_dir.medium else None,
|
||||
"sizeLimit": v.empty_dir.size_limit if v.empty_dir.size_limit else None,
|
||||
}
|
||||
}
|
||||
|
||||
if not volume_source:
|
||||
return None
|
||||
|
||||
return clean_dict({"name": v.name, **volume_source})
|
||||
|
||||
|
||||
def fuzzy_select(items):
|
||||
"""Use fzf to select from a list of (name, namespace) tuples"""
|
||||
if not items:
|
||||
return None, None
|
||||
|
||||
# If there's only one item, return it without prompting
|
||||
if len(items) == 1:
|
||||
return items[0]
|
||||
|
||||
# Format items for display
|
||||
formatted_items = [f"{name} ({namespace})" for name, namespace in items]
|
||||
|
||||
# Use fzf for selection
|
||||
try:
|
||||
selected = iterfzf(formatted_items)
|
||||
if not selected:
|
||||
return None, None
|
||||
|
||||
# Parse selection back into name and namespace
|
||||
# Example: "pod-name (namespace)" -> ("pod-name", "namespace")
|
||||
name = selected.split(" (")[0]
|
||||
namespace = selected.split(" (")[1][:-1]
|
||||
return name, namespace
|
||||
|
||||
except Exception as e:
|
||||
typer.echo(f"Error during selection: {e}")
|
||||
return None, None
|
||||
|
||||
|
||||
def get_pods(
|
||||
namespace=None,
|
||||
label_selector: str = "app=krayt",
|
||||
):
|
||||
"""Get list of pods in the specified namespace or all namespaces"""
|
||||
try:
|
||||
config.load_kube_config()
|
||||
api = client.CoreV1Api()
|
||||
if namespace:
|
||||
pods = api.list_namespaced_pod(
|
||||
namespace=namespace,
|
||||
label_selector=label_selector,
|
||||
)
|
||||
else:
|
||||
pods = api.list_pod_for_all_namespaces(
|
||||
label_selector=label_selector,
|
||||
)
|
||||
|
||||
# Convert to list of (name, namespace) tuples
|
||||
pod_list = []
|
||||
for pod in pods.items:
|
||||
if pod.metadata.namespace not in PROTECTED_NAMESPACES:
|
||||
pod_list.append((pod.metadata.name, pod.metadata.namespace))
|
||||
return pod_list
|
||||
|
||||
except client.rest.ApiException as e:
|
||||
typer.echo(f"Error listing pods: {e}")
|
||||
raise typer.Exit(1)
|
||||
|
||||
|
||||
def get_pod_spec(pod_name, namespace):
|
||||
config.load_kube_config()
|
||||
v1 = client.CoreV1Api()
|
||||
return v1.read_namespaced_pod(pod_name, namespace)
|
||||
|
||||
|
||||
def get_pod_volumes_and_mounts(pod_spec):
|
||||
"""Extract all volumes and mounts from a pod spec"""
|
||||
volume_mounts = []
|
||||
for container in pod_spec.spec.containers:
|
||||
if container.volume_mounts:
|
||||
volume_mounts.extend(container.volume_mounts)
|
||||
|
||||
# Filter out None values from volume mounts
|
||||
volume_mounts = [vm for vm in volume_mounts if format_volume_mount(vm)]
|
||||
|
||||
# Get all volumes, including device mounts
|
||||
volumes = []
|
||||
if pod_spec.spec.volumes:
|
||||
for v in pod_spec.spec.volumes:
|
||||
# Handle device mounts
|
||||
if v.name in ["cache-volume"]:
|
||||
volumes.append(
|
||||
client.V1Volume(
|
||||
name=v.name,
|
||||
empty_dir=client.V1EmptyDirVolumeSource(medium="Memory"),
|
||||
)
|
||||
)
|
||||
elif v.name in ["coral-device"]:
|
||||
volumes.append(
|
||||
client.V1Volume(
|
||||
name=v.name,
|
||||
host_path=client.V1HostPathVolumeSource(
|
||||
path="/dev/apex_0", type="CharDevice"
|
||||
),
|
||||
)
|
||||
)
|
||||
elif v.name in ["qsv-device"]:
|
||||
volumes.append(
|
||||
client.V1Volume(
|
||||
name=v.name,
|
||||
host_path=client.V1HostPathVolumeSource(
|
||||
path="/dev/dri", type="Directory"
|
||||
),
|
||||
)
|
||||
)
|
||||
else:
|
||||
volumes.append(v)
|
||||
|
||||
# Filter out None values from volumes
|
||||
volumes = [v for v in volumes if format_volume(v)]
|
||||
|
||||
return volume_mounts, volumes
|
||||
|
||||
|
||||
def get_env_vars_and_secret_volumes(api, namespace: str):
|
||||
"""Get environment variables and secret volumes for the inspector pod"""
|
||||
env_vars = []
|
||||
volumes = []
|
||||
|
||||
# Add proxy environment variables if they exist in the host environment
|
||||
proxy_vars = [
|
||||
"HTTP_PROXY",
|
||||
"HTTPS_PROXY",
|
||||
"NO_PROXY",
|
||||
"http_proxy",
|
||||
"https_proxy",
|
||||
"no_proxy",
|
||||
]
|
||||
|
||||
for var in proxy_vars:
|
||||
if var in os.environ:
|
||||
env_vars.append({"name": var, "value": os.environ[var]})
|
||||
|
||||
# Look for secret volumes in the namespace
|
||||
try:
|
||||
secrets = api.list_namespaced_secret(namespace)
|
||||
for secret in secrets.items:
|
||||
# Skip service account tokens and other system secrets
|
||||
if secret.type != "Opaque" or secret.metadata.name.startswith(
|
||||
"default-token-"
|
||||
):
|
||||
continue
|
||||
|
||||
# Mount each secret as a volume
|
||||
volume_name = f"secret-{secret.metadata.name}"
|
||||
volumes.append(
|
||||
client.V1Volume(
|
||||
name=volume_name,
|
||||
secret=client.V1SecretVolumeSource(
|
||||
secret_name=secret.metadata.name
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
except client.exceptions.ApiException as e:
|
||||
if e.status != 404: # Ignore if no secrets found
|
||||
logging.warning(f"Failed to list secrets in namespace {namespace}: {e}")
|
||||
|
||||
return env_vars, volumes
|
||||
|
||||
|
||||
def get_init_scripts():
|
||||
"""Get the contents of init scripts to be run in the pod"""
|
||||
init_dir = Path.home() / ".config" / "krayt" / "init.d"
|
||||
if not init_dir.exists():
|
||||
logging.debug("No init.d directory found at %s", init_dir)
|
||||
return ""
|
||||
|
||||
scripts = sorted(init_dir.glob("*.sh"))
|
||||
if not scripts:
|
||||
logging.debug("No init scripts found in %s", init_dir)
|
||||
return ""
|
||||
|
||||
# Create a combined script that will run all init scripts
|
||||
init_script = "#!/bin/bash\n\n"
|
||||
init_script += "exec 2>&1 # Redirect stderr to stdout for proper logging\n"
|
||||
init_script += "set -e # Exit on error\n\n"
|
||||
init_script += "echo 'Running initialization scripts...' | tee /tmp/init.log\n\n"
|
||||
init_script += "mkdir -p /tmp/init.d\n\n" # Create directory once at the start
|
||||
|
||||
for script in scripts:
|
||||
try:
|
||||
with open(script, "r") as f:
|
||||
script_content = f.read()
|
||||
if not script_content.strip():
|
||||
logging.debug("Skipping empty script %s", script)
|
||||
continue
|
||||
|
||||
# Use a unique heredoc delimiter for each script to avoid nesting issues
|
||||
delimiter = f"EOF_SCRIPT_{script.stem.upper()}"
|
||||
|
||||
init_script += f"echo '=== Running {script.name} ===' | tee -a /tmp/init.log\n"
|
||||
init_script += f"cat > /tmp/init.d/{script.name} << '{delimiter}'\n"
|
||||
init_script += script_content
|
||||
if not script_content.endswith("\n"):
|
||||
init_script += "\n"
|
||||
init_script += f"{delimiter}\n"
|
||||
init_script += f"chmod +x /tmp/init.d/{script.name}\n"
|
||||
init_script += f"cd /tmp/init.d && ./{script.name} 2>&1 | tee -a /tmp/init.log || {{ echo \"Failed to run {script.name}\"; exit 1; }}\n"
|
||||
init_script += f"echo '=== Finished {script.name} ===' | tee -a /tmp/init.log\n\n"
|
||||
except Exception as e:
|
||||
logging.error(f"Failed to load init script {script}: {e}")
|
||||
|
||||
init_script += "echo 'Initialization scripts complete.' | tee -a /tmp/init.log\n"
|
||||
return init_script
|
||||
|
||||
|
||||
def get_motd_script(mount_info, pvc_info):
|
||||
"""Generate the MOTD script with proper escaping"""
|
||||
return f"""
|
||||
# Create MOTD
|
||||
cat << EOF > /etc/motd
|
||||
====================================
|
||||
Krayt Dragon's Lair
|
||||
A safe haven for volume inspection
|
||||
====================================
|
||||
|
||||
"Inside every volume lies a pearl of wisdom waiting to be discovered."
|
||||
|
||||
Mounted Volumes:
|
||||
$(echo "{",".join(mount_info)}" | tr ',' '\\n' | sed 's/^/- /')
|
||||
|
||||
Persistent Volume Claims:
|
||||
$(echo "{",".join(pvc_info)}" | tr ',' '\\n' | sed 's/^/- /')
|
||||
|
||||
Mounted Secrets:
|
||||
$(for d in /mnt/secrets/*; do if [ -d "$d" ]; then echo "- $(basename $d)"; fi; done)
|
||||
|
||||
Init Script Status:
|
||||
$(if [ -f /tmp/init.log ]; then echo "View initialization log at /tmp/init.log"; fi)
|
||||
EOF
|
||||
"""
|
||||
|
||||
|
||||
def create_inspector_job(
|
||||
api,
|
||||
namespace: str,
|
||||
pod_name: str,
|
||||
volume_mounts: list,
|
||||
volumes: list,
|
||||
image: str = "alpine:latest",
|
||||
imagepullsecret: Optional[str] = None,
|
||||
):
|
||||
"""Create a Krayt inspector job with the given mounts"""
|
||||
timestamp = int(time.time())
|
||||
job_name = f"{pod_name}-krayt-{timestamp}"
|
||||
|
||||
# Get environment variables and secret volumes from the target pod
|
||||
env_vars, secret_volumes = get_env_vars_and_secret_volumes(api, namespace)
|
||||
|
||||
# Add secret volumes to our volumes list
|
||||
volumes.extend(secret_volumes)
|
||||
|
||||
# Create corresponding volume mounts for secrets
|
||||
secret_mounts = []
|
||||
for vol in secret_volumes:
|
||||
secret_mounts.append(
|
||||
{
|
||||
"name": vol.name,
|
||||
"mountPath": f"/mnt/secrets/{vol.secret.secret_name}",
|
||||
"readOnly": True,
|
||||
}
|
||||
)
|
||||
|
||||
# Convert volume mounts to dictionaries
|
||||
formatted_mounts = [format_volume_mount(vm) for vm in volume_mounts]
|
||||
formatted_mounts.extend(secret_mounts)
|
||||
|
||||
# Format mount and PVC info for MOTD
|
||||
mount_info = []
|
||||
for vm in formatted_mounts:
|
||||
if vm:
|
||||
mount_info.append(f"{vm['name']}:{vm['mountPath']}")
|
||||
|
||||
pvc_info = []
|
||||
for v in volumes:
|
||||
if hasattr(v, "persistent_volume_claim") and v.persistent_volume_claim:
|
||||
pvc_info.append(f"{v.name}:{v.persistent_volume_claim.claim_name}")
|
||||
|
||||
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 = {
|
||||
"apiVersion": "batch/v1",
|
||||
"kind": "Job",
|
||||
"metadata": {
|
||||
"name": job_name,
|
||||
"namespace": namespace,
|
||||
"labels": {"app": "krayt"},
|
||||
"annotations": {"pvcs": ",".join(pvc_info) if pvc_info else "none"},
|
||||
},
|
||||
"spec": {
|
||||
"template": {
|
||||
"metadata": {"labels": {"app": "krayt"}},
|
||||
"spec": {
|
||||
"containers": [
|
||||
{
|
||||
"name": "inspector",
|
||||
"image": image,
|
||||
"command": ["sh", "-c", "\n".join(command_parts)],
|
||||
"env": env_vars,
|
||||
"volumeMounts": formatted_mounts,
|
||||
}
|
||||
],
|
||||
"volumes": [format_volume(v) for v in volumes if format_volume(v)],
|
||||
"imagePullSecrets": [{"name": imagepullsecret}] if imagepullsecret else None,
|
||||
"restartPolicy": "Never",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
return inspector_job
|
||||
|
||||
|
||||
PROTECTED_NAMESPACES = {
|
||||
"kube-system",
|
||||
"kube-public",
|
||||
"kube-node-lease",
|
||||
"argo-events",
|
||||
"argo-rollouts",
|
||||
"argo-workflows",
|
||||
"argocd",
|
||||
"cert-manager",
|
||||
"ingress-nginx",
|
||||
"monitoring",
|
||||
"prometheus",
|
||||
"istio-system",
|
||||
"linkerd",
|
||||
}
|
||||
|
||||
|
||||
def load_init_scripts():
|
||||
"""Load and execute initialization scripts from ~/.config/krayt/scripts/"""
|
||||
init_dir = Path.home() / ".config" / "krayt" / "scripts"
|
||||
if not init_dir.exists():
|
||||
return
|
||||
|
||||
# Sort scripts to ensure consistent execution order
|
||||
scripts = sorted(init_dir.glob("*.py"))
|
||||
|
||||
for script in scripts:
|
||||
try:
|
||||
with open(script, "r") as f:
|
||||
exec(f.read(), globals())
|
||||
logging.debug(f"Loaded init script: {script}")
|
||||
except Exception as e:
|
||||
logging.error(f"Failed to load init script {script}: {e}")
|
||||
|
||||
|
||||
def setup_environment():
|
||||
"""Set up the environment with proxy settings and other configurations"""
|
||||
# Load environment variables for proxies
|
||||
proxy_vars = [
|
||||
"HTTP_PROXY",
|
||||
"HTTPS_PROXY",
|
||||
"NO_PROXY",
|
||||
"http_proxy",
|
||||
"https_proxy",
|
||||
"no_proxy",
|
||||
]
|
||||
|
||||
for var in proxy_vars:
|
||||
if var in os.environ:
|
||||
# Make both upper and lower case versions available
|
||||
os.environ[var.upper()] = os.environ[var]
|
||||
os.environ[var.lower()] = os.environ[var]
|
||||
|
||||
|
||||
def version_callback(value: bool):
|
||||
if value:
|
||||
typer.echo(f"Version: {KRAYT_VERSION}")
|
||||
raise typer.Exit()
|
||||
|
||||
|
||||
@app.callback(invoke_without_command=True)
|
||||
def main(
|
||||
ctx: typer.Context,
|
||||
version: bool = typer.Option(
|
||||
False, "--version", "-v", help="Show version", callback=version_callback
|
||||
),
|
||||
):
|
||||
"""
|
||||
Krack open a Krayt dragon!
|
||||
"""
|
||||
if ctx.invoked_subcommand is None:
|
||||
ctx.get_help()
|
||||
|
||||
|
||||
@app.command()
|
||||
def exec(
|
||||
namespace: Optional[str] = typer.Option(
|
||||
None,
|
||||
help="Kubernetes namespace. If not specified, will search for inspectors across all namespaces.",
|
||||
),
|
||||
):
|
||||
"""
|
||||
Enter the Krayt dragon's lair! Connect to a running inspector pod.
|
||||
If multiple inspectors are found, you'll get to choose which one to explore.
|
||||
"""
|
||||
config.load_kube_config()
|
||||
batch_api = client.BatchV1Api()
|
||||
|
||||
try:
|
||||
if namespace:
|
||||
logging.debug(f"Listing jobs in namespace {namespace}")
|
||||
jobs = batch_api.list_namespaced_job(
|
||||
namespace=namespace, label_selector="app=krayt"
|
||||
)
|
||||
else:
|
||||
logging.debug("Listing jobs in all namespaces")
|
||||
jobs = batch_api.list_job_for_all_namespaces(label_selector="app=krayt")
|
||||
|
||||
running_inspectors = []
|
||||
for job in jobs.items:
|
||||
# Get the pod for this job
|
||||
v1 = client.CoreV1Api()
|
||||
pods = v1.list_namespaced_pod(
|
||||
namespace=job.metadata.namespace,
|
||||
label_selector=f"job-name={job.metadata.name}",
|
||||
)
|
||||
for pod in pods.items:
|
||||
if pod.status.phase == "Running":
|
||||
running_inspectors.append(
|
||||
(pod.metadata.name, pod.metadata.namespace)
|
||||
)
|
||||
|
||||
if not running_inspectors:
|
||||
typer.echo("No running inspector pods found.")
|
||||
raise typer.Exit(1)
|
||||
|
||||
if len(running_inspectors) == 1:
|
||||
pod_name, pod_namespace = running_inspectors[0]
|
||||
else:
|
||||
pod_name, pod_namespace = fuzzy_select(running_inspectors)
|
||||
if not pod_name:
|
||||
typer.echo("No inspector selected.")
|
||||
raise typer.Exit(1)
|
||||
|
||||
exec_command = [
|
||||
"kubectl",
|
||||
"exec",
|
||||
"-it",
|
||||
"-n",
|
||||
pod_namespace,
|
||||
pod_name,
|
||||
"--",
|
||||
"/bin/bash",
|
||||
"-l",
|
||||
]
|
||||
|
||||
os.execvp("kubectl", exec_command)
|
||||
|
||||
except client.exceptions.ApiException as e:
|
||||
logging.error(f"Failed to list jobs: {e}")
|
||||
typer.echo(f"Failed to list jobs: {e}", err=True)
|
||||
raise typer.Exit(1)
|
||||
|
||||
|
||||
@app.command()
|
||||
def clean(
|
||||
namespace: Optional[str] = typer.Option(
|
||||
None,
|
||||
help="Kubernetes namespace. If not specified, will cleanup in all namespaces.",
|
||||
),
|
||||
yes: bool = typer.Option(
|
||||
False,
|
||||
"--yes",
|
||||
"-y",
|
||||
help="Skip confirmation prompt.",
|
||||
),
|
||||
):
|
||||
"""
|
||||
Clean up after your hunt! Remove all Krayt inspector jobs.
|
||||
Use --yes/-y to skip confirmation and clean up immediately.
|
||||
"""
|
||||
config.load_kube_config()
|
||||
batch_api = client.BatchV1Api()
|
||||
|
||||
try:
|
||||
if namespace:
|
||||
if namespace in PROTECTED_NAMESPACES:
|
||||
typer.echo(f"Error: Cannot cleanup in protected namespace {namespace}")
|
||||
raise typer.Exit(1)
|
||||
logging.debug(f"Listing jobs in namespace {namespace}")
|
||||
jobs = batch_api.list_namespaced_job(
|
||||
namespace=namespace, label_selector="app=krayt"
|
||||
)
|
||||
else:
|
||||
logging.debug("Listing jobs in all namespaces")
|
||||
jobs = batch_api.list_job_for_all_namespaces(label_selector="app=krayt")
|
||||
|
||||
# Filter out jobs in protected namespaces
|
||||
jobs.items = [
|
||||
job
|
||||
for job in jobs.items
|
||||
if job.metadata.namespace not in PROTECTED_NAMESPACES
|
||||
]
|
||||
|
||||
if not jobs.items:
|
||||
typer.echo("No Krayt inspector jobs found.")
|
||||
return
|
||||
|
||||
# Show confirmation prompt
|
||||
if not yes:
|
||||
job_list = "\n".join(
|
||||
f" {job.metadata.namespace}/{job.metadata.name}" for job in jobs.items
|
||||
)
|
||||
typer.echo(f"The following inspector jobs will be deleted:\n{job_list}")
|
||||
if not typer.confirm("Are you sure you want to continue?"):
|
||||
typer.echo("Operation cancelled.")
|
||||
return
|
||||
|
||||
# Delete each job
|
||||
for job in jobs.items:
|
||||
try:
|
||||
logging.debug(
|
||||
f"Deleting job {job.metadata.namespace}/{job.metadata.name}"
|
||||
)
|
||||
batch_api.delete_namespaced_job(
|
||||
name=job.metadata.name,
|
||||
namespace=job.metadata.namespace,
|
||||
body=client.V1DeleteOptions(propagation_policy="Background"),
|
||||
)
|
||||
typer.echo(f"Deleted job: {job.metadata.namespace}/{job.metadata.name}")
|
||||
except client.exceptions.ApiException as e:
|
||||
logging.error(
|
||||
f"Failed to delete job {job.metadata.namespace}/{job.metadata.name}: {e}"
|
||||
)
|
||||
typer.echo(
|
||||
f"Failed to delete job {job.metadata.namespace}/{job.metadata.name}: {e}",
|
||||
err=True,
|
||||
)
|
||||
|
||||
except client.exceptions.ApiException as e:
|
||||
logging.error(f"Failed to list jobs: {e}")
|
||||
typer.echo(f"Failed to list jobs: {e}", err=True)
|
||||
raise typer.Exit(1)
|
||||
|
||||
|
||||
@app.command()
|
||||
def create(
|
||||
namespace: Optional[str] = typer.Option(
|
||||
None,
|
||||
help="Kubernetes namespace. If not specified, will search for pods across all namespaces.",
|
||||
),
|
||||
image: str = typer.Option(
|
||||
"alpine:latest",
|
||||
"--image",
|
||||
"-i",
|
||||
help="Container image to use for the inspector pod",
|
||||
),
|
||||
imagepullsecret: Optional[str] = typer.Option(
|
||||
None,
|
||||
"--imagepullsecret",
|
||||
help="Name of the image pull secret to use for pulling private images",
|
||||
),
|
||||
):
|
||||
"""
|
||||
Krack open a Krayt dragon! Create an inspector pod to explore what's inside your volumes.
|
||||
If namespace is not specified, will search for pods across all namespaces.
|
||||
The inspector will be created in the same namespace as the selected pod.
|
||||
"""
|
||||
# For create, we want to list all pods, not just Krayt pods
|
||||
pods = get_pods(namespace, label_selector=None)
|
||||
if not pods:
|
||||
typer.echo("No pods found.")
|
||||
raise typer.Exit(1)
|
||||
|
||||
selected_pod, selected_namespace = fuzzy_select(pods)
|
||||
if not selected_pod:
|
||||
typer.echo("No pod selected.")
|
||||
raise typer.Exit(1)
|
||||
|
||||
pod_spec = get_pod_spec(selected_pod, selected_namespace)
|
||||
volume_mounts, volumes = get_pod_volumes_and_mounts(pod_spec)
|
||||
|
||||
inspector_job = create_inspector_job(
|
||||
client.CoreV1Api(),
|
||||
selected_namespace,
|
||||
selected_pod,
|
||||
volume_mounts,
|
||||
volumes,
|
||||
image=image,
|
||||
imagepullsecret=imagepullsecret,
|
||||
)
|
||||
|
||||
# Output the job manifest
|
||||
typer.echo(yaml.dump(clean_dict(inspector_job), sort_keys=False))
|
||||
|
||||
|
||||
@app.command()
|
||||
def version():
|
||||
"""Show the version of Krayt."""
|
||||
typer.echo(f"Version: {KRAYT_VERSION}")
|
||||
|
||||
|
||||
@app.command()
|
||||
def logs(
|
||||
namespace: Optional[str] = typer.Option(
|
||||
None,
|
||||
help="Kubernetes namespace. If not specified, will search for inspectors across all namespaces.",
|
||||
),
|
||||
follow: bool = typer.Option(
|
||||
False,
|
||||
"--follow",
|
||||
"-f",
|
||||
help="Follow the logs in real-time",
|
||||
),
|
||||
):
|
||||
"""
|
||||
View logs from a Krayt inspector pod.
|
||||
If multiple inspectors are found, you'll get to choose which one to explore.
|
||||
"""
|
||||
pods = get_pods(namespace)
|
||||
if not pods:
|
||||
typer.echo("No pods found.")
|
||||
raise typer.Exit(1)
|
||||
|
||||
selected_pod, selected_namespace = fuzzy_select(pods)
|
||||
if not selected_pod:
|
||||
typer.echo("No pod selected.")
|
||||
raise typer.Exit(1)
|
||||
|
||||
try:
|
||||
config.load_kube_config()
|
||||
api = client.CoreV1Api()
|
||||
logs = api.read_namespaced_pod_log(
|
||||
name=selected_pod,
|
||||
namespace=selected_namespace,
|
||||
follow=follow,
|
||||
_preload_content=False,
|
||||
)
|
||||
|
||||
if follow:
|
||||
for line in logs:
|
||||
typer.echo(line.decode("utf-8").strip())
|
||||
else:
|
||||
typer.echo(logs.read().decode("utf-8"))
|
||||
|
||||
except client.rest.ApiException as e:
|
||||
typer.echo(f"Error reading logs: {e}")
|
||||
raise typer.Exit(1)
|
||||
|
||||
|
||||
def main():
|
||||
setup_environment()
|
||||
load_init_scripts()
|
||||
app()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
337
krayt2.py
Executable file
337
krayt2.py
Executable 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()
|
||||
69
pyproject.toml
Normal file
69
pyproject.toml
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
[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",
|
||||
]
|
||||
|
||||
[[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']
|
||||
|
|
@ -13,7 +13,7 @@ function fail {
|
|||
echo "Error: $msg" 1>&2
|
||||
exit 1
|
||||
}
|
||||
function check_deps {
|
||||
function check_uv {
|
||||
if ! command -v uv &>/dev/null; then
|
||||
echo " Error: uv is not installed"
|
||||
echo "krayt requires uv to run. You can install it with:"
|
||||
|
|
@ -24,15 +24,67 @@ function check_deps {
|
|||
fail "uv not found"
|
||||
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 {
|
||||
#settings
|
||||
USER="waylonwalker"
|
||||
PROG="krayt"
|
||||
ASPROG="krayt"
|
||||
MOVE="true"
|
||||
MOVE="false"
|
||||
RELEASE="{{VERSION}}"
|
||||
INSECURE="false"
|
||||
OUT_DIR="/usr/local/bin"
|
||||
OUT_DIR="$(pwd)"
|
||||
GH="https://github.com"
|
||||
#bash check
|
||||
[ ! "$BASH_VERSION" ] && fail "Please use bash instead"
|
||||
|
|
@ -57,70 +109,126 @@ function install {
|
|||
else
|
||||
fail "neither wget/curl are installed"
|
||||
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
|
||||
Darwin) OS="darwin" ;;
|
||||
Linux) OS="linux" ;;
|
||||
*) fail "unknown os: $(uname -s)" ;;
|
||||
esac
|
||||
#find ARCH
|
||||
if uname -m | grep -E '(arm|aarch)64' >/dev/null; then
|
||||
ARCH="aarch64"
|
||||
if uname -m | grep -E '(arm|arch)64' >/dev/null; then
|
||||
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
|
||||
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
|
||||
fail "unknown arch: $(uname -m)"
|
||||
fi
|
||||
#choose from asset list
|
||||
URL=""
|
||||
FTYPE=""
|
||||
VERSION=${RELEASE#v}
|
||||
if [[ $VERSION == "" ]]; then
|
||||
VERSION=$(curl -s https://api.github.com/repos/$USER/$PROG/releases/latest | grep -o '"tag_name": "[^"]*' | cut -d'"' -f4)
|
||||
case "${OS}_${ARCH}" in
|
||||
"linux_amd64")
|
||||
URL="https://github.com/WaylonWalker/nvim-manager/releases/download/v${RELEASE}/nvim-manager-${RELEASE}-x86_64-unknown-linux-gnu.tar.gz"
|
||||
FTYPE=".tar.gz"
|
||||
;;
|
||||
"linux_arm64")
|
||||
URL="https://github.com/WaylonWalker/nvim-manager/releases/download/v${RELEASE}/nvim-manager-${RELEASE}-aarch64-unknown-linux-gnu.tar.gz"
|
||||
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
|
||||
if [[ $VERSION == "" ]]; then
|
||||
fail "cannot find latest version"
|
||||
if [ ! -z "$ASPROG" ]; then
|
||||
echo -n " as $ASPROG"
|
||||
fi
|
||||
VERSION=${VERSION#v}
|
||||
ASSET_URL="$GH/$USER/$PROG/releases/download/v$VERSION/${PROG}-${VERSION}-${ARCH}-unknown-${OS}-gnu.tar.gz"
|
||||
echo "Installing $PROG v$VERSION..."
|
||||
echo "Downloading binary from $ASSET_URL"
|
||||
echo -n " (${OS}/${ARCH})"
|
||||
|
||||
echo "....."
|
||||
|
||||
#enter tempdir
|
||||
mkdir -p $TMP_DIR
|
||||
cd $TMP_DIR
|
||||
#download and unpack
|
||||
if [[ $ASSET_URL =~ \.gz$ ]]; then
|
||||
which tar >/dev/null || fail "tar not installed"
|
||||
if [[ $GET =~ ^curl ]]; then
|
||||
curl -s ${ASSET_URL} | tar zx || fail "download failed"
|
||||
else
|
||||
wget -qO- ${ASSET_URL} | tar zx || fail "download failed"
|
||||
fi
|
||||
if [[ $FTYPE = ".gz" ]]; then
|
||||
which gzip >/dev/null || fail "gzip is not installed"
|
||||
bash -c "$GET $URL" | gzip -d - >$PROG || fail "download failed"
|
||||
elif [[ $FTYPE = ".bz2" ]]; then
|
||||
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" >"nvim-manager_${OS}_${ARCH}" || fail "download failed"
|
||||
else
|
||||
fail "unknown file type: $ASSET_URL"
|
||||
fail "unknown file type: $FTYPE"
|
||||
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
|
||||
#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
|
||||
fail "cannot find binary"
|
||||
fail "mv failed ($OUT)"
|
||||
fi
|
||||
fi
|
||||
echo "Installation complete!"
|
||||
echo "Downloaded to $DEST"
|
||||
#done
|
||||
cleanup
|
||||
check_uv
|
||||
}
|
||||
check_deps
|
||||
install
|
||||
setup_config_dir
|
||||
|
|
|
|||
247
test.sh
Normal file
247
test.sh
Normal 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
206
test.yaml
Normal 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
|
||||
2
version
2
version
|
|
@ -1 +1 @@
|
|||
0.0.0
|
||||
0.2.0
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue