diff --git a/.github/workflows/release-pypi.yaml b/.github/workflows/release-pypi.yaml deleted file mode 100644 index 16111e2..0000000 --- a/.github/workflows/release-pypi.yaml +++ /dev/null @@ -1,47 +0,0 @@ -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 diff --git a/.gitignore b/.gitignore index 6f4e31a..e1a3186 100644 --- a/.gitignore +++ b/.gitignore @@ -962,4 +962,3 @@ FodyWeavers.xsd # Additional files built by Visual Studio # End of https://www.toptal.com/developers/gitignore/api/vim,node,data,emacs,python,pycharm,executable,sublimetext,visualstudio,visualstudiocode -*.null-ls* diff --git a/CHANGELOG.md b/CHANGELOG.md index 29ffba6..2645ca0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,54 +1,3 @@ -## 0.4.3 - -- working out binary release process - -## 0.4.2 - -- working out binary release process - -## 0.4.1 - -- Automated release for both pypi and github - -## 0.4.0 - -- create now has --apply to apply the generated manifest to the cluster -- generic templates endpoint for cli -- better motd for volume mounts - -## 0.3.0 - -- created pypi release -- updated releases to use pyapp -- all new package -- port forward support -- additional_packages support - -## 0.2.0 - -### Added - -- Support for imagepullsecret flag on krayt create command - - Allows pulling from private container registries by specifying an image pull secret - -## 0.1.0 - -### Added - -- Support for initialization scripts in `~/.config/krayt/init.d/` - - Scripts run before package installation - - Support for proxy configuration - - Custom package repositories setup - - Environment variable configuration -- Example initialization scripts: - - `00_proxy.sh` for proxy configuration - - `10_install_git.sh` for git installation and configuration -- Improved binary installation process - - Better platform detection - - Support for multiple archive formats (.tar.gz, .gz, .bz2, .zip, .bin) - - Improved error handling and user feedback - - Automatic sudo elevation when needed for binary installation - ## 0.0.0 - Initial release diff --git a/LICENSE b/LICENSE deleted file mode 100644 index 38ea521..0000000 --- a/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2021-present Waylon S. Walker - -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. diff --git a/README.md b/README.md index b554981..c05e73c 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,5 @@ # Krayt - The Kubernetes Volume Inspector -![krayt hero image](./krayt.webp "A dark, cartoon-style wide-format illustration featuring a heroic explorer standing in a twilight desert beside a cracked-open dragon skull. The explorer holds a glowing pearl that reveals floating icons representing data and technology. The hero wears utility gear and a sword, with terminal and file icons on their belt. The desert backdrop includes jagged rocks, two moons in a starry sky, and moody blue and purple tones. At the top, the word “KRAYT” is displayed in bold, tech-inspired fantasy lettering.") - Like cracking open a Krayt dragon pearl, this tool helps you inspect what's inside your Kubernetes volumes. Hunt down storage issues and explore your persistent data like a true Tatooine dragon hunter. @@ -40,12 +38,6 @@ This will install the `krayt` command to `/usr/local/bin`. # Create a new inspector and apply it directly krayt create | kubectl apply -f - -# Use a custom image -krayt create --image custom-image:latest | kubectl apply -f - - -# Use a private image with pull secret -krayt create --image private-registry.com/image:latest --imagepullsecret my-registry-secret | kubectl apply -f - - # Or review the manifest first krayt create > inspector.yaml kubectl apply -f inspector.yaml @@ -73,80 +65,6 @@ Your inspector pod comes equipped with a full arsenal of tools: - **Network Tools**: `mtr`, `dig` - **Cloud & Database**: `aws-cli`, `sqlite3` -## Customization - -### Init Scripts - -Krayt supports initialization scripts that run in the inspector pod before any packages are installed. These scripts are useful for: -- Setting up proxy configurations -- Installing additional tools -- Configuring custom package repositories -- Setting environment variables - -Place your scripts in `~/.config/krayt/init.d/` with a `.sh` extension. Scripts are executed in alphabetical order, so you can control the execution sequence using numerical prefixes. - -Example init scripts: - -1. Install additional tools (`~/.config/krayt/init.d/10_install_git.sh`): -```bash -#!/bin/sh -echo "Installing additional tools..." - -# Install git for source control -apk add git - -# Configure git -git config --global init.defaultBranch main -git config --global core.editor vi -``` - -2. Set up custom repositories (`~/.config/krayt/init.d/20_custom_repos.sh`): -```bash -#!/bin/sh -echo "Adding custom package repositories..." - -# Add testing repository for newer packages -echo "@testing http://dl-cdn.alpinelinux.org/alpine/edge/testing" >> /etc/apk/repositories - -# Update package list -apk update -``` - -### Proxy Configuration - -If your environment requires a proxy, you have two options: - -1. **Environment Variables** (Recommended): - ```bash - # Add to your shell's rc file (e.g., ~/.bashrc, ~/.zshrc) - export HTTP_PROXY="http://proxy.example.com:8080" - export HTTPS_PROXY="http://proxy.example.com:8080" - export NO_PROXY="localhost,127.0.0.1,.internal.example.com" - ``` - -2. **Init Script** (`~/.config/krayt/init.d/00_proxy.sh`): - ```bash - #!/bin/sh - echo "Configuring proxy settings..." - - # Set proxy for Alpine package manager - mkdir -p /etc/apk - cat > /etc/apk/repositories << EOF - http://dl-cdn.alpinelinux.org/alpine/latest-stable/main - http://dl-cdn.alpinelinux.org/alpine/latest-stable/community - - # Configure proxy - proxy=http://proxy.example.com:8080 - EOF - - # Set proxy for other tools - export HTTP_PROXY="http://proxy.example.com:8080" - export HTTPS_PROXY="http://proxy.example.com:8080" - export NO_PROXY="localhost,127.0.0.1,.internal.example.com" - ``` - -The proxy configuration will be applied before any packages are installed, ensuring that all package installations and network operations work correctly through your proxy. - ## Quotes from the Field > "Inside every volume lies a pearl of wisdom waiting to be discovered." diff --git a/examples/init.d/00_proxy.sh b/examples/init.d/00_proxy.sh deleted file mode 100644 index 37d99cb..0000000 --- a/examples/init.d/00_proxy.sh +++ /dev/null @@ -1,25 +0,0 @@ -#!/bin/sh -echo "Configuring proxy settings..." - -# Set proxy for Alpine package manager -mkdir -p /etc/apk -cat > /etc/apk/repositories << EOF -http://dl-cdn.alpinelinux.org/alpine/latest-stable/main -http://dl-cdn.alpinelinux.org/alpine/latest-stable/community - -# Configure proxy -proxy=http://proxy.example.com:8080 -EOF - -# Set proxy for other tools -export HTTP_PROXY="http://proxy.example.com:8080" -export HTTPS_PROXY="http://proxy.example.com:8080" -export NO_PROXY="localhost,127.0.0.1,.internal.example.com" - -# Test proxy configuration -echo "Testing proxy configuration..." -if curl -s -m 5 https://www.google.com > /dev/null; then - echo "Proxy configuration successful!" -else - echo "Warning: Proxy test failed. Check your proxy settings." -fi diff --git a/examples/init.d/10_install_git.sh b/examples/init.d/10_install_git.sh deleted file mode 100644 index 1646dbf..0000000 --- a/examples/init.d/10_install_git.sh +++ /dev/null @@ -1,20 +0,0 @@ -#!/bin/sh -echo "Installing additional development tools..." - -# Install git and related tools -apk add git git-lfs - -# Configure git defaults -git config --global init.defaultBranch main -git config --global core.editor vi -git config --global pull.rebase false - -# Add some helpful git aliases -git config --global alias.st status -git config --global alias.co checkout -git config --global alias.br branch -git config --global alias.ci commit -git config --global alias.unstage 'reset HEAD --' -git config --global alias.last 'log -1 HEAD' - -echo "Git configuration complete." diff --git a/examples/init.d/20_custom_repos.sh b/examples/init.d/20_custom_repos.sh deleted file mode 100644 index 164f496..0000000 --- a/examples/init.d/20_custom_repos.sh +++ /dev/null @@ -1,19 +0,0 @@ -#!/bin/sh -echo "Setting up additional package repositories..." - -# Add testing repository for newer packages -echo "@testing http://dl-cdn.alpinelinux.org/alpine/edge/testing" >> /etc/apk/repositories - -# Add community repository -echo "@community http://dl-cdn.alpinelinux.org/alpine/edge/community" >> /etc/apk/repositories - -# Update package list -apk update - -# Install some useful tools from testing/community -apk add \ - @testing golang \ - @community rust \ - @community cargo - -echo "Additional repositories configured and packages installed." diff --git a/justfile b/justfile index f62f5c9..938d946 100644 --- a/justfile +++ b/justfile @@ -3,7 +3,7 @@ delete-tag: set -euo pipefail # Get the version - VERSION=$(hatch version) + VERSION=$(cat version) # Delete the tag git tag -d "v$VERSION" @@ -14,65 +14,59 @@ delete-release: set -euo pipefail # Get the version - VERSION=$(hatch version) + VERSION=$(cat version) # Delete the release gh release delete "v$VERSION" create-tag: #!/usr/bin/env bash - VERSION=$(hatch version) + VERSION=$(cat version) git tag -a "v$VERSION" -m "Release v$VERSION" git push origin "v$VERSION" create-archives: #!/usr/bin/env bash - VERSION=$(hatch version) + VERSION=$(cat version) rm -rf dist build - hatch build -t binary - - krayt_bin=dist/binary/krayt-${VERSION} + mkdir -p dist # Create the binary for each platform for platform in "x86_64-unknown-linux-gnu" "aarch64-unknown-linux-gnu"; do - outbin="krayt-${VERSION}-${platform}" + outdir="krayt-${VERSION}-${platform}" + mkdir -p "dist/${outdir}" + # Copy the Python script and update version - cp ${krayt_bin} "dist/binary/${outbin}" + cp krayt.py "dist/${outdir}/krayt.py" + sed -i "s/NIGHTLY/${VERSION}/" "dist/${outdir}/krayt.py" + + cd dist + tar czf "${outdir}.tar.gz" "${outdir}" + sha256sum "${outdir}.tar.gz" > "${outdir}.tar.gz.sha256" + cd .. done # Generate install.sh - # ./scripts/generate_install_script.py "$VERSION" - # chmod +x dist/install.sh + ./scripts/generate_install_script.py "$VERSION" + chmod +x dist/install.sh create-release: create-tag create-archives #!/usr/bin/env bash - VERSION=$(hatch version) + VERSION=$(cat version) ./scripts/get_release_notes.py "$VERSION" > release_notes.tmp - - # Check if release already exists - if gh release view "v$VERSION" &>/dev/null; then - echo "Release v$VERSION already exists. Uploading binaries..." - # Upload binaries to existing release - gh release upload "v$VERSION" \ - dist/binary/krayt-${VERSION} \ - dist/binary/krayt-${VERSION}-aarch64-unknown-linux-gnu \ - dist/binary/krayt-${VERSION}-x86_64-unknown-linux-gnu || true - else - echo "Creating new release v$VERSION" - # Create new release with binaries - gh release create "v$VERSION" \ - --title "v$VERSION" \ - --notes-file release_notes.tmp \ - dist/binary/krayt-${VERSION} \ - dist/binary/krayt-${VERSION}-aarch64-unknown-linux-gnu \ - dist/binary/krayt-${VERSION}-x86_64-unknown-linux-gnu - fi + gh release create "v$VERSION" \ + --title "v$VERSION" \ + --notes-file release_notes.tmp \ + dist/krayt-${VERSION}-x86_64-unknown-linux-gnu.tar.gz \ + dist/krayt-${VERSION}-x86_64-unknown-linux-gnu.tar.gz.sha256 \ + dist/krayt-${VERSION}-aarch64-unknown-linux-gnu.tar.gz \ + dist/krayt-${VERSION}-aarch64-unknown-linux-gnu.tar.gz.sha256 \ + dist/install.sh rm release_notes.tmp preview-release-notes: #!/usr/bin/env bash - VERSION=$(hatch version) + VERSION=$(cat version) ./scripts/get_release_notes.py "$VERSION" | less -R release: create-release - diff --git a/krayt-squooshed.png b/krayt-squooshed.png deleted file mode 100644 index c3f6c5c..0000000 Binary files a/krayt-squooshed.png and /dev/null differ diff --git a/krayt.png b/krayt.png deleted file mode 100644 index eb181a0..0000000 Binary files a/krayt.png and /dev/null differ diff --git a/krayt1.py b/krayt.py similarity index 50% rename from krayt1.py rename to krayt.py index d602a34..566f3e5 100755 --- a/krayt1.py +++ b/krayt.py @@ -13,6 +13,13 @@ Krayt - The Kubernetes Volume Inspector Like cracking open a Krayt dragon pearl, this tool helps you inspect what's inside your Kubernetes volumes. Hunt down storage issues and explore your persistent data like a true Tatooine dragon hunter. +Features: +- Create inspector pods with all the tools you need +- Access volumes and device mounts from any pod +- Fuzzy search across all namespaces +- Built-in tools for file exploration and analysis +- Automatic cleanup of inspector pods + May the Force be with your volumes! """ @@ -20,7 +27,6 @@ from iterfzf import iterfzf from kubernetes import client, config import logging import os -from pathlib import Path import time import typer from typing import Any, Optional @@ -100,57 +106,47 @@ def fuzzy_select(items): 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 as "namespace/name" for display + formatted_items = [f"{ns}/{name}" for name, ns in items] + logging.debug(f"Found {len(formatted_items)} pods") - # Format items for display - formatted_items = [f"{name} ({namespace})" for name, namespace in items] - - # Use fzf for selection try: + # Use iterfzf for selection selected = iterfzf(formatted_items) - if not selected: + + if selected: + namespace, name = selected.split("/") + logging.debug(f"Selected pod {name} in namespace {namespace}") + return name, namespace + else: + logging.debug("No selection made") return None, None - # 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 + logging.error(f"Error during selection: {e}", exc_info=True) + typer.echo(f"Error during selection: {e}", err=True) + raise typer.Exit(1) -def get_pods( - namespace=None, - label_selector: str = "app=krayt", -): +def get_pods(namespace=None): """Get list of pods in the specified namespace or all namespaces""" + config.load_kube_config() + v1 = client.CoreV1Api() + try: - config.load_kube_config() - api = client.CoreV1Api() if namespace: - pods = api.list_namespaced_pod( - namespace=namespace, - label_selector=label_selector, - ) + logging.debug(f"Listing pods in namespace {namespace}") + pod_list = v1.list_namespaced_pod(namespace=namespace) else: - pods = api.list_pod_for_all_namespaces( - label_selector=label_selector, - ) + logging.debug("Listing pods in all namespaces") + pod_list = v1.list_pod_for_all_namespaces() - # 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}") + pods = [(pod.metadata.name, pod.metadata.namespace) for pod in pod_list.items] + logging.debug(f"Found {len(pods)} pods") + return pods + except client.exceptions.ApiException as e: + logging.error(f"Error listing pods: {e}") + typer.echo(f"Error listing pods: {e}", err=True) raise typer.Exit(1) @@ -209,141 +205,57 @@ def get_pod_volumes_and_mounts(pod_spec): return volume_mounts, volumes -def get_env_vars_and_secret_volumes(api, namespace: str): - """Get environment variables and secret volumes for the inspector pod""" +def get_pod_env_and_secrets(api, namespace, pod_name): + pod = api.read_namespaced_pod(pod_name, namespace) + + # Get environment variables from the pod env_vars = [] - volumes = [] + for container in pod.spec.containers: + if container.env: + for env in container.env: + env_dict = {"name": env.name} + if env.value: + env_dict["value"] = env.value + elif env.value_from: + if env.value_from.config_map_key_ref: + env_dict["valueFrom"] = { + "configMapKeyRef": { + "name": env.value_from.config_map_key_ref.name, + "key": env.value_from.config_map_key_ref.key, + } + } + elif env.value_from.secret_key_ref: + env_dict["valueFrom"] = { + "secretKeyRef": { + "name": env.value_from.secret_key_ref.name, + "key": env.value_from.secret_key_ref.key, + } + } + elif env.value_from.field_ref: + env_dict["valueFrom"] = { + "fieldRef": { + "fieldPath": env.value_from.field_ref.field_path + } + } + env_vars.append(env_dict) - # 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", - ] + # Get all volume mounts that are secrets + secret_volumes = [] + if pod.spec.volumes: + secret_volumes = [v for v in pod.spec.volumes if v.secret] - for var in proxy_vars: - if var in os.environ: - env_vars.append({"name": var, "value": os.environ[var]}) - - # Look for secret volumes in the namespace - try: - secrets = api.list_namespaced_secret(namespace) - for secret in secrets.items: - # Skip service account tokens and other system secrets - if secret.type != "Opaque" or secret.metadata.name.startswith( - "default-token-" - ): - continue - - # Mount each secret as a volume - volume_name = f"secret-{secret.metadata.name}" - volumes.append( - client.V1Volume( - name=volume_name, - secret=client.V1SecretVolumeSource( - secret_name=secret.metadata.name - ), - ) - ) - - except client.exceptions.ApiException as e: - if e.status != 404: # Ignore if no secrets found - logging.warning(f"Failed to list secrets in namespace {namespace}: {e}") - - return env_vars, volumes - - -def get_init_scripts(): - """Get the contents of init scripts to be run in the pod""" - init_dir = Path.home() / ".config" / "krayt" / "init.d" - if not init_dir.exists(): - logging.debug("No init.d directory found at %s", init_dir) - return "" - - scripts = sorted(init_dir.glob("*.sh")) - if not scripts: - logging.debug("No init scripts found in %s", init_dir) - return "" - - # Create a combined script that will run all init scripts - init_script = "#!/bin/bash\n\n" - init_script += "exec 2>&1 # Redirect stderr to stdout for proper logging\n" - init_script += "set -e # Exit on error\n\n" - init_script += "echo 'Running initialization scripts...' | tee /tmp/init.log\n\n" - init_script += "mkdir -p /tmp/init.d\n\n" # Create directory once at the start - - for script in scripts: - try: - with open(script, "r") as f: - script_content = f.read() - if not script_content.strip(): - logging.debug("Skipping empty script %s", script) - continue - - # Use a unique heredoc delimiter for each script to avoid nesting issues - delimiter = f"EOF_SCRIPT_{script.stem.upper()}" - - init_script += f"echo '=== Running {script.name} ===' | tee -a /tmp/init.log\n" - init_script += f"cat > /tmp/init.d/{script.name} << '{delimiter}'\n" - init_script += script_content - if not script_content.endswith("\n"): - init_script += "\n" - init_script += f"{delimiter}\n" - init_script += f"chmod +x /tmp/init.d/{script.name}\n" - init_script += f"cd /tmp/init.d && ./{script.name} 2>&1 | tee -a /tmp/init.log || {{ echo \"Failed to run {script.name}\"; exit 1; }}\n" - init_script += f"echo '=== Finished {script.name} ===' | tee -a /tmp/init.log\n\n" - except Exception as e: - logging.error(f"Failed to load init script {script}: {e}") - - init_script += "echo 'Initialization scripts complete.' | tee -a /tmp/init.log\n" - return init_script - - -def get_motd_script(mount_info, pvc_info): - """Generate the MOTD script with proper escaping""" - return f""" -# Create MOTD -cat << EOF > /etc/motd -==================================== -Krayt Dragon's Lair -A safe haven for volume inspection -==================================== - -"Inside every volume lies a pearl of wisdom waiting to be discovered." - -Mounted Volumes: -$(echo "{",".join(mount_info)}" | tr ',' '\\n' | sed 's/^/- /') - -Persistent Volume Claims: -$(echo "{",".join(pvc_info)}" | tr ',' '\\n' | sed 's/^/- /') - -Mounted Secrets: -$(for d in /mnt/secrets/*; do if [ -d "$d" ]; then echo "- $(basename $d)"; fi; done) - -Init Script Status: -$(if [ -f /tmp/init.log ]; then echo "View initialization log at /tmp/init.log"; fi) -EOF -""" + return env_vars, secret_volumes def create_inspector_job( - api, - namespace: str, - pod_name: str, - volume_mounts: list, - volumes: list, - image: str = "alpine:latest", - imagepullsecret: Optional[str] = None, + api, namespace: str, pod_name: str, volume_mounts: list, volumes: list ): """Create a Krayt inspector job with the given mounts""" timestamp = int(time.time()) job_name = f"{pod_name}-krayt-{timestamp}" - # Get environment variables and secret volumes from the target pod - env_vars, secret_volumes = get_env_vars_and_secret_volumes(api, namespace) + # Get environment variables and secrets from the target pod + env_vars, secret_volumes = get_pod_env_and_secrets(api, namespace, pod_name) # Add secret volumes to our volumes list volumes.extend(secret_volumes) @@ -374,142 +286,6 @@ def create_inspector_job( if hasattr(v, "persistent_volume_claim") and v.persistent_volume_claim: pvc_info.append(f"{v.name}:{v.persistent_volume_claim.claim_name}") - init_scripts = get_init_scripts() - - # Build the command script - command_parts = [] - - # Configure apk proxy settings BEFORE any package installation - command_parts.extend( - [ - "# Configure apk proxy settings", - "mkdir -p /etc/apk", - "cat > /etc/apk/repositories << 'EOF'", - "https://dl-cdn.alpinelinux.org/alpine/latest-stable/main", - "https://dl-cdn.alpinelinux.org/alpine/latest-stable/community", - "EOF", - "", - 'if [ ! -z "$HTTP_PROXY" ]; then', - ' echo "Setting up apk proxy configuration..."', - " mkdir -p /etc/apk/", - " cat > /etc/apk/repositories << EOF", - "#/media/cdrom/apks", - "http://dl-cdn.alpinelinux.org/alpine/latest-stable/main", - "http://dl-cdn.alpinelinux.org/alpine/latest-stable/community", - "", - "# Configure proxy", - "proxy=$HTTP_PROXY", - "EOF", - "fi", - "", - "# Install basic tools first", - "apk update", - "apk add curl", - "", - "# Install uv CLI", - "echo 'Installing uv CLI...'", - "curl -LsSf https://astral.sh/uv/install.sh | sh", - "echo 'uv version:'", - "uv --version", - "", - "echo 'Installing starship...'", - "curl -sS https://starship.rs/install.sh | sh -s -- -y", - "echo 'starship version:'", - "starship --version", - "", - "", - "# Install additional tools", - "apk add " - + " ".join( - [ - "ripgrep", - "exa", - "ncdu", - "dust", - "file", - "hexyl", - "jq", - "yq", - "bat", - "fd", - "fzf", - "htop", - "bottom", - "difftastic", - "mtr", - "bind-tools", - "aws-cli", - "sqlite", - "sqlite-dev", - "sqlite-libs", - "bash", - "neovim", - ] - ), - "", - ] - ) - - # Add init scripts if present - if init_scripts: - command_parts.extend( - [ - "# Set up init script environment", - "mkdir -p /tmp/init.d", - "", - "# Write and run init scripts", - "cat > /tmp/init.sh << 'EOFSCRIPT'", - init_scripts, - "EOFSCRIPT", - "", - "# Make init script executable and run it", - "chmod +x /tmp/init.sh", - "bash /tmp/init.sh", - "", - ] - ) - - # Add shell setup and MOTD - command_parts.extend( - [ - "# Create .ashrc with MOTD", - "cat > /root/.ashrc << 'EOF'", - "# Display MOTD on login", - "[ -f /etc/motd ] && cat /etc/motd", - "# Set up shell environment", - "export EDITOR=vi", - "export PAGER=less", - "# Set up aliases", - "alias ll='ls -la'", - "alias l='ls -la'", - "alias la='ls -la'", - "alias vi='vim'", - "# Set up PATH", - "export PATH=/root/.local/bin:$PATH", - 'eval "$(starship init bash)"', - "EOF", - "", - "", - "# Set up environment to always source our RC file", - "echo 'export ENV=/root/.ashrc' > /etc/profile", - "echo 'export ENV=/root/.ashrc' > /etc/environment", - "", - "# Make RC file available to all shells", - "mkdir -p /etc/profile.d", - "cp /root/.ashrc /etc/profile.d/motd.sh", - "ln -sf /root/.ashrc /root/.profile", - "ln -sf /root/.ashrc /root/.bashrc", - "ln -sf /root/.ashrc /root/.mkshrc", - "ln -sf /root/.ashrc /etc/shinit", - "", - "# Update MOTD", - get_motd_script(mount_info, pvc_info), - "", - "# Keep container running", - "tail -f /dev/null", - ] - ) - inspector_job = { "apiVersion": "batch/v1", "kind": "Job", @@ -517,23 +293,187 @@ def create_inspector_job( "name": job_name, "namespace": namespace, "labels": {"app": "krayt"}, - "annotations": {"pvcs": ",".join(pvc_info) if pvc_info else "none"}, }, "spec": { + "ttlSecondsAfterFinished": 0, # Delete immediately after completion "template": { "metadata": {"labels": {"app": "krayt"}}, "spec": { "containers": [ { - "name": "inspector", - "image": image, - "command": ["sh", "-c", "\n".join(command_parts)], - "env": env_vars, + "name": "krayt", + "image": "alpine:latest", # Use Alpine as base for package management + "command": [ + "sh", + "-c", + """ +# Install basic tools first +apk update +apk add curl + +# Install lf (terminal file manager) +curl -L https://github.com/gokcehan/lf/releases/download/r31/lf-linux-amd64.tar.gz | tar xzf - -C /usr/local/bin + +# Install the rest of the tools +apk add ripgrep exa ncdu dust \ + file hexyl jq yq bat fd fzf \ + htop bottom difftastic \ + mtr bind-tools \ + aws-cli sqlite sqlite-dev sqlite-libs + +# Function to update MOTD +update_motd() { + cat << EOF > /etc/motd +==================================== +Krayt Dragon's Lair +==================================== +"Inside every volume lies a pearl of wisdom waiting to be discovered." + +Mounted Volumes: +$(echo "$MOUNTS" | tr ',' '\\n' | sed 's/^/- /') + +Persistent Volume Claims: +$(echo "$PVCS" | tr ',' '\\n' | sed 's/^/- /') + +Mounted Secrets: +$(for d in /mnt/secrets/*; do if [ -d "$d" ]; then echo "- $(basename $d)"; fi; done) + +Environment Variables: +$(env | sort | sed 's/^/- /') + +Your Hunting Tools: +File Navigation: +- lf: Terminal file manager (run 'lf') +- exa: Modern ls (run 'ls', 'll', or 'tree') +- fd: Modern find (run 'fd pattern') + +Search & Analysis: +- rg (ripgrep): Fast search (run 'rg pattern') +- bat: Better cat with syntax highlighting +- hexyl: Hex viewer (run 'hexyl file') +- file: File type detection + +Disk Usage: +- ncdu: Interactive disk usage analyzer +- dust: Disk usage analyzer +- du: Standard disk usage tool + +File Comparison: +- difft: Modern diff tool (alias 'diff') + +System Monitoring: +- btm: Modern system monitor (alias 'top') +- htop: Interactive process viewer + +JSON/YAML Tools: +- jq: JSON processor +- yq: YAML processor + +Network Tools: +- dig: DNS lookup +- mtr: Network diagnostics + +Cloud & Database: +- aws: AWS CLI +- sqlite3: SQLite database tool + +Type 'tools-help' for detailed usage information +==================================== +EOF +} + +# Create helpful aliases and functions +cat << 'EOF' > /root/.ashrc +if [ "$PS1" ]; then + cat /etc/motd +fi + +# Aliases for better file navigation +alias ls='exa' +alias ll='exa -l' +alias la='exa -la' +alias tree='exa --tree' +alias find='fd' +alias top='btm' +alias diff='difft' +alias cat='bat --paging=never' + +# Function to show detailed tool help +tools-help() { + echo "Krayt Dragon Hunter's Guide:" + echo + echo "File Navigation:" + echo " lf : Navigate with arrow keys, q to quit, h for help" + echo " ls, ll, la : List files (exa with different options)" + echo " tree : Show directory structure" + echo " fd pattern : Find files matching pattern" + echo + echo "Search & Analysis:" + echo " rg pattern : Search file contents" + echo " bat file : View file with syntax highlighting" + echo " hexyl file : View file in hex format" + echo " file path : Determine file type" + echo + echo "Disk Usage:" + echo " ncdu : Interactive disk usage analyzer (navigate with arrows)" + echo " dust path : Tree-based disk usage" + echo " du -sh * : Summarize disk usage" + echo + echo "File Comparison:" + echo " diff file1 file2 : Compare files with syntax highlighting" + echo + echo "System Monitoring:" + echo " top (btm) : Modern system monitor" + echo " htop : Process viewer" + echo + echo "JSON/YAML Tools:" + echo " jq . file.json : Format and query JSON" + echo " yq . file.yaml : Format and query YAML" + echo + echo "Network Tools:" + echo " dig domain : DNS lookup" + echo " mtr host : Network diagnostics" + echo + echo "Cloud & Database:" + echo " aws : AWS CLI tool" + echo " sqlite3 : SQLite database tool" + echo + echo "Secrets:" + echo " ls /mnt/secrets : List mounted secrets" +} + +# Set some helpful environment variables +export EDITOR=vi +export PAGER=less +EOF + +# Set up environment to always source our RC file +echo "export ENV=/root/.ashrc" > /etc/profile +echo "export ENV=/root/.ashrc" > /etc/environment + +# Make RC file available to all shells +cp /root/.ashrc /etc/profile.d/motd.sh +ln -sf /root/.ashrc /root/.profile +ln -sf /root/.ashrc /root/.bashrc +ln -sf /root/.ashrc /root/.mkshrc +ln -sf /root/.ashrc /etc/shinit + +# Create initial MOTD +update_motd + +sleep 3600 + """, + ], + "env": env_vars + + [ + {"name": "MOUNTS", "value": ",".join(mount_info)}, + {"name": "PVCS", "value": ",".join(pvc_info)}, + {"name": "ENV", "value": "/root/.ashrc"}, + ], "volumeMounts": formatted_mounts, } ], "volumes": [format_volume(v) for v in volumes if format_volume(v)], - "imagePullSecrets": [{"name": imagepullsecret}] if imagepullsecret else None, "restartPolicy": "Never", }, }, @@ -559,43 +499,6 @@ PROTECTED_NAMESPACES = { } -def load_init_scripts(): - """Load and execute initialization scripts from ~/.config/krayt/scripts/""" - init_dir = Path.home() / ".config" / "krayt" / "scripts" - if not init_dir.exists(): - return - - # Sort scripts to ensure consistent execution order - scripts = sorted(init_dir.glob("*.py")) - - for script in scripts: - try: - with open(script, "r") as f: - exec(f.read(), globals()) - logging.debug(f"Loaded init script: {script}") - except Exception as e: - logging.error(f"Failed to load init script {script}: {e}") - - -def setup_environment(): - """Set up the environment with proxy settings and other configurations""" - # Load environment variables for proxies - proxy_vars = [ - "HTTP_PROXY", - "HTTPS_PROXY", - "NO_PROXY", - "http_proxy", - "https_proxy", - "no_proxy", - ] - - for var in proxy_vars: - if var in os.environ: - # Make both upper and lower case versions available - os.environ[var.upper()] = os.environ[var] - os.environ[var.lower()] = os.environ[var] - - def version_callback(value: bool): if value: typer.echo(f"Version: {KRAYT_VERSION}") @@ -666,19 +569,12 @@ def exec( typer.echo("No inspector selected.") raise typer.Exit(1) - exec_command = [ + # Execute the shell + typer.echo(f"Connecting to inspector {pod_namespace}/{pod_name}...") + os.execvp( "kubectl", - "exec", - "-it", - "-n", - pod_namespace, - pod_name, - "--", - "/bin/bash", - "-l", - ] - - os.execvp("kubectl", exec_command) + ["kubectl", "exec", "-it", "-n", pod_namespace, pod_name, "--", "sh", "-l"], + ) except client.exceptions.ApiException as e: logging.error(f"Failed to list jobs: {e}") @@ -773,25 +669,13 @@ def create( None, help="Kubernetes namespace. If not specified, will search for pods across all namespaces.", ), - image: str = typer.Option( - "alpine:latest", - "--image", - "-i", - help="Container image to use for the inspector pod", - ), - imagepullsecret: Optional[str] = typer.Option( - None, - "--imagepullsecret", - help="Name of the image pull secret to use for pulling private images", - ), ): """ Krack open a Krayt dragon! Create an inspector pod to explore what's inside your volumes. If namespace is not specified, will search for pods across all namespaces. The inspector will be created in the same namespace as the selected pod. """ - # For create, we want to list all pods, not just Krayt pods - pods = get_pods(namespace, label_selector=None) + pods = get_pods(namespace) if not pods: typer.echo("No pods found.") raise typer.Exit(1) @@ -805,13 +689,7 @@ def create( 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, + client.CoreV1Api(), selected_namespace, selected_pod, volume_mounts, volumes ) # Output the job manifest @@ -824,59 +702,5 @@ def version(): typer.echo(f"Version: {KRAYT_VERSION}") -@app.command() -def logs( - namespace: Optional[str] = typer.Option( - None, - help="Kubernetes namespace. If not specified, will search for inspectors across all namespaces.", - ), - follow: bool = typer.Option( - False, - "--follow", - "-f", - help="Follow the logs in real-time", - ), -): - """ - View logs from a Krayt inspector pod. - If multiple inspectors are found, you'll get to choose which one to explore. - """ - pods = get_pods(namespace) - if not pods: - typer.echo("No pods found.") - raise typer.Exit(1) - - selected_pod, selected_namespace = fuzzy_select(pods) - if not selected_pod: - typer.echo("No pod selected.") - raise typer.Exit(1) - - try: - config.load_kube_config() - api = client.CoreV1Api() - logs = api.read_namespaced_pod_log( - name=selected_pod, - namespace=selected_namespace, - follow=follow, - _preload_content=False, - ) - - if follow: - for line in logs: - typer.echo(line.decode("utf-8").strip()) - else: - typer.echo(logs.read().decode("utf-8")) - - except client.rest.ApiException as e: - typer.echo(f"Error reading logs: {e}") - raise typer.Exit(1) - - -def main(): - setup_environment() - load_init_scripts() - app() - - if __name__ == "__main__": - main() + app() diff --git a/krayt.webp b/krayt.webp deleted file mode 100644 index 92d165f..0000000 Binary files a/krayt.webp and /dev/null differ diff --git a/krayt/__about__.py b/krayt/__about__.py deleted file mode 100644 index f6b7e26..0000000 --- a/krayt/__about__.py +++ /dev/null @@ -1 +0,0 @@ -__version__ = "0.4.3" diff --git a/krayt/__init__.py b/krayt/__init__.py deleted file mode 100644 index 236e53a..0000000 --- a/krayt/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -from krayt.__about__ import __version__ - -__all__ = [ - "__version__", -] diff --git a/krayt/bundles.py b/krayt/bundles.py deleted file mode 100644 index cecc8d9..0000000 --- a/krayt/bundles.py +++ /dev/null @@ -1,88 +0,0 @@ -""" -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"], - ] - ) -) diff --git a/krayt/cli/__init__.py b/krayt/cli/__init__.py deleted file mode 100644 index 035a6de..0000000 --- a/krayt/cli/__init__.py +++ /dev/null @@ -1,25 +0,0 @@ -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() diff --git a/krayt/cli/bundles.py b/krayt/cli/bundles.py deleted file mode 100644 index be30fe8..0000000 --- a/krayt/cli/bundles.py +++ /dev/null @@ -1,25 +0,0 @@ -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}") diff --git a/krayt/cli/pod.py b/krayt/cli/pod.py deleted file mode 100644 index d8ac87a..0000000 --- a/krayt/cli/pod.py +++ /dev/null @@ -1,925 +0,0 @@ -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() diff --git a/krayt/cli/templates.py b/krayt/cli/templates.py deleted file mode 100644 index 7416db6..0000000 --- a/krayt/cli/templates.py +++ /dev/null @@ -1,98 +0,0 @@ -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) diff --git a/krayt/package.py b/krayt/package.py deleted file mode 100644 index 907a412..0000000 --- a/krayt/package.py +++ /dev/null @@ -1,191 +0,0 @@ -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)) diff --git a/krayt/templates.py b/krayt/templates.py deleted file mode 100644 index 7b3ba95..0000000 --- a/krayt/templates.py +++ /dev/null @@ -1,13 +0,0 @@ -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 diff --git a/krayt/templates/.kraytrc b/krayt/templates/.kraytrc deleted file mode 100644 index 591b80e..0000000 --- a/krayt/templates/.kraytrc +++ /dev/null @@ -1,3 +0,0 @@ -if [ -t 1 ] && [ -f /etc/motd ]; then - cat /etc/motd -fi diff --git a/krayt/templates/base.sh b/krayt/templates/base.sh deleted file mode 100644 index c248460..0000000 --- a/krayt/templates/base.sh +++ /dev/null @@ -1,25 +0,0 @@ -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 diff --git a/krayt/templates/install.sh b/krayt/templates/install.sh deleted file mode 100644 index de056ad..0000000 --- a/krayt/templates/install.sh +++ /dev/null @@ -1,88 +0,0 @@ -{% 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 [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 [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 %} - diff --git a/krayt/templates/kraytrc.sh b/krayt/templates/kraytrc.sh deleted file mode 100644 index dd4fe85..0000000 --- a/krayt/templates/kraytrc.sh +++ /dev/null @@ -1,116 +0,0 @@ -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 </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 diff --git a/krayt/templates/motd.sh b/krayt/templates/motd.sh deleted file mode 100644 index 5838161..0000000 --- a/krayt/templates/motd.sh +++ /dev/null @@ -1,40 +0,0 @@ -cat </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 diff --git a/krayt2.py b/krayt2.py deleted file mode 100755 index e79d280..0000000 --- a/krayt2.py +++ /dev/null @@ -1,337 +0,0 @@ -#!/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 [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() diff --git a/pyproject.toml b/pyproject.toml deleted file mode 100644 index 5ef96cd..0000000 --- a/pyproject.toml +++ /dev/null @@ -1,70 +0,0 @@ -[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'] diff --git a/scripts/get_release_notes.py b/scripts/get_release_notes.py index f564136..d782a15 100755 --- a/scripts/get_release_notes.py +++ b/scripts/get_release_notes.py @@ -21,11 +21,7 @@ def get_release_notes(version): You can install krayt using one of these methods: -## pypi - -``` bash -pip install krayt -``` +> !krayt requires [uv](https://docs.astral.sh/uv/getting-started/installation/) to be installed ### Using i.jpillora.com (recommended) @@ -41,8 +37,8 @@ curl -fsSL https://github.com/waylonwalker/krayt/releases/download/v{version}/in ### Manual download You can also manually download the archive for your platform from the releases page: -- [x86_64-unknown-linux-gnu](https://github.com/waylonwalker/krayt/releases/download/v{version}/krayt-{version}-x86_64-unknown-linux-gnu) -- [aarch64-unknown-linux-gnu](https://github.com/waylonwalker/krayt/releases/download/v{version}/krayt-{version}-aarch64-unknown-linux-gnu)""" +- [x86_64-unknown-linux-gnu](https://github.com/waylonwalker/krayt/releases/download/v{version}/krayt-{version}-x86_64-unknown-linux-gnu.tar.gz) +- [aarch64-unknown-linux-gnu](https://github.com/waylonwalker/krayt/releases/download/v{version}/krayt-{version}-aarch64-unknown-linux-gnu.tar.gz)""" # Get help output for main command and all subcommands try: @@ -50,23 +46,17 @@ You can also manually download the archive for your platform from the releases p # Get main help output main_help = subprocess.check_output( - ["krayt", "--help"], + ["./krayt.py", "--help"], stderr=subprocess.STDOUT, universal_newlines=True, ) help_outputs.append(("Main Command", main_help)) # Get help for each subcommand - subcommands = [ - "create", - "exec", - "clean", - "version", - "pod", - ] + subcommands = ["create", "exec", "clean", "version"] for cmd in subcommands: cmd_help = subprocess.check_output( - ["krayt", cmd, "--help"], + ["./krayt.py", cmd, "--help"], stderr=subprocess.STDOUT, universal_newlines=True, ) diff --git a/scripts/install.sh.template b/scripts/install.sh.template index 8e930c9..907cc23 100644 --- a/scripts/install.sh.template +++ b/scripts/install.sh.template @@ -13,7 +13,7 @@ function fail { echo "Error: $msg" 1>&2 exit 1 } -function check_uv { +function check_deps { if ! command -v uv &>/dev/null; then echo " Error: uv is not installed" echo "krayt requires uv to run. You can install it with:" @@ -24,67 +24,15 @@ function check_uv { fail "uv not found" fi } - -function setup_config_dir { - # Create config directory - CONFIG_DIR="${HOME}/.config/krayt" - mkdir -p "${CONFIG_DIR}/init.d" - - # Create example init script if it doesn't exist - EXAMPLE_SCRIPT="${CONFIG_DIR}/init.d/00_proxy.sh.example" - if [ ! -f "$EXAMPLE_SCRIPT" ]; then - cat > "$EXAMPLE_SCRIPT" << 'EOF' -#!/bin/sh -# Example initialization script for Krayt inspector pods -# This script runs before any packages are installed -# To use this script, rename it to remove the .example extension - -# Example: Set up proxy configuration -setup_proxy() { - # Uncomment and modify these lines to set up your proxy - # export HTTP_PROXY="http://proxy.example.com:8080" - # export HTTPS_PROXY="http://proxy.example.com:8080" - # export NO_PROXY="localhost,127.0.0.1,.example.com" - - # Set up proxy for apk if needed - if [ ! -z "$HTTP_PROXY" ]; then - echo "proxy = $HTTP_PROXY" >> /etc/apk/repositories - fi -} - -# Example: Add custom Alpine repositories -setup_repos() { - # Uncomment to add custom repos - # echo "http://dl-cdn.alpinelinux.org/alpine/edge/testing" >> /etc/apk/repositories - : -} - -# Run the setup functions -setup_proxy -setup_repos - -# Log the configuration -echo "Krayt inspector pod initialization complete" -echo "Proxy settings:" -echo "HTTP_PROXY=$HTTP_PROXY" -echo "HTTPS_PROXY=$HTTPS_PROXY" -echo "NO_PROXY=$NO_PROXY" -EOF - fi - - echo "Created config directory at ${CONFIG_DIR}" - echo "Example init script created at ${EXAMPLE_SCRIPT}" -} - function install { #settings USER="waylonwalker" PROG="krayt" ASPROG="krayt" - MOVE="false" + MOVE="true" RELEASE="{{VERSION}}" INSECURE="false" - OUT_DIR="$(pwd)" + OUT_DIR="/usr/local/bin" GH="https://github.com" #bash check [ ! "$BASH_VERSION" ] && fail "Please use bash instead" @@ -109,126 +57,70 @@ function install { else fail "neither wget/curl are installed" fi - #debug HTTP - if [ "$DEBUG" == "1" ]; then - GET="$GET -v" - fi - #optional auth to install from private repos - #NOTE: this also needs to be set on your instance of installer - AUTH="${GITHUB_TOKEN}" - if [ ! -z "$AUTH" ]; then - GET="$GET -H 'Authorization: $AUTH'" - fi - #find OS #TODO BSDs and other posixs + #find OS case $(uname -s) in Darwin) OS="darwin" ;; Linux) OS="linux" ;; *) fail "unknown os: $(uname -s)" ;; esac #find ARCH - if uname -m | grep -E '(arm|arch)64' >/dev/null; then - ARCH="arm64" - - # no m1 assets. if on mac arm64, rosetta allows fallback to amd64 - if [[ $OS = "darwin" ]]; then - ARCH="amd64" - fi - + if uname -m | grep -E '(arm|aarch)64' >/dev/null; then + ARCH="aarch64" elif uname -m | grep 64 >/dev/null; then - ARCH="amd64" - elif uname -m | grep arm >/dev/null; then - ARCH="arm" #TODO armv6/v7 - elif uname -m | grep 386 >/dev/null; then - ARCH="386" + ARCH="x86_64" else fail "unknown arch: $(uname -m)" fi #choose from asset list URL="" FTYPE="" - case "${OS}_${ARCH}" in - "linux_amd64") - URL="https://github.com/WaylonWalker/krayt/releases/download/v${RELEASE}/krayt-${RELEASE}-x86_64-unknown-linux-gnu.tar.gz" - FTYPE=".tar.gz" - ;; - "linux_arm64") - URL="https://github.com/WaylonWalker/krayt/releases/download/v${RELEASE}/krayt-${RELEASE}-aarch64-unknown-linux-gnu.tar.gz" - FTYPE=".tar.gz" - ;; - *) fail "No asset for platform ${OS}-${ARCH}" ;; - esac - #got URL! download it... - echo -n "Downloading" - echo -n " $USER/$PROG" - if [ ! -z "$RELEASE" ]; then - echo -n " $RELEASE" + VERSION=${RELEASE#v} + if [[ $VERSION == "" ]]; then + VERSION=$(curl -s https://api.github.com/repos/$USER/$PROG/releases/latest | grep -o '"tag_name": "[^"]*' | cut -d'"' -f4) fi - if [ ! -z "$ASPROG" ]; then - echo -n " as $ASPROG" + if [[ $VERSION == "" ]]; then + fail "cannot find latest version" fi - echo -n " (${OS}/${ARCH})" - - echo "....." - + VERSION=${VERSION#v} + ASSET_URL="$GH/$USER/$PROG/releases/download/v$VERSION/${PROG}-${VERSION}-${ARCH}-unknown-${OS}-gnu.tar.gz" + echo "Installing $PROG v$VERSION..." + echo "Downloading binary from $ASSET_URL" #enter tempdir - mkdir -p $TMP_DIR cd $TMP_DIR - if [[ $FTYPE = ".gz" ]]; then - which gzip >/dev/null || fail "gzip is not installed" - bash -c "$GET $URL" | gzip -d - >$PROG || fail "download failed" - elif [[ $FTYPE = ".bz2" ]]; then - which bzip2 >/dev/null || fail "bzip2 is not installed" - bash -c "$GET $URL" | bzip2 -d - >$PROG || fail "download failed" - elif [[ $FTYPE = ".tar.bz" ]] || [[ $FTYPE = ".tar.bz2" ]]; then - which tar >/dev/null || fail "tar is not installed" - which bzip2 >/dev/null || fail "bzip2 is not installed" - bash -c "$GET $URL" | tar jxf - || fail "download failed" - elif [[ $FTYPE = ".tar.gz" ]] || [[ $FTYPE = ".tgz" ]]; then - which tar >/dev/null || fail "tar is not installed" - which gzip >/dev/null || fail "gzip is not installed" - bash -c "$GET $URL" | tar zxf - || fail "download failed" - elif [[ $FTYPE = ".zip" ]]; then - which unzip >/dev/null || fail "unzip is not installed" - bash -c "$GET $URL" >tmp.zip || fail "download failed" - unzip -o -qq tmp.zip || fail "unzip failed" - rm tmp.zip || fail "cleanup failed" - elif [[ $FTYPE = ".bin" ]]; then - bash -c "$GET $URL" >"krayt_${OS}_${ARCH}" || fail "download failed" - else - fail "unknown file type: $FTYPE" - fi - #search subtree largest file (bin) - TMP_BIN=$(find . -type f | xargs du | sort -n | tail -n 1 | cut -f 2) - if [ ! -f "$TMP_BIN" ]; then - fail "could not find find binary (largest file)" - fi - #ensure its larger than 1MB - #TODO linux=elf/darwin=macho file detection? - if [[ $(du -m $TMP_BIN | cut -f1) -lt 1 ]]; then - fail "no binary found ($TMP_BIN is not larger than 1MB)" - fi - #move into PATH or cwd - chmod +x $TMP_BIN || fail "chmod +x failed" - DEST="$OUT_DIR/$PROG" - if [ ! -z "$ASPROG" ]; then - DEST="$OUT_DIR/$ASPROG" - fi - #move without sudo - OUT=$(mv $TMP_BIN $DEST 2>&1) - STATUS=$? - # failed and string contains "Permission denied" - if [ $STATUS -ne 0 ]; then - if [[ $OUT =~ "Permission denied" ]]; then - echo "mv with sudo..." - sudo mv $TMP_BIN $DEST || fail "sudo mv failed" - else - fail "mv failed ($OUT)" + #download and unpack + if [[ $ASSET_URL =~ \.gz$ ]]; then + which tar >/dev/null || fail "tar not installed" + if [[ $GET =~ ^curl ]]; then + curl -s ${ASSET_URL} | tar zx || fail "download failed" + else + wget -qO- ${ASSET_URL} | tar zx || fail "download failed" fi + else + fail "unknown file type: $ASSET_URL" fi - echo "Downloaded to $DEST" - #done + #check for error + cd ${PROG}-${VERSION}-${ARCH}-unknown-${OS}-gnu + #move binary + if [[ -f "${PROG}.py" ]]; then + chmod +x "${PROG}.py" + if [[ $MOVE == "true" ]]; then + echo "Moving binary to $OUT_DIR/$ASPROG" + # Create a wrapper script to ensure uv is used + cat > "$OUT_DIR/$ASPROG" << EOF +#!/bin/bash +exec uv run --quiet --script "$OUT_DIR/${ASPROG}.py" "\$@" +EOF + chmod +x "$OUT_DIR/$ASPROG" + mv "${PROG}.py" "$OUT_DIR/${ASPROG}.py" || fail "Cannot move binary to $OUT_DIR" + else + echo "Moving binary to $OUT_DIR/${ASPROG}.py" + mv "${PROG}.py" "$OUT_DIR/${ASPROG}.py" || fail "Cannot move binary to $OUT_DIR" + fi + else + fail "cannot find binary" + fi + echo "Installation complete!" cleanup - check_uv } +check_deps install -setup_config_dir diff --git a/test.sh b/test.sh deleted file mode 100644 index 1643cab..0000000 --- a/test.sh +++ /dev/null @@ -1,247 +0,0 @@ -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 [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 [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 </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 </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 diff --git a/test.yaml b/test.yaml deleted file mode 100644 index b2b19a0..0000000 --- a/test.yaml +++ /dev/null @@ -1,206 +0,0 @@ -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 [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 </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 </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 diff --git a/version b/version index 0ea3a94..77d6f4c 100644 --- a/version +++ b/version @@ -1 +1 @@ -0.2.0 +0.0.0