Compare commits
50 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0f9e70267e | ||
|
|
f8c44999fe | ||
|
|
45e5dd74aa | ||
|
|
eb54e314ff | ||
|
|
d6056cead2 | ||
|
|
be6ee91098 | ||
|
|
1fee6e2c97 | ||
|
|
2fbc15ae84 | ||
|
|
b64e635b71 | ||
|
|
7511cace42 | ||
|
|
1138f695f4 | ||
|
|
0a6e19731d | ||
|
|
64077d3ba2 | ||
|
|
681194fc1f | ||
|
|
ed449034f7 | ||
|
|
17c088526b | ||
|
|
225edce32d | ||
|
|
db96853646 | ||
|
|
3ce69baf26 | ||
|
|
7daa9a3874 | ||
|
|
cc425cf812 | ||
|
|
ae918bf5f2 | ||
|
|
2f16036c8e | ||
|
|
82102c4adf | ||
|
|
1959e1a39b | ||
|
|
0025cea947 | ||
|
|
2f049f1f99 | ||
|
|
10f7f61cec | ||
|
|
b3500c9170 | ||
|
|
09f68faded | ||
|
|
0bd055f881 | ||
|
|
2e4eb8ee5f | ||
|
|
caa07ac771 | ||
|
|
575c70ee9f | ||
|
|
a9669afff8 | ||
|
|
96788a412b | ||
|
|
9b6efdc7fe | ||
|
|
abb7dacc86 | ||
|
|
74fba4f23e | ||
|
|
301b628d39 | ||
|
|
61fbec4e31 | ||
|
|
a6ecb8240c | ||
|
|
e8686a2c66 | ||
|
|
47e7f1cb5e | ||
|
|
2899ee23eb | ||
|
|
0d913f7656 | ||
|
|
378744632f | ||
|
|
9737746923 | ||
|
|
a60562b7fc | ||
|
|
86985a62a9 |
27 changed files with 2652 additions and 37 deletions
47
.github/workflows/release-pypi.yaml
vendored
Normal file
47
.github/workflows/release-pypi.yaml
vendored
Normal file
|
|
@ -0,0 +1,47 @@
|
||||||
|
name: Release Krayt
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
push:
|
||||||
|
paths:
|
||||||
|
- "krayt/**"
|
||||||
|
- "pyproject.toml"
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
pull-requests: write
|
||||||
|
issues: read
|
||||||
|
packages: none
|
||||||
|
id-token: write
|
||||||
|
jobs:
|
||||||
|
pypi-release-krayt:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: waylonwalker/hatch-action@v4
|
||||||
|
with:
|
||||||
|
before-command: "lint-format"
|
||||||
|
env:
|
||||||
|
# required for gh release
|
||||||
|
GH_TOKEN: ${{ github.token }}
|
||||||
|
- run: sudo rm -rf dist
|
||||||
|
- name: Install just
|
||||||
|
run: |
|
||||||
|
curl --proto '=https' --tlsv1.2 -sSf https://just.systems/install.sh | bash -s -- --to /usr/local/bin
|
||||||
|
shell: bash
|
||||||
|
- name: Install uv
|
||||||
|
run: |
|
||||||
|
curl -LsSf https://astral.sh/uv/0.6.16/install.sh | sh
|
||||||
|
shell: bash
|
||||||
|
- name: Install hatch
|
||||||
|
run: |
|
||||||
|
uv tool install hatch
|
||||||
|
shell: bash
|
||||||
|
- name: Configure Git
|
||||||
|
run: |
|
||||||
|
git config --global user.name "github-actions[bot]"
|
||||||
|
git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||||
|
shell: bash
|
||||||
|
- name: GitHub Release (just release)
|
||||||
|
run: just create-release
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ github.token }}
|
||||||
|
shell: bash
|
||||||
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -962,4 +962,4 @@ FodyWeavers.xsd
|
||||||
# Additional files built by Visual Studio
|
# Additional files built by Visual Studio
|
||||||
|
|
||||||
# End of https://www.toptal.com/developers/gitignore/api/vim,node,data,emacs,python,pycharm,executable,sublimetext,visualstudio,visualstudiocode
|
# End of https://www.toptal.com/developers/gitignore/api/vim,node,data,emacs,python,pycharm,executable,sublimetext,visualstudio,visualstudiocode
|
||||||
krayt
|
*.null-ls*
|
||||||
|
|
|
||||||
26
CHANGELOG.md
26
CHANGELOG.md
|
|
@ -1,3 +1,29 @@
|
||||||
|
## 0.4.3
|
||||||
|
|
||||||
|
- working out binary release process
|
||||||
|
|
||||||
|
## 0.4.2
|
||||||
|
|
||||||
|
- working out binary release process
|
||||||
|
|
||||||
|
## 0.4.1
|
||||||
|
|
||||||
|
- Automated release for both pypi and github
|
||||||
|
|
||||||
|
## 0.4.0
|
||||||
|
|
||||||
|
- create now has --apply to apply the generated manifest to the cluster
|
||||||
|
- generic templates endpoint for cli
|
||||||
|
- better motd for volume mounts
|
||||||
|
|
||||||
|
## 0.3.0
|
||||||
|
|
||||||
|
- created pypi release
|
||||||
|
- updated releases to use pyapp
|
||||||
|
- all new package
|
||||||
|
- port forward support
|
||||||
|
- additional_packages support
|
||||||
|
|
||||||
## 0.2.0
|
## 0.2.0
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
|
||||||
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.
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
# Krayt - The Kubernetes Volume Inspector
|
# Krayt - The Kubernetes Volume Inspector
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
Like cracking open a Krayt dragon pearl, this tool helps you inspect what's inside your Kubernetes volumes.
|
Like cracking open a Krayt dragon pearl, this tool helps you inspect what's inside your Kubernetes volumes.
|
||||||
Hunt down storage issues and explore your persistent data like a true Tatooine dragon hunter.
|
Hunt down storage issues and explore your persistent data like a true Tatooine dragon hunter.
|
||||||
|
|
||||||
|
|
|
||||||
60
justfile
60
justfile
|
|
@ -3,7 +3,7 @@ delete-tag:
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
# Get the version
|
# Get the version
|
||||||
VERSION=$(cat version)
|
VERSION=$(hatch version)
|
||||||
|
|
||||||
# Delete the tag
|
# Delete the tag
|
||||||
git tag -d "v$VERSION"
|
git tag -d "v$VERSION"
|
||||||
|
|
@ -14,59 +14,65 @@ delete-release:
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
# Get the version
|
# Get the version
|
||||||
VERSION=$(cat version)
|
VERSION=$(hatch version)
|
||||||
|
|
||||||
# Delete the release
|
# Delete the release
|
||||||
gh release delete "v$VERSION"
|
gh release delete "v$VERSION"
|
||||||
|
|
||||||
create-tag:
|
create-tag:
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
VERSION=$(cat version)
|
VERSION=$(hatch version)
|
||||||
git tag -a "v$VERSION" -m "Release v$VERSION"
|
git tag -a "v$VERSION" -m "Release v$VERSION"
|
||||||
git push origin "v$VERSION"
|
git push origin "v$VERSION"
|
||||||
|
|
||||||
create-archives:
|
create-archives:
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
VERSION=$(cat version)
|
VERSION=$(hatch version)
|
||||||
rm -rf dist build
|
rm -rf dist build
|
||||||
mkdir -p dist
|
hatch build -t binary
|
||||||
|
|
||||||
|
krayt_bin=dist/binary/krayt-${VERSION}
|
||||||
|
|
||||||
# Create the binary for each platform
|
# Create the binary for each platform
|
||||||
for platform in "x86_64-unknown-linux-gnu" "aarch64-unknown-linux-gnu"; do
|
for platform in "x86_64-unknown-linux-gnu" "aarch64-unknown-linux-gnu"; do
|
||||||
outdir="krayt-${VERSION}-${platform}"
|
outbin="krayt-${VERSION}-${platform}"
|
||||||
mkdir -p "dist/${outdir}"
|
|
||||||
|
|
||||||
# Copy the Python script and update version
|
# Copy the Python script and update version
|
||||||
cp krayt.py "dist/${outdir}/krayt.py"
|
cp ${krayt_bin} "dist/binary/${outbin}"
|
||||||
sed -i "s/NIGHTLY/${VERSION}/" "dist/${outdir}/krayt.py"
|
|
||||||
|
|
||||||
cd dist
|
|
||||||
tar czf "${outdir}.tar.gz" "${outdir}"
|
|
||||||
sha256sum "${outdir}.tar.gz" > "${outdir}.tar.gz.sha256"
|
|
||||||
cd ..
|
|
||||||
done
|
done
|
||||||
|
|
||||||
# Generate install.sh
|
# Generate install.sh
|
||||||
./scripts/generate_install_script.py "$VERSION"
|
# ./scripts/generate_install_script.py "$VERSION"
|
||||||
chmod +x dist/install.sh
|
# chmod +x dist/install.sh
|
||||||
|
|
||||||
create-release: create-tag create-archives
|
create-release: create-tag create-archives
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
VERSION=$(cat version)
|
VERSION=$(hatch version)
|
||||||
./scripts/get_release_notes.py "$VERSION" > release_notes.tmp
|
./scripts/get_release_notes.py "$VERSION" > release_notes.tmp
|
||||||
gh release create "v$VERSION" \
|
|
||||||
--title "v$VERSION" \
|
# Check if release already exists
|
||||||
--notes-file release_notes.tmp \
|
if gh release view "v$VERSION" &>/dev/null; then
|
||||||
dist/krayt-${VERSION}-x86_64-unknown-linux-gnu.tar.gz \
|
echo "Release v$VERSION already exists. Uploading binaries..."
|
||||||
dist/krayt-${VERSION}-x86_64-unknown-linux-gnu.tar.gz.sha256 \
|
# Upload binaries to existing release
|
||||||
dist/krayt-${VERSION}-aarch64-unknown-linux-gnu.tar.gz \
|
gh release upload "v$VERSION" \
|
||||||
dist/krayt-${VERSION}-aarch64-unknown-linux-gnu.tar.gz.sha256 \
|
dist/binary/krayt-${VERSION} \
|
||||||
dist/install.sh
|
dist/binary/krayt-${VERSION}-aarch64-unknown-linux-gnu \
|
||||||
|
dist/binary/krayt-${VERSION}-x86_64-unknown-linux-gnu || true
|
||||||
|
else
|
||||||
|
echo "Creating new release v$VERSION"
|
||||||
|
# Create new release with binaries
|
||||||
|
gh release create "v$VERSION" \
|
||||||
|
--title "v$VERSION" \
|
||||||
|
--notes-file release_notes.tmp \
|
||||||
|
dist/binary/krayt-${VERSION} \
|
||||||
|
dist/binary/krayt-${VERSION}-aarch64-unknown-linux-gnu \
|
||||||
|
dist/binary/krayt-${VERSION}-x86_64-unknown-linux-gnu
|
||||||
|
fi
|
||||||
rm release_notes.tmp
|
rm release_notes.tmp
|
||||||
|
|
||||||
preview-release-notes:
|
preview-release-notes:
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
VERSION=$(cat version)
|
VERSION=$(hatch version)
|
||||||
./scripts/get_release_notes.py "$VERSION" | less -R
|
./scripts/get_release_notes.py "$VERSION" | less -R
|
||||||
|
|
||||||
release: create-release
|
release: create-release
|
||||||
|
|
||||||
|
|
|
||||||
1
krayt/__about__.py
Normal file
1
krayt/__about__.py
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
__version__ = "0.4.3"
|
||||||
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="template", no_args_is_help=True)
|
||||||
|
app.add_typer(pod_app, name="pod", no_args_is_help=True)
|
||||||
|
app.command(name="create")(create)
|
||||||
|
app.command(name="c")(create)
|
||||||
|
app.command(name="clean")(clean)
|
||||||
|
app.command(name="exec")(exec)
|
||||||
|
app.command(name="logs")(logs)
|
||||||
|
app.add_typer(bundles_app, name="bundles", no_args_is_help=True)
|
||||||
|
|
||||||
|
|
||||||
|
@app.command()
|
||||||
|
def version():
|
||||||
|
print(__version__)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
app()
|
||||||
25
krayt/cli/bundles.py
Normal file
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}")
|
||||||
925
krayt/cli/pod.py
Normal file
925
krayt/cli/pod.py
Normal file
|
|
@ -0,0 +1,925 @@
|
||||||
|
import iterfzf
|
||||||
|
from krayt.templates import env
|
||||||
|
from kubernetes.stream import stream
|
||||||
|
from kubernetes import client, config
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import time
|
||||||
|
import typer
|
||||||
|
from typing import Any, List, Optional
|
||||||
|
import yaml
|
||||||
|
from krayt.__about__ import __version__
|
||||||
|
import sys
|
||||||
|
import tty
|
||||||
|
import termios
|
||||||
|
import select
|
||||||
|
import signal
|
||||||
|
import json
|
||||||
|
|
||||||
|
|
||||||
|
logging.basicConfig(level=logging.WARNING)
|
||||||
|
|
||||||
|
app = typer.Typer()
|
||||||
|
|
||||||
|
|
||||||
|
def clean_dict(d: dict[str, Any]) -> dict[str, Any]:
|
||||||
|
"""Remove None values and empty dicts from a dictionary recursively."""
|
||||||
|
if not isinstance(d, dict):
|
||||||
|
return d
|
||||||
|
return {
|
||||||
|
k: clean_dict(v)
|
||||||
|
for k, v in d.items()
|
||||||
|
if v is not None and v != {} and not (isinstance(v, dict) and not clean_dict(v))
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def format_volume_mount(vm: client.V1VolumeMount) -> dict[str, Any]:
|
||||||
|
"""Format volume mount with only relevant fields."""
|
||||||
|
# Skip Kubernetes service account mounts
|
||||||
|
if vm.mount_path.startswith("/var/run/secrets/kubernetes.io/"):
|
||||||
|
return None
|
||||||
|
|
||||||
|
return clean_dict(
|
||||||
|
{
|
||||||
|
"name": vm.name,
|
||||||
|
"mount_path": vm.mount_path,
|
||||||
|
"read_only": vm.read_only if vm.read_only else None,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def format_volume(v: client.V1Volume) -> dict[str, Any]:
|
||||||
|
"""Format volume into a dictionary, return None if it should be skipped"""
|
||||||
|
# Skip Kubernetes service account volumes
|
||||||
|
if v.name.startswith("kube-api-access-"):
|
||||||
|
return None
|
||||||
|
|
||||||
|
volume_source = None
|
||||||
|
if v.persistent_volume_claim:
|
||||||
|
volume_source = {
|
||||||
|
"persistentVolumeClaim": {"claimName": v.persistent_volume_claim.claim_name}
|
||||||
|
}
|
||||||
|
elif v.config_map:
|
||||||
|
volume_source = {"configMap": {"name": v.config_map.name}}
|
||||||
|
elif v.secret:
|
||||||
|
volume_source = {"secret": {"secretName": v.secret.secret_name}}
|
||||||
|
elif v.host_path: # Add support for hostPath volumes (used for device mounts)
|
||||||
|
volume_source = {
|
||||||
|
"hostPath": {
|
||||||
|
"path": v.host_path.path,
|
||||||
|
"type": v.host_path.type if v.host_path.type else None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
elif v.empty_dir: # Add support for emptyDir volumes (used for /dev/shm)
|
||||||
|
volume_source = {
|
||||||
|
"emptyDir": {
|
||||||
|
"medium": v.empty_dir.medium if v.empty_dir.medium else None,
|
||||||
|
"sizeLimit": v.empty_dir.size_limit if v.empty_dir.size_limit else None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if not volume_source:
|
||||||
|
return None
|
||||||
|
|
||||||
|
return clean_dict({"name": v.name, **volume_source})
|
||||||
|
|
||||||
|
|
||||||
|
def fuzzy_select(items):
|
||||||
|
"""Use fzf to select from a list of (name, namespace) tuples"""
|
||||||
|
if not items:
|
||||||
|
return None, None
|
||||||
|
|
||||||
|
# If there's only one item, return it without prompting
|
||||||
|
if len(items) == 1:
|
||||||
|
return items[0]
|
||||||
|
|
||||||
|
# Format items for display
|
||||||
|
formatted_items = [f"{name} ({namespace})" for name, namespace in items]
|
||||||
|
|
||||||
|
# Use fzf for selection
|
||||||
|
try:
|
||||||
|
# selected = inquirer.fuzzy(
|
||||||
|
# message="Select a pod to clone:", choices=formatted_items
|
||||||
|
# ).execute()
|
||||||
|
|
||||||
|
selected = iterfzf.iterfzf(
|
||||||
|
formatted_items,
|
||||||
|
prompt="Select a pod to clone:",
|
||||||
|
# preview='''kubectl describe pod "$(echo {} | awk -F'[(|)]' '{gsub(/\x1b\[[0-9;]*m/, "", $1); print $1}' | xargs)" -n "$(echo {} | awk -F'[(|)]' '{gsub(/\x1b\[[0-9;]*m/, "", $2); print $2}' | xargs)"''',
|
||||||
|
)
|
||||||
|
if not selected:
|
||||||
|
return None, None
|
||||||
|
|
||||||
|
# Parse selection back into name and namespace
|
||||||
|
# Example: "pod-name (namespace)" -> ("pod-name", "namespace")
|
||||||
|
name = selected.split(" (")[0]
|
||||||
|
namespace = selected.split(" (")[1][:-1]
|
||||||
|
return name, namespace
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
typer.echo(f"Error during selection: {e}")
|
||||||
|
return None, None
|
||||||
|
|
||||||
|
|
||||||
|
def get_pods(
|
||||||
|
namespace=None,
|
||||||
|
label_selector: str = "app=krayt",
|
||||||
|
):
|
||||||
|
"""Get list of pods in the specified namespace or all namespaces"""
|
||||||
|
try:
|
||||||
|
config.load_kube_config()
|
||||||
|
api = client.CoreV1Api()
|
||||||
|
if namespace:
|
||||||
|
pods = api.list_namespaced_pod(
|
||||||
|
namespace=namespace,
|
||||||
|
label_selector=label_selector,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
pods = api.list_pod_for_all_namespaces(
|
||||||
|
label_selector=label_selector,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Convert to list of (name, namespace) tuples
|
||||||
|
pod_list = []
|
||||||
|
for pod in pods.items:
|
||||||
|
if pod.metadata.namespace not in PROTECTED_NAMESPACES:
|
||||||
|
pod_list.append((pod.metadata.name, pod.metadata.namespace))
|
||||||
|
return pod_list
|
||||||
|
|
||||||
|
except client.rest.ApiException as e:
|
||||||
|
typer.echo(f"Error listing pods: {e}")
|
||||||
|
raise typer.Exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
def get_namespaces(
|
||||||
|
namespace=None,
|
||||||
|
label_selector: str = "app=krayt",
|
||||||
|
):
|
||||||
|
config.load_kube_config()
|
||||||
|
api = client.CoreV1Api()
|
||||||
|
|
||||||
|
all_namespaces = [n.metadata.name for n in api.list_namespace().items]
|
||||||
|
return all_namespaces
|
||||||
|
|
||||||
|
|
||||||
|
def get_pod_spec(pod_name, namespace):
|
||||||
|
config.load_kube_config()
|
||||||
|
v1 = client.CoreV1Api()
|
||||||
|
return v1.read_namespaced_pod(pod_name, namespace)
|
||||||
|
|
||||||
|
|
||||||
|
def get_pod_volumes_and_mounts(pod_spec):
|
||||||
|
"""Extract all volumes and mounts from a pod spec"""
|
||||||
|
volume_mounts = []
|
||||||
|
for container in pod_spec.spec.containers:
|
||||||
|
if container.volume_mounts:
|
||||||
|
volume_mounts.extend(container.volume_mounts)
|
||||||
|
|
||||||
|
# Filter out None values from volume mounts
|
||||||
|
volume_mounts = [vm for vm in volume_mounts if format_volume_mount(vm)]
|
||||||
|
|
||||||
|
# Get all volumes, including device mounts
|
||||||
|
volumes = []
|
||||||
|
if pod_spec.spec.volumes:
|
||||||
|
for v in pod_spec.spec.volumes:
|
||||||
|
# Handle device mounts
|
||||||
|
if v.name in ["cache-volume"]:
|
||||||
|
volumes.append(
|
||||||
|
client.V1Volume(
|
||||||
|
name=v.name,
|
||||||
|
empty_dir=client.V1EmptyDirVolumeSource(medium="Memory"),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
elif v.name in ["coral-device"]:
|
||||||
|
volumes.append(
|
||||||
|
client.V1Volume(
|
||||||
|
name=v.name,
|
||||||
|
host_path=client.V1HostPathVolumeSource(
|
||||||
|
path="/dev/apex_0", type="CharDevice"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
elif v.name in ["qsv-device"]:
|
||||||
|
volumes.append(
|
||||||
|
client.V1Volume(
|
||||||
|
name=v.name,
|
||||||
|
host_path=client.V1HostPathVolumeSource(
|
||||||
|
path="/dev/dri", type="Directory"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
volumes.append(v)
|
||||||
|
|
||||||
|
# Filter out None values from volumes
|
||||||
|
volumes = [v for v in volumes if format_volume(v)]
|
||||||
|
|
||||||
|
return volume_mounts, volumes
|
||||||
|
|
||||||
|
|
||||||
|
def get_env_vars_and_secret_volumes(api, namespace: str):
|
||||||
|
"""Get environment variables and secret volumes for the inspector pod"""
|
||||||
|
env_vars = []
|
||||||
|
volumes = []
|
||||||
|
|
||||||
|
# Add proxy environment variables if they exist in the host environment
|
||||||
|
proxy_vars = [
|
||||||
|
"HTTP_PROXY",
|
||||||
|
"HTTPS_PROXY",
|
||||||
|
"NO_PROXY",
|
||||||
|
"http_proxy",
|
||||||
|
"https_proxy",
|
||||||
|
"no_proxy",
|
||||||
|
]
|
||||||
|
|
||||||
|
for var in proxy_vars:
|
||||||
|
if var in os.environ:
|
||||||
|
env_vars.append({"name": var, "value": os.environ[var]})
|
||||||
|
|
||||||
|
# Look for secret volumes in the namespace
|
||||||
|
try:
|
||||||
|
secrets = api.list_namespaced_secret(namespace)
|
||||||
|
for secret in secrets.items:
|
||||||
|
# Skip service account tokens and other system secrets
|
||||||
|
if secret.type != "Opaque" or secret.metadata.name.startswith(
|
||||||
|
"default-token-"
|
||||||
|
):
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Mount each secret as a volume
|
||||||
|
volume_name = f"secret-{secret.metadata.name}"
|
||||||
|
volumes.append(
|
||||||
|
client.V1Volume(
|
||||||
|
name=volume_name,
|
||||||
|
secret=client.V1SecretVolumeSource(
|
||||||
|
secret_name=secret.metadata.name
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
except client.exceptions.ApiException as e:
|
||||||
|
if e.status != 404: # Ignore if no secrets found
|
||||||
|
logging.warning(f"Failed to list secrets in namespace {namespace}: {e}")
|
||||||
|
|
||||||
|
return env_vars, volumes
|
||||||
|
|
||||||
|
|
||||||
|
def create_inspector_job(
|
||||||
|
api,
|
||||||
|
namespace: str,
|
||||||
|
pod_name: str,
|
||||||
|
volume_mounts: list,
|
||||||
|
volumes: list,
|
||||||
|
image: str = "alpine:latest",
|
||||||
|
imagepullsecret: Optional[str] = None,
|
||||||
|
additional_packages: Optional[List[str]] = None,
|
||||||
|
pre_init_scripts: Optional[List[str]] = None,
|
||||||
|
post_init_scripts: Optional[List[str]] = None,
|
||||||
|
pre_init_hooks: Optional[List[str]] = None,
|
||||||
|
post_init_hooks: Optional[List[str]] = None,
|
||||||
|
):
|
||||||
|
timestamp = int(time.time())
|
||||||
|
job_name = f"{pod_name}-krayt-{timestamp}"
|
||||||
|
|
||||||
|
env_vars, secret_volumes = get_env_vars_and_secret_volumes(api, namespace)
|
||||||
|
volumes.extend(secret_volumes)
|
||||||
|
|
||||||
|
secret_mounts = [
|
||||||
|
client.V1VolumeMount(
|
||||||
|
name=vol.name,
|
||||||
|
mount_path=f"/mnt/secrets/{vol.secret.secret_name}",
|
||||||
|
read_only=True,
|
||||||
|
)
|
||||||
|
for vol in secret_volumes
|
||||||
|
]
|
||||||
|
|
||||||
|
formatted_mounts = [format_volume_mount(vm) for vm in volume_mounts]
|
||||||
|
formatted_mounts = [client.V1VolumeMount(**vm) for vm in formatted_mounts if vm]
|
||||||
|
formatted_mounts.extend(secret_mounts)
|
||||||
|
|
||||||
|
pvc_info = [
|
||||||
|
f"{v.name}:{v.persistent_volume_claim.claim_name}"
|
||||||
|
for v in volumes
|
||||||
|
if hasattr(v, "persistent_volume_claim") and v.persistent_volume_claim
|
||||||
|
]
|
||||||
|
|
||||||
|
template = env.get_template("base.sh")
|
||||||
|
command = template.render(
|
||||||
|
volumes=volumes,
|
||||||
|
pvcs=None,
|
||||||
|
additional_packages=additional_packages,
|
||||||
|
pre_init_scripts=None,
|
||||||
|
post_init_scripts=None,
|
||||||
|
pre_init_hooks=None,
|
||||||
|
post_init_hooks=None,
|
||||||
|
)
|
||||||
|
|
||||||
|
container = client.V1Container(
|
||||||
|
name="inspector",
|
||||||
|
image=image,
|
||||||
|
command=["sh", "-c", command],
|
||||||
|
env=env_vars,
|
||||||
|
volume_mounts=formatted_mounts,
|
||||||
|
)
|
||||||
|
|
||||||
|
spec = client.V1PodSpec(
|
||||||
|
containers=[container],
|
||||||
|
volumes=[format_volume(v) for v in volumes if format_volume(v)],
|
||||||
|
restart_policy="Never",
|
||||||
|
image_pull_secrets=[client.V1LocalObjectReference(name=imagepullsecret)]
|
||||||
|
if imagepullsecret
|
||||||
|
else None,
|
||||||
|
)
|
||||||
|
|
||||||
|
template = client.V1PodTemplateSpec(
|
||||||
|
metadata=client.V1ObjectMeta(labels={"app": "krayt"}), spec=spec
|
||||||
|
)
|
||||||
|
|
||||||
|
job_spec = client.V1JobSpec(
|
||||||
|
template=template,
|
||||||
|
ttl_seconds_after_finished=600,
|
||||||
|
)
|
||||||
|
|
||||||
|
job = client.V1Job(
|
||||||
|
api_version="batch/v1",
|
||||||
|
kind="Job",
|
||||||
|
metadata=client.V1ObjectMeta(
|
||||||
|
name=job_name,
|
||||||
|
namespace=namespace,
|
||||||
|
labels={"app": "krayt"},
|
||||||
|
annotations={"pvcs": ",".join(pvc_info) if pvc_info else "none"},
|
||||||
|
),
|
||||||
|
spec=job_spec,
|
||||||
|
)
|
||||||
|
|
||||||
|
return job
|
||||||
|
|
||||||
|
|
||||||
|
PROTECTED_NAMESPACES = {
|
||||||
|
"kube-system",
|
||||||
|
"kube-public",
|
||||||
|
"kube-node-lease",
|
||||||
|
"argo-events",
|
||||||
|
"argo-rollouts",
|
||||||
|
"argo-workflows",
|
||||||
|
"argocd",
|
||||||
|
"cert-manager",
|
||||||
|
"ingress-nginx",
|
||||||
|
"monitoring",
|
||||||
|
"prometheus",
|
||||||
|
"istio-system",
|
||||||
|
"linkerd",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def setup_environment():
|
||||||
|
"""Set up the environment with proxy settings and other configurations"""
|
||||||
|
# Load environment variables for proxies
|
||||||
|
proxy_vars = [
|
||||||
|
"HTTP_PROXY",
|
||||||
|
"HTTPS_PROXY",
|
||||||
|
"NO_PROXY",
|
||||||
|
"http_proxy",
|
||||||
|
"https_proxy",
|
||||||
|
"no_proxy",
|
||||||
|
]
|
||||||
|
|
||||||
|
for var in proxy_vars:
|
||||||
|
if var in os.environ:
|
||||||
|
# Make both upper and lower case versions available
|
||||||
|
os.environ[var.upper()] = os.environ[var]
|
||||||
|
os.environ[var.lower()] = os.environ[var]
|
||||||
|
|
||||||
|
|
||||||
|
def version_callback(value: bool):
|
||||||
|
if value:
|
||||||
|
typer.echo(f"Version: {__version__}")
|
||||||
|
raise typer.Exit()
|
||||||
|
|
||||||
|
|
||||||
|
def get_pod(namespace: Optional[str] = None):
|
||||||
|
config.load_kube_config()
|
||||||
|
batch_api = client.BatchV1Api()
|
||||||
|
|
||||||
|
try:
|
||||||
|
if namespace:
|
||||||
|
logging.debug(f"Listing jobs in namespace {namespace}")
|
||||||
|
jobs = batch_api.list_namespaced_job(
|
||||||
|
namespace=namespace, label_selector="app=krayt"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
logging.debug("Listing jobs in all namespaces")
|
||||||
|
jobs = batch_api.list_job_for_all_namespaces(label_selector="app=krayt")
|
||||||
|
|
||||||
|
running_inspectors = []
|
||||||
|
for job in jobs.items:
|
||||||
|
# Get the pod for this job
|
||||||
|
v1 = client.CoreV1Api()
|
||||||
|
pods = v1.list_namespaced_pod(
|
||||||
|
namespace=job.metadata.namespace,
|
||||||
|
label_selector=f"job-name={job.metadata.name}",
|
||||||
|
)
|
||||||
|
for pod in pods.items:
|
||||||
|
if pod.status.phase == "Running":
|
||||||
|
running_inspectors.append(
|
||||||
|
(pod.metadata.name, pod.metadata.namespace)
|
||||||
|
)
|
||||||
|
|
||||||
|
if not running_inspectors:
|
||||||
|
typer.echo("No running inspector pods found.")
|
||||||
|
raise typer.Exit(1)
|
||||||
|
|
||||||
|
if len(running_inspectors) == 1:
|
||||||
|
pod_name, pod_namespace = running_inspectors[0]
|
||||||
|
else:
|
||||||
|
pod_name, pod_namespace = fuzzy_select(running_inspectors)
|
||||||
|
if not pod_name:
|
||||||
|
typer.echo("No inspector selected.")
|
||||||
|
raise typer.Exit(1)
|
||||||
|
|
||||||
|
except client.exceptions.ApiException as e:
|
||||||
|
logging.error(f"Failed to list jobs: {e}")
|
||||||
|
typer.echo(f"Failed to list jobs: {e}", err=True)
|
||||||
|
raise typer.Exit(1)
|
||||||
|
|
||||||
|
return pod_name, pod_namespace
|
||||||
|
|
||||||
|
|
||||||
|
def interactive_exec(pod_name: str, namespace: str):
|
||||||
|
# Load kubeconfig from local context (or use load_incluster_config if running inside the cluster)
|
||||||
|
print(f"Connecting to pod {pod_name} in namespace {namespace}...")
|
||||||
|
try:
|
||||||
|
config.load_kube_config()
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error loading kubeconfig: {e}", file=sys.stderr)
|
||||||
|
return
|
||||||
|
|
||||||
|
core_v1 = client.CoreV1Api()
|
||||||
|
command = ["/bin/bash", "-l"]
|
||||||
|
resp = None
|
||||||
|
|
||||||
|
# Save the current terminal settings
|
||||||
|
oldtty = termios.tcgetattr(sys.stdin)
|
||||||
|
|
||||||
|
# Function to handle window resize events
|
||||||
|
def handle_resize(signum, frame):
|
||||||
|
if resp and resp.is_open():
|
||||||
|
# Get the current terminal size
|
||||||
|
cols, rows = os.get_terminal_size()
|
||||||
|
# Send terminal resize command via websocket
|
||||||
|
# Format matches kubectl's resize message format
|
||||||
|
resize_msg = json.dumps({"Width": cols, "Height": rows})
|
||||||
|
resp.write_channel(4, resize_msg)
|
||||||
|
|
||||||
|
# Function to handle exit signals
|
||||||
|
def handle_exit(signum, frame):
|
||||||
|
if resp and resp.is_open():
|
||||||
|
# Send Ctrl+C to the remote process
|
||||||
|
resp.write_stdin("\x03")
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Put terminal into raw mode but don't handle local echo ourselves
|
||||||
|
# Let the remote terminal handle echoing and control characters
|
||||||
|
tty.setraw(sys.stdin.fileno())
|
||||||
|
|
||||||
|
# Set up signal handlers
|
||||||
|
signal.signal(signal.SIGWINCH, handle_resize) # Window resize
|
||||||
|
signal.signal(signal.SIGINT, handle_exit) # Ctrl+C
|
||||||
|
|
||||||
|
# Create a TTY-enabled exec connection to the pod
|
||||||
|
try:
|
||||||
|
resp = stream(
|
||||||
|
core_v1.connect_get_namespaced_pod_exec,
|
||||||
|
pod_name,
|
||||||
|
namespace,
|
||||||
|
command=command,
|
||||||
|
stderr=True,
|
||||||
|
stdin=True,
|
||||||
|
stdout=True,
|
||||||
|
tty=True,
|
||||||
|
_preload_content=False,
|
||||||
|
)
|
||||||
|
print(f"Connected to {pod_name}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"\nError connecting to pod: {e}", file=sys.stderr)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Wait for the connection to be ready
|
||||||
|
time.sleep(0.2)
|
||||||
|
|
||||||
|
# Send initial terminal size
|
||||||
|
cols, rows = os.get_terminal_size()
|
||||||
|
resize_msg = json.dumps({"Width": cols, "Height": rows})
|
||||||
|
resp.write_channel(4, resize_msg)
|
||||||
|
|
||||||
|
# Make sure the size is set by sending a resize event
|
||||||
|
handle_resize(None, None)
|
||||||
|
|
||||||
|
# Set up a simple select-based event loop to handle I/O
|
||||||
|
try:
|
||||||
|
while resp and resp.is_open():
|
||||||
|
# Update the websocket connection
|
||||||
|
resp.update(timeout=0.1)
|
||||||
|
|
||||||
|
# Handle output from the pod
|
||||||
|
if resp.peek_stdout():
|
||||||
|
sys.stdout.write(resp.read_stdout())
|
||||||
|
sys.stdout.flush()
|
||||||
|
if resp.peek_stderr():
|
||||||
|
sys.stderr.write(resp.read_stderr())
|
||||||
|
sys.stderr.flush()
|
||||||
|
|
||||||
|
# Check for input from the user
|
||||||
|
rlist, _, _ = select.select([sys.stdin], [], [], 0.01)
|
||||||
|
if sys.stdin in rlist:
|
||||||
|
# Read input and forward it to the pod without local echo
|
||||||
|
data = os.read(sys.stdin.fileno(), 1024)
|
||||||
|
if not data: # EOF (e.g., user pressed Ctrl+D)
|
||||||
|
break
|
||||||
|
resp.write_stdin(data.decode())
|
||||||
|
except Exception as e:
|
||||||
|
print(f"\nConnection error: {e}", file=sys.stderr)
|
||||||
|
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
# Handle Ctrl+C gracefully
|
||||||
|
print("\nSession terminated by user", file=sys.stderr)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"\nError in interactive session: {e}", file=sys.stderr)
|
||||||
|
finally:
|
||||||
|
# Reset signal handlers
|
||||||
|
signal.signal(signal.SIGWINCH, signal.SIG_DFL)
|
||||||
|
signal.signal(signal.SIGINT, signal.SIG_DFL)
|
||||||
|
|
||||||
|
# Close the connection if it's still open
|
||||||
|
if resp and resp.is_open():
|
||||||
|
try:
|
||||||
|
resp.close()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Always restore terminal settings
|
||||||
|
termios.tcsetattr(sys.stdin, termios.TCSADRAIN, oldtty)
|
||||||
|
print("\nConnection closed", file=sys.stderr)
|
||||||
|
|
||||||
|
|
||||||
|
@app.command()
|
||||||
|
def exec(
|
||||||
|
namespace: Optional[str] = typer.Option(
|
||||||
|
None,
|
||||||
|
help="Kubernetes namespace. If not specified, will search for inspectors across all namespaces.",
|
||||||
|
),
|
||||||
|
shell: Optional[str] = typer.Option(
|
||||||
|
"/bin/bash",
|
||||||
|
"--shell",
|
||||||
|
"-s",
|
||||||
|
help="Shell to use for the inspector pod",
|
||||||
|
),
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Enter the Krayt dragon's lair! Connect to a running inspector pod.
|
||||||
|
If multiple inspectors are found, you'll get to choose which one to explore.
|
||||||
|
"""
|
||||||
|
config.load_kube_config() # or config.load_incluster_config() if running inside a pod
|
||||||
|
client.CoreV1Api()
|
||||||
|
|
||||||
|
pod_name, pod_namespace = get_pod(namespace)
|
||||||
|
|
||||||
|
try:
|
||||||
|
pod_name, pod_namespace = get_pod(namespace)
|
||||||
|
exec_command = [
|
||||||
|
"kubectl",
|
||||||
|
"exec",
|
||||||
|
"-it",
|
||||||
|
"-n",
|
||||||
|
pod_namespace,
|
||||||
|
pod_name,
|
||||||
|
"--",
|
||||||
|
shell,
|
||||||
|
"-l",
|
||||||
|
]
|
||||||
|
|
||||||
|
os.execvp("kubectl", exec_command)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error executing command with kubectl trying python api: {e}")
|
||||||
|
|
||||||
|
interactive_exec(pod_name, pod_namespace)
|
||||||
|
|
||||||
|
|
||||||
|
@app.command()
|
||||||
|
def port_forward(
|
||||||
|
namespace: Optional[str] = typer.Option(
|
||||||
|
None,
|
||||||
|
help="Kubernetes namespace. If not specified, will search for inspectors across all namespaces.",
|
||||||
|
),
|
||||||
|
port: str = typer.Option(
|
||||||
|
"8080:8080",
|
||||||
|
"--port",
|
||||||
|
"-p",
|
||||||
|
help="Port to forward to the inspector pod",
|
||||||
|
),
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Enter the Krayt dragon's lair! Connect to a running inspector pod.
|
||||||
|
If multiple inspectors are found, you'll get to choose which one to explore.
|
||||||
|
"""
|
||||||
|
if ":" not in port:
|
||||||
|
# if port does not contain a ":" it should be an int
|
||||||
|
port = int(port)
|
||||||
|
port = f"{port}:{port}"
|
||||||
|
|
||||||
|
pod_name, pod_namespace = get_pod(namespace)
|
||||||
|
port_forward_command = [
|
||||||
|
"kubectl",
|
||||||
|
"port-forward",
|
||||||
|
"-n",
|
||||||
|
pod_namespace,
|
||||||
|
pod_name,
|
||||||
|
port,
|
||||||
|
]
|
||||||
|
|
||||||
|
os.execvp("kubectl", port_forward_command)
|
||||||
|
|
||||||
|
|
||||||
|
@app.command()
|
||||||
|
def clean(
|
||||||
|
namespace: Optional[str] = typer.Option(
|
||||||
|
None,
|
||||||
|
help="Kubernetes namespace. If not specified, will cleanup in all namespaces.",
|
||||||
|
),
|
||||||
|
yes: bool = typer.Option(
|
||||||
|
False,
|
||||||
|
"--yes",
|
||||||
|
"-y",
|
||||||
|
help="Skip confirmation prompt.",
|
||||||
|
),
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Clean up after your hunt! Remove all Krayt inspector jobs.
|
||||||
|
Use --yes/-y to skip confirmation and clean up immediately.
|
||||||
|
"""
|
||||||
|
config.load_kube_config()
|
||||||
|
batch_api = client.BatchV1Api()
|
||||||
|
|
||||||
|
try:
|
||||||
|
if namespace:
|
||||||
|
if namespace in PROTECTED_NAMESPACES:
|
||||||
|
typer.echo(f"Error: Cannot cleanup in protected namespace {namespace}")
|
||||||
|
raise typer.Exit(1)
|
||||||
|
logging.debug(f"Listing jobs in namespace {namespace}")
|
||||||
|
jobs = batch_api.list_namespaced_job(
|
||||||
|
namespace=namespace, label_selector="app=krayt"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
logging.debug("Listing jobs in all namespaces")
|
||||||
|
jobs = batch_api.list_job_for_all_namespaces(label_selector="app=krayt")
|
||||||
|
|
||||||
|
# Filter out jobs in protected namespaces
|
||||||
|
jobs.items = [
|
||||||
|
job
|
||||||
|
for job in jobs.items
|
||||||
|
if job.metadata.namespace not in PROTECTED_NAMESPACES
|
||||||
|
]
|
||||||
|
|
||||||
|
if not jobs.items:
|
||||||
|
typer.echo("No Krayt inspector jobs found.")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Show confirmation prompt
|
||||||
|
if not yes:
|
||||||
|
job_list = "\n".join(
|
||||||
|
f" {job.metadata.namespace}/{job.metadata.name}" for job in jobs.items
|
||||||
|
)
|
||||||
|
typer.echo(f"The following inspector jobs will be deleted:\n{job_list}")
|
||||||
|
if not typer.confirm("Are you sure you want to continue?"):
|
||||||
|
typer.echo("Operation cancelled.")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Delete each job
|
||||||
|
for job in jobs.items:
|
||||||
|
try:
|
||||||
|
logging.debug(
|
||||||
|
f"Deleting job {job.metadata.namespace}/{job.metadata.name}"
|
||||||
|
)
|
||||||
|
batch_api.delete_namespaced_job(
|
||||||
|
name=job.metadata.name,
|
||||||
|
namespace=job.metadata.namespace,
|
||||||
|
body=client.V1DeleteOptions(propagation_policy="Background"),
|
||||||
|
)
|
||||||
|
typer.echo(f"Deleted job: {job.metadata.namespace}/{job.metadata.name}")
|
||||||
|
except client.exceptions.ApiException as e:
|
||||||
|
logging.error(
|
||||||
|
f"Failed to delete job {job.metadata.namespace}/{job.metadata.name}: {e}"
|
||||||
|
)
|
||||||
|
typer.echo(
|
||||||
|
f"Failed to delete job {job.metadata.namespace}/{job.metadata.name}: {e}",
|
||||||
|
err=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
except client.exceptions.ApiException as e:
|
||||||
|
logging.error(f"Failed to list jobs: {e}")
|
||||||
|
typer.echo(f"Failed to list jobs: {e}", err=True)
|
||||||
|
raise typer.Exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
@app.command()
|
||||||
|
def create(
|
||||||
|
namespace: Optional[str] = typer.Option(
|
||||||
|
None,
|
||||||
|
"--namespace",
|
||||||
|
"-n",
|
||||||
|
help="Kubernetes namespace. If not specified, will search for pods across all namespaces.",
|
||||||
|
),
|
||||||
|
clone: Optional[str] = typer.Option(
|
||||||
|
None,
|
||||||
|
"--clone",
|
||||||
|
"-c",
|
||||||
|
help="Clone an existing pod",
|
||||||
|
),
|
||||||
|
image: str = typer.Option(
|
||||||
|
"alpine:latest",
|
||||||
|
"--image",
|
||||||
|
"-i",
|
||||||
|
help="Container image to use for the inspector pod",
|
||||||
|
),
|
||||||
|
imagepullsecret: Optional[str] = typer.Option(
|
||||||
|
None,
|
||||||
|
"--imagepullsecret",
|
||||||
|
help="Name of the image pull secret to use for pulling private images",
|
||||||
|
),
|
||||||
|
additional_packages: Optional[List[str]] = typer.Option(
|
||||||
|
None,
|
||||||
|
"--additional-packages",
|
||||||
|
"-ap",
|
||||||
|
help="additional packages to install in the inspector pod",
|
||||||
|
),
|
||||||
|
additional_package_bundles: Optional[List[str]] = typer.Option(
|
||||||
|
None,
|
||||||
|
"--additional-package-bundles",
|
||||||
|
"-ab",
|
||||||
|
help="additional packages to install in the inspector pod",
|
||||||
|
),
|
||||||
|
pre_init_scripts: Optional[List[str]] = typer.Option(
|
||||||
|
None,
|
||||||
|
"--pre-init-scripts",
|
||||||
|
help="additional scripts to execute at the end of container initialization",
|
||||||
|
),
|
||||||
|
post_init_scripts: Optional[List[str]] = typer.Option(
|
||||||
|
None,
|
||||||
|
"--post-init-scripts",
|
||||||
|
"--init-scripts",
|
||||||
|
help="additional scripts to execute at the start of container initialization",
|
||||||
|
),
|
||||||
|
pre_init_hooks: Optional[List[str]] = typer.Option(
|
||||||
|
None,
|
||||||
|
"--pre-init-hooks",
|
||||||
|
help="additional hooks to execute at the end of container initialization",
|
||||||
|
),
|
||||||
|
post_init_hooks: Optional[List[str]] = typer.Option(
|
||||||
|
None,
|
||||||
|
"--post-init-hooks",
|
||||||
|
help="additional hooks to execute at the start of container initialization",
|
||||||
|
),
|
||||||
|
apply: bool = typer.Option(
|
||||||
|
False,
|
||||||
|
"--apply",
|
||||||
|
help="Automatically apply the changes instead of just echoing them.",
|
||||||
|
),
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Krack open a Krayt dragon! Create an inspector pod to explore what's inside your volumes.
|
||||||
|
If namespace is not specified, will search for pods across all namespaces.
|
||||||
|
The inspector will be created in the same namespace as the selected pod.
|
||||||
|
"""
|
||||||
|
# For create, we want to list all pods, not just Krayt pods
|
||||||
|
selected_namespace = namespace
|
||||||
|
selected_pod = clone
|
||||||
|
|
||||||
|
if namespace is None and clone is not None and "/" in clone:
|
||||||
|
selected_namespace, selected_pod = clone.split("/", 1)
|
||||||
|
|
||||||
|
get_namespaces(namespace)
|
||||||
|
pods = get_pods(namespace, label_selector="app!=krayt")
|
||||||
|
|
||||||
|
if not pods:
|
||||||
|
typer.echo("No pods found.")
|
||||||
|
raise typer.Exit(1)
|
||||||
|
|
||||||
|
if selected_pod not in (p[0] for p in pods) or selected_pod is None:
|
||||||
|
if selected_pod is not None:
|
||||||
|
pods = [p for p in pods if selected_pod in p[0]]
|
||||||
|
if len(pods) == 1:
|
||||||
|
selected_pod, selected_namespace = pods[0]
|
||||||
|
else:
|
||||||
|
selected_pod, selected_namespace = fuzzy_select(pods)
|
||||||
|
if not selected_pod:
|
||||||
|
typer.echo("No pod selected.")
|
||||||
|
raise typer.Exit(1)
|
||||||
|
|
||||||
|
pod_spec = get_pod_spec(selected_pod, selected_namespace)
|
||||||
|
volume_mounts, volumes = get_pod_volumes_and_mounts(pod_spec)
|
||||||
|
|
||||||
|
inspector_job = create_inspector_job(
|
||||||
|
client.CoreV1Api(),
|
||||||
|
selected_namespace,
|
||||||
|
selected_pod,
|
||||||
|
volume_mounts,
|
||||||
|
volumes,
|
||||||
|
image=image,
|
||||||
|
imagepullsecret=imagepullsecret,
|
||||||
|
additional_packages=additional_packages,
|
||||||
|
pre_init_scripts=pre_init_scripts,
|
||||||
|
post_init_scripts=post_init_scripts,
|
||||||
|
pre_init_hooks=pre_init_hooks,
|
||||||
|
post_init_hooks=post_init_hooks,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Output the job manifest
|
||||||
|
api_client = client.ApiClient()
|
||||||
|
job_dict = api_client.sanitize_for_serialization(inspector_job)
|
||||||
|
job_yaml = yaml.dump(job_dict, sort_keys=False)
|
||||||
|
|
||||||
|
if apply:
|
||||||
|
batch_api = client.BatchV1Api()
|
||||||
|
job = batch_api.create_namespaced_job(
|
||||||
|
namespace=selected_namespace,
|
||||||
|
body=inspector_job,
|
||||||
|
)
|
||||||
|
print(f"Job {job.metadata.name} created.")
|
||||||
|
return job
|
||||||
|
else:
|
||||||
|
# Just echo the YAML
|
||||||
|
typer.echo(job_yaml)
|
||||||
|
|
||||||
|
|
||||||
|
@app.command()
|
||||||
|
def version():
|
||||||
|
"""Show the version of Krayt."""
|
||||||
|
typer.echo(f"Version: {__version__}")
|
||||||
|
|
||||||
|
|
||||||
|
@app.command()
|
||||||
|
def logs(
|
||||||
|
namespace: Optional[str] = typer.Option(
|
||||||
|
None,
|
||||||
|
help="Kubernetes namespace. If not specified, will search for inspectors across all namespaces.",
|
||||||
|
),
|
||||||
|
follow: bool = typer.Option(
|
||||||
|
False,
|
||||||
|
"--follow",
|
||||||
|
"-f",
|
||||||
|
help="Follow the logs in real-time",
|
||||||
|
),
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
View logs from a Krayt inspector pod.
|
||||||
|
If multiple inspectors are found, you'll get to choose which one to explore.
|
||||||
|
"""
|
||||||
|
pods = get_pods(namespace)
|
||||||
|
if not pods:
|
||||||
|
typer.echo("No pods found.")
|
||||||
|
raise typer.Exit(1)
|
||||||
|
|
||||||
|
selected_pod, selected_namespace = fuzzy_select(pods)
|
||||||
|
if not selected_pod:
|
||||||
|
typer.echo("No pod selected.")
|
||||||
|
raise typer.Exit(1)
|
||||||
|
|
||||||
|
try:
|
||||||
|
config.load_kube_config()
|
||||||
|
api = client.CoreV1Api()
|
||||||
|
logs = api.read_namespaced_pod_log(
|
||||||
|
name=selected_pod,
|
||||||
|
namespace=selected_namespace,
|
||||||
|
follow=follow,
|
||||||
|
_preload_content=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
if follow:
|
||||||
|
for line in logs:
|
||||||
|
typer.echo(line.decode("utf-8").strip())
|
||||||
|
else:
|
||||||
|
typer.echo(logs.read().decode("utf-8"))
|
||||||
|
|
||||||
|
except client.rest.ApiException as e:
|
||||||
|
typer.echo(f"Error reading logs: {e}")
|
||||||
|
raise typer.Exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
@app.command("list")
|
||||||
|
def list_pods():
|
||||||
|
pods = get_pods()
|
||||||
|
if not pods:
|
||||||
|
typer.echo("No pods found.")
|
||||||
|
raise typer.Exit(1)
|
||||||
|
|
||||||
|
for pod, namespace in pods:
|
||||||
|
typer.echo(f"{pod} ({namespace})")
|
||||||
|
|
||||||
|
|
||||||
|
# def main():
|
||||||
|
# setup_environment()
|
||||||
|
# app()
|
||||||
|
#
|
||||||
|
#
|
||||||
|
# if __name__ == "__main__":
|
||||||
|
# main()
|
||||||
98
krayt/cli/templates.py
Normal file
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 render(
|
||||||
|
template_name: Optional[str] = typer.Option("base.sh", "--template-name", "-t"),
|
||||||
|
volumes: Optional[List[str]] = typer.Option(
|
||||||
|
None,
|
||||||
|
"--volume",
|
||||||
|
),
|
||||||
|
pvcs: Optional[List[str]] = typer.Option(
|
||||||
|
None,
|
||||||
|
"--pvc",
|
||||||
|
),
|
||||||
|
additional_packages: Optional[List[str]] = typer.Option(
|
||||||
|
None, "--additional-packages", "-ap"
|
||||||
|
),
|
||||||
|
pre_init_scripts: Optional[List[str]] = typer.Option(
|
||||||
|
None,
|
||||||
|
"--pre-init-scripts",
|
||||||
|
help="additional scripts to execute at the end of container initialization",
|
||||||
|
),
|
||||||
|
post_init_scripts: Optional[List[str]] = typer.Option(
|
||||||
|
None,
|
||||||
|
"--post-init-scripts",
|
||||||
|
"--init-scripts",
|
||||||
|
help="additional scripts to execute at the start of container initialization",
|
||||||
|
),
|
||||||
|
pre_init_hooks: Optional[List[str]] = typer.Option(
|
||||||
|
None,
|
||||||
|
"--pre-init-hooks",
|
||||||
|
help="additional hooks to execute at the end of container initialization",
|
||||||
|
),
|
||||||
|
post_init_hooks: Optional[List[str]] = typer.Option(
|
||||||
|
None,
|
||||||
|
"--post-init-hooks",
|
||||||
|
"--init-hooks",
|
||||||
|
help="additional hooks to execute at the start of container initialization",
|
||||||
|
),
|
||||||
|
):
|
||||||
|
template = env.get_template(template_name)
|
||||||
|
rendered = template.render(
|
||||||
|
volumes=volumes,
|
||||||
|
pvcs=pvcs,
|
||||||
|
additional_packages=additional_packages,
|
||||||
|
pre_init_scripts=pre_init_scripts,
|
||||||
|
post_init_scripts=post_init_scripts,
|
||||||
|
pre_init_hooks=pre_init_hooks,
|
||||||
|
post_init_hooks=post_init_hooks,
|
||||||
|
)
|
||||||
|
print(rendered)
|
||||||
|
|
||||||
|
|
||||||
|
# @app.command()
|
||||||
|
# def install(
|
||||||
|
# additional_packages: Optional[List[str]] = typer.Option(
|
||||||
|
# ..., "--additional-packages", "-ap"
|
||||||
|
# ),
|
||||||
|
# ):
|
||||||
|
# template_name = "install.sh"
|
||||||
|
# template = env.get_template(template_name)
|
||||||
|
# rendered = template.render(additional_packages=additional_packages)
|
||||||
|
# print(rendered)
|
||||||
|
#
|
||||||
|
#
|
||||||
|
# @app.command()
|
||||||
|
# def motd(
|
||||||
|
# volumes: Optional[List[str]] = typer.Option(
|
||||||
|
# None,
|
||||||
|
# "--volume",
|
||||||
|
# ),
|
||||||
|
# pvcs: Optional[List[str]] = typer.Option(
|
||||||
|
# None,
|
||||||
|
# "--pvc",
|
||||||
|
# ),
|
||||||
|
# additional_packages: Optional[List[str]] = typer.Option(
|
||||||
|
# ..., "--additional-packages", "-ap"
|
||||||
|
# ),
|
||||||
|
# ):
|
||||||
|
# template_name = "motd.sh"
|
||||||
|
# template = env.get_template(template_name)
|
||||||
|
# rendered = template.render(
|
||||||
|
# volumes=volumes,
|
||||||
|
# pvcs=pvcs,
|
||||||
|
# additional_packages=additional_packages,
|
||||||
|
# )
|
||||||
|
# print(rendered)
|
||||||
191
krayt/package.py
Normal file
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 mounts %}
|
||||||
|
|
||||||
|
Mounted Volumes:
|
||||||
|
{%- for mount in mounts %}
|
||||||
|
- {{ mount.name }}:{{ mount.mount_path }}
|
||||||
|
{%- endfor %}
|
||||||
|
{%- endif %}
|
||||||
|
|
||||||
|
{%- if pvcs %}
|
||||||
|
|
||||||
|
Persistent Volume Claims:
|
||||||
|
{%- for pvc in pvcs %}
|
||||||
|
- {{ pvc }}
|
||||||
|
{%- endfor %}
|
||||||
|
{%- endif %}
|
||||||
|
|
||||||
|
{%- if secrets %}
|
||||||
|
|
||||||
|
Mounted Secrets:
|
||||||
|
{%- for secret in secrets %}
|
||||||
|
- {{ secret }}
|
||||||
|
{%- endfor %}
|
||||||
|
{%- endif %}
|
||||||
|
|
||||||
|
{%- if additional_packages %}
|
||||||
|
|
||||||
|
Additional Packages:
|
||||||
|
{%- for package in additional_packages %}
|
||||||
|
- {{ package }}
|
||||||
|
{%- endfor %}
|
||||||
|
{%- endif %}
|
||||||
|
|
||||||
|
EOF
|
||||||
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()
|
||||||
70
pyproject.toml
Normal file
70
pyproject.toml
Normal file
|
|
@ -0,0 +1,70 @@
|
||||||
|
[build-system]
|
||||||
|
requires = ["hatchling"]
|
||||||
|
build-backend = "hatchling.build"
|
||||||
|
|
||||||
|
[tool.hatch.build.targets.wheel]
|
||||||
|
|
||||||
|
[tool.hatch.build.targets.sdist]
|
||||||
|
exclude = ["/.github"]
|
||||||
|
|
||||||
|
[tool.hatch.build.targets.binary]
|
||||||
|
|
||||||
|
[project]
|
||||||
|
name = "krayt"
|
||||||
|
dynamic = ["version"]
|
||||||
|
description = 'kubernetes volume explorer'
|
||||||
|
readme = "README.md"
|
||||||
|
requires-python = ">=3.8"
|
||||||
|
keywords = []
|
||||||
|
classifiers = [
|
||||||
|
"Development Status :: 4 - Beta",
|
||||||
|
"Programming Language :: Python",
|
||||||
|
"Programming Language :: Python :: 3.8",
|
||||||
|
"Programming Language :: Python :: 3.9",
|
||||||
|
"Programming Language :: Python :: 3.10",
|
||||||
|
"Programming Language :: Python :: 3.11",
|
||||||
|
"Programming Language :: Python :: 3.12",
|
||||||
|
"Programming Language :: Python :: Implementation :: CPython",
|
||||||
|
"Programming Language :: Python :: Implementation :: PyPy",
|
||||||
|
]
|
||||||
|
dependencies = [
|
||||||
|
"typer",
|
||||||
|
"kubernetes",
|
||||||
|
"inquirerPy",
|
||||||
|
"inquirer",
|
||||||
|
"jinja2",
|
||||||
|
"iterfzf",
|
||||||
|
"pydantic",
|
||||||
|
"more-itertools",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[project.authors]]
|
||||||
|
name = "Waylon Walker"
|
||||||
|
email = "waylon@waylonwalker.com"
|
||||||
|
|
||||||
|
[project.license]
|
||||||
|
file = "LICENSE"
|
||||||
|
|
||||||
|
[project.urls]
|
||||||
|
Homepage = "https://github.com/waylonwalker/krayt#readme"
|
||||||
|
Documentation = "https://github.com/waylonwalker/krayt#readme"
|
||||||
|
Changelog = "https://github.com/waylonwalker/krayt#changelog"
|
||||||
|
Issues = "https://github.com/waylonwalker/krayt/issues"
|
||||||
|
Source = "https://github.com/waylonwalker/krayt"
|
||||||
|
|
||||||
|
[tool.hatch.version]
|
||||||
|
path = "krayt/__about__.py"
|
||||||
|
|
||||||
|
[project.scripts]
|
||||||
|
krayt = "krayt.cli:app"
|
||||||
|
|
||||||
|
[tool.hatch.envs.default]
|
||||||
|
dependencies = [
|
||||||
|
"ruff",
|
||||||
|
"pyinstrument",
|
||||||
|
]
|
||||||
|
|
||||||
|
[tool.hatch.envs.default.scripts]
|
||||||
|
lint = "ruff check krayt"
|
||||||
|
format = "ruff format krayt"
|
||||||
|
lint-format = ['lint', 'format']
|
||||||
|
|
@ -21,7 +21,11 @@ def get_release_notes(version):
|
||||||
|
|
||||||
You can install krayt using one of these methods:
|
You can install krayt using one of these methods:
|
||||||
|
|
||||||
> !krayt requires [uv](https://docs.astral.sh/uv/getting-started/installation/) to be installed
|
## pypi
|
||||||
|
|
||||||
|
``` bash
|
||||||
|
pip install krayt
|
||||||
|
```
|
||||||
|
|
||||||
### Using i.jpillora.com (recommended)
|
### Using i.jpillora.com (recommended)
|
||||||
|
|
||||||
|
|
@ -37,8 +41,8 @@ curl -fsSL https://github.com/waylonwalker/krayt/releases/download/v{version}/in
|
||||||
|
|
||||||
### Manual download
|
### Manual download
|
||||||
You can also manually download the archive for your platform from the releases page:
|
You can also manually download the archive for your platform from the releases page:
|
||||||
- [x86_64-unknown-linux-gnu](https://github.com/waylonwalker/krayt/releases/download/v{version}/krayt-{version}-x86_64-unknown-linux-gnu.tar.gz)
|
- [x86_64-unknown-linux-gnu](https://github.com/waylonwalker/krayt/releases/download/v{version}/krayt-{version}-x86_64-unknown-linux-gnu)
|
||||||
- [aarch64-unknown-linux-gnu](https://github.com/waylonwalker/krayt/releases/download/v{version}/krayt-{version}-aarch64-unknown-linux-gnu.tar.gz)"""
|
- [aarch64-unknown-linux-gnu](https://github.com/waylonwalker/krayt/releases/download/v{version}/krayt-{version}-aarch64-unknown-linux-gnu)"""
|
||||||
|
|
||||||
# Get help output for main command and all subcommands
|
# Get help output for main command and all subcommands
|
||||||
try:
|
try:
|
||||||
|
|
@ -46,17 +50,23 @@ You can also manually download the archive for your platform from the releases p
|
||||||
|
|
||||||
# Get main help output
|
# Get main help output
|
||||||
main_help = subprocess.check_output(
|
main_help = subprocess.check_output(
|
||||||
["./krayt.py", "--help"],
|
["krayt", "--help"],
|
||||||
stderr=subprocess.STDOUT,
|
stderr=subprocess.STDOUT,
|
||||||
universal_newlines=True,
|
universal_newlines=True,
|
||||||
)
|
)
|
||||||
help_outputs.append(("Main Command", main_help))
|
help_outputs.append(("Main Command", main_help))
|
||||||
|
|
||||||
# Get help for each subcommand
|
# Get help for each subcommand
|
||||||
subcommands = ["create", "exec", "clean", "version"]
|
subcommands = [
|
||||||
|
"create",
|
||||||
|
"exec",
|
||||||
|
"clean",
|
||||||
|
"version",
|
||||||
|
"pod",
|
||||||
|
]
|
||||||
for cmd in subcommands:
|
for cmd in subcommands:
|
||||||
cmd_help = subprocess.check_output(
|
cmd_help = subprocess.check_output(
|
||||||
["./krayt.py", cmd, "--help"],
|
["krayt", cmd, "--help"],
|
||||||
stderr=subprocess.STDOUT,
|
stderr=subprocess.STDOUT,
|
||||||
universal_newlines=True,
|
universal_newlines=True,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -148,11 +148,11 @@ function install {
|
||||||
FTYPE=""
|
FTYPE=""
|
||||||
case "${OS}_${ARCH}" in
|
case "${OS}_${ARCH}" in
|
||||||
"linux_amd64")
|
"linux_amd64")
|
||||||
URL="https://github.com/WaylonWalker/nvim-manager/releases/download/v${RELEASE}/nvim-manager-${RELEASE}-x86_64-unknown-linux-gnu.tar.gz"
|
URL="https://github.com/WaylonWalker/krayt/releases/download/v${RELEASE}/krayt-${RELEASE}-x86_64-unknown-linux-gnu.tar.gz"
|
||||||
FTYPE=".tar.gz"
|
FTYPE=".tar.gz"
|
||||||
;;
|
;;
|
||||||
"linux_arm64")
|
"linux_arm64")
|
||||||
URL="https://github.com/WaylonWalker/nvim-manager/releases/download/v${RELEASE}/nvim-manager-${RELEASE}-aarch64-unknown-linux-gnu.tar.gz"
|
URL="https://github.com/WaylonWalker/krayt/releases/download/v${RELEASE}/krayt-${RELEASE}-aarch64-unknown-linux-gnu.tar.gz"
|
||||||
FTYPE=".tar.gz"
|
FTYPE=".tar.gz"
|
||||||
;;
|
;;
|
||||||
*) fail "No asset for platform ${OS}-${ARCH}" ;;
|
*) fail "No asset for platform ${OS}-${ARCH}" ;;
|
||||||
|
|
@ -193,7 +193,7 @@ function install {
|
||||||
unzip -o -qq tmp.zip || fail "unzip failed"
|
unzip -o -qq tmp.zip || fail "unzip failed"
|
||||||
rm tmp.zip || fail "cleanup failed"
|
rm tmp.zip || fail "cleanup failed"
|
||||||
elif [[ $FTYPE = ".bin" ]]; then
|
elif [[ $FTYPE = ".bin" ]]; then
|
||||||
bash -c "$GET $URL" >"nvim-manager_${OS}_${ARCH}" || fail "download failed"
|
bash -c "$GET $URL" >"krayt_${OS}_${ARCH}" || fail "download failed"
|
||||||
else
|
else
|
||||||
fail "unknown file type: $FTYPE"
|
fail "unknown file type: $FTYPE"
|
||||||
fi
|
fi
|
||||||
|
|
|
||||||
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
|
||||||
Loading…
Add table
Add a link
Reference in a new issue