diff --git a/.gitignore b/.gitignore index ec9ade7..466951b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ *.temp info/* +docs/ diff --git a/README.md b/README.md index 3e1e12c..4706762 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,12 @@ # kagglelink -A streamlined solution for accessing Kaggle computational resources via SSH and VS Code, powered by Zrok for secure tunneling. +Turn any Kaggle notebook into an SSH-accessible server. One command. Free GPUs. ## Overview -KaggleLink allows you to connect to Kaggle environments via SSH, enabling you to leverage Kaggle's computational resources +KaggleLink establishes a secure SSH tunnel to your Kaggle notebook, giving you full terminal access to Kaggle's free GPU resources. Use `screen`, `tmux`, run background jobs, transfer files with `rsync`—treat it like your own remote server. + +> **Note:** Kaggle now offers native VS Code Remote support. KaggleLink focuses on **SSH terminal access** for workflows that require a real shell: session persistence, scripting, and direct server management. ![](https://github.com/user-attachments/assets/db4454ff-5545-4094-adeb-47b74ab0c33a) @@ -25,6 +27,8 @@ Execute the following one-line command in a Kaggle notebook cell. This script wi !curl -sS https://bhdai.github.io/setup | bash -s -- -k -t ``` +KaggleLink uses a project-managed pinned Zrok version (`v1.1.11`) during setup for stable Linux amd64 compatibility. + > [!NOTE] > Replace `` with the URL of your public SSH key file and `` with your Zrok token. diff --git a/setup_kaggle_zrok.sh b/setup_kaggle_zrok.sh index 9f61331..8df181e 100644 --- a/setup_kaggle_zrok.sh +++ b/setup_kaggle_zrok.sh @@ -164,28 +164,99 @@ install_packages() { } install_zrok() { - log_step_start "Downloading and installing Zrok" - curl -s https://api.github.com/repos/openziti/zrok/releases/latest | - grep "browser_download_url.*linux_amd64.tar.gz" | - cut -d : -f 2,3 | - tr -d \" | - wget -qi - - - log_info "Extracting Zrok" - if ! tar -xzf zrok_*_linux_amd64.tar.gz -C /usr/local/bin/; then - categorize_error "network" "Failed to extract Zrok" "Check downloaded tar file integrity" + local zrok_version_pin="v1.1.11" + local zrok_release_url="https://api.github.com/repos/openziti/zrok/releases/tags/${zrok_version_pin}" + local zrok_target_path="/usr/local/bin/zrok" + local tmp_dir archive_path extract_dir release_metadata asset_url zrok_binary_path system_arch + local zrok_candidates=() + + log_step_start "Downloading and installing Zrok ${zrok_version_pin}" + + system_arch=$(uname -m) + if [ "$system_arch" != "x86_64" ] && [ "$system_arch" != "amd64" ]; then + categorize_error "upstream" "Unsupported architecture for pinned Zrok asset: ${system_arch}" "Use an amd64 host or update the installer asset selection strategy" + exit 1 + fi + + if ! tmp_dir=$(mktemp -d); then + categorize_error "upstream" "Failed to create temporary directory for Zrok install" "Check filesystem permissions and available disk space" + exit 1 + fi + archive_path="${tmp_dir}/zrok_linux_amd64.tar.gz" + extract_dir="${tmp_dir}/extracted" + + if ! release_metadata=$(curl -fsSL "$zrok_release_url"); then + categorize_error "network" "Failed to fetch Zrok release metadata for ${zrok_version_pin}" "Verify GitHub API connectivity and retry" + rm -rf "$tmp_dir" + exit 1 + fi + + asset_url=$(printf '%s\n' "$release_metadata" | + grep -Eo '"browser_download_url":[[:space:]]*"[^"]+"' | + cut -d '"' -f 4 | + grep -E '/zrok_[^/"]*_linux_amd64\.tar\.gz$' | + head -n 1) + + if [ -z "$asset_url" ]; then + categorize_error "upstream" "Failed to resolve linux amd64 asset for ${zrok_version_pin}" "Check release assets for the pinned tag and update the project pin if needed" + rm -rf "$tmp_dir" + exit 1 + fi + + if [[ "$asset_url" != https://* ]]; then + categorize_error "upstream" "Resolved Zrok asset URL is not HTTPS for ${zrok_version_pin}" "Only HTTPS release assets are allowed" + rm -rf "$tmp_dir" + exit 1 + fi + + if ! wget -qO "$archive_path" "$asset_url"; then + categorize_error "network" "Failed to download Zrok archive for ${zrok_version_pin}" "Check network connectivity and release asset availability" + rm -rf "$tmp_dir" exit 1 fi - rm zrok_*_linux_amd64.tar.gz - # check if zrok is installed correctly - if ! zrok version &>/dev/null; then - categorize_error "upstream" "Zrok install failed" "Try manual installation or check Zrok service" + if ! mkdir -p "$extract_dir"; then + categorize_error "upstream" "Failed to create extraction directory ${extract_dir}" "Check filesystem permissions and available disk space" + rm -rf "$tmp_dir" + exit 1 + fi + + if ! tar -xzf "$archive_path" -C "$extract_dir"; then + categorize_error "upstream" "Failed to extract Zrok archive ${archive_path}" "Check archive integrity and release packaging for ${zrok_version_pin}" + rm -rf "$tmp_dir" + exit 1 + fi + + mapfile -t zrok_candidates < <(find "$extract_dir" -type f -name zrok) + if [ "${#zrok_candidates[@]}" -eq 0 ]; then + categorize_error "upstream" "Failed to locate zrok binary after extracting ${archive_path}" "Verify release layout for ${zrok_version_pin} includes a zrok executable" + rm -rf "$tmp_dir" + exit 1 + fi + + if [ "${#zrok_candidates[@]}" -gt 1 ]; then + categorize_error "upstream" "Multiple zrok binaries found after extracting ${archive_path}" "Update binary selection strategy to match release layout for ${zrok_version_pin}" + rm -rf "$tmp_dir" + exit 1 + fi + + zrok_binary_path="${zrok_candidates[0]}" + + if ! install -m 0755 "$zrok_binary_path" "$zrok_target_path"; then + categorize_error "upstream" "Failed to install zrok binary to ${zrok_target_path}" "Check filesystem permissions and retry setup" + rm -rf "$tmp_dir" + exit 1 + fi + + rm -rf "$tmp_dir" + + if ! "$zrok_target_path" version &>/dev/null; then + categorize_error "upstream" "Post-install validation failed for Zrok ${zrok_version_pin}" "Verify ${zrok_target_path} is executable and rerun setup" exit 1 fi - log_step_complete "Downloading and installing Zrok" - log_success "Zrok version: $(zrok version)" + log_step_complete "Downloading and installing Zrok ${zrok_version_pin}" + log_success "Zrok version: $($zrok_target_path version)" } setup_install_extensions_command() { diff --git a/tests/unit/test_zrok_install.bats b/tests/unit/test_zrok_install.bats new file mode 100644 index 0000000..1e1acbe --- /dev/null +++ b/tests/unit/test_zrok_install.bats @@ -0,0 +1,323 @@ +#!/usr/bin/env bats + +load '../test_helper/common.bash' + +setup() { + create_test_dir + + export ORIGINAL_PATH="$PATH" + export MOCK_BIN_DIR="$TEST_TEMP_DIR/mock-bin" + export MOCK_LOG="$TEST_TEMP_DIR/mock.log" + export MOCK_INSTALLED_BIN="$TEST_TEMP_DIR/installed/zrok" + + mkdir -p "$MOCK_BIN_DIR" + mkdir -p "$(dirname "$MOCK_INSTALLED_BIN")" + + export PATH="$MOCK_BIN_DIR:$ORIGINAL_PATH" + + create_mock_commands + create_install_harness +} + +teardown() { + export PATH="$ORIGINAL_PATH" + cleanup_test_dir +} + +create_mock_commands() { + cat > "$MOCK_BIN_DIR/curl" << 'EOF' +#!/bin/bash + +echo "curl:$*" >> "$MOCK_LOG" + +case "${MOCK_CURL_MODE:-success}" in + fail) + exit 22 + ;; + missing_asset) + cat << 'JSON' +{"assets":[{"browser_download_url":"https://github.com/openziti/zrok/releases/download/v1.1.11/zrok_1.1.11_windows_amd64.zip"}]} +JSON + ;; + non_https_asset) + cat << 'JSON' +{"assets":[{"browser_download_url":"http://example.com/zrok_1.1.11_linux_amd64.tar.gz"}]} +JSON + ;; + *) + cat << 'JSON' +{"assets":[{"browser_download_url":"https://github.com/openziti/zrok/releases/download/v1.1.11/zrok_1.1.11_linux_amd64.tar.gz"}]} +JSON + ;; +esac +EOF + chmod +x "$MOCK_BIN_DIR/curl" + + cat > "$MOCK_BIN_DIR/wget" << 'EOF' +#!/bin/bash + +output_path="" +url="" + +while [ "$#" -gt 0 ]; do + case "$1" in + -qO) + output_path="$2" + shift 2 + ;; + *) + url="$1" + shift + ;; + esac +done + +echo "wget:${url}:${output_path}" >> "$MOCK_LOG" + +if [ "${MOCK_WGET_MODE:-success}" = "fail" ]; then + exit 1 +fi + +: > "$output_path" +exit 0 +EOF + chmod +x "$MOCK_BIN_DIR/wget" + + cat > "$MOCK_BIN_DIR/tar" << 'EOF' +#!/bin/bash + +extract_dir="" + +while [ "$#" -gt 0 ]; do + case "$1" in + -C) + extract_dir="$2" + shift 2 + ;; + *) + shift + ;; + esac +done + +echo "tar:${extract_dir}" >> "$MOCK_LOG" + +case "${MOCK_TAR_MODE:-success}" in + fail) + exit 2 + ;; + multi) + mkdir -p "$extract_dir/release/bin" + mkdir -p "$extract_dir/other/bin" + printf '#!/bin/bash\necho "zrok version v1.1.11"\n' > "$extract_dir/release/bin/zrok" + printf '#!/bin/bash\necho "zrok version v1.1.11"\n' > "$extract_dir/other/bin/zrok" + chmod +x "$extract_dir/release/bin/zrok" + chmod +x "$extract_dir/other/bin/zrok" + ;; + *) + mkdir -p "$extract_dir/release/bin" + printf '#!/bin/bash\necho "zrok version v1.1.11"\n' > "$extract_dir/release/bin/zrok" + chmod +x "$extract_dir/release/bin/zrok" + ;; +esac +EOF + chmod +x "$MOCK_BIN_DIR/tar" + + cat > "$MOCK_BIN_DIR/install" << 'EOF' +#!/bin/bash + +echo "install:$*" >> "$MOCK_LOG" + +if [ "${MOCK_INSTALL_MODE:-success}" = "fail" ]; then + exit 1 +fi + +if [ "$1" = "-m" ]; then + src="$3" + dest="$4" +else + src="$1" + dest="$2" +fi + +mkdir -p "$(dirname "$dest")" + +if [ "${MOCK_ZROK_MODE:-success}" = "fail" ]; then + printf '#!/bin/bash\nexit 1\n' > "$dest" +else + cp "$src" "$dest" +fi + +chmod +x "$dest" + +if [ -n "$MOCK_INSTALLED_BIN" ]; then + cp "$dest" "$MOCK_INSTALLED_BIN" + chmod +x "$MOCK_INSTALLED_BIN" +fi + +exit 0 +EOF + chmod +x "$MOCK_BIN_DIR/install" + + cat > "$MOCK_BIN_DIR/zrok" << 'EOF' +#!/bin/bash + +if [ "${MOCK_ZROK_MODE:-success}" = "fail" ]; then + exit 1 +fi + +if [ "$1" = "version" ]; then + echo "zrok version v1.1.11" +fi + +exit 0 +EOF + chmod +x "$MOCK_BIN_DIR/zrok" +} + +create_install_harness() { + local function_body + function_body=$(awk '/^install_zrok\(\)/,/^}/' "$PROJECT_ROOT/setup_kaggle_zrok.sh") + + cat > "$TEST_TEMP_DIR/install_zrok_harness.sh" << EOF +#!/bin/bash +set -e + +source "$PROJECT_ROOT/logging_utils.sh" + +${function_body} + +install_zrok +EOF + + chmod +x "$TEST_TEMP_DIR/install_zrok_harness.sh" +} + +@test "P0: install_zrok happy path uses pinned tag and installs zrok" { + run env \ + MOCK_CURL_MODE=success \ + MOCK_WGET_MODE=success \ + MOCK_TAR_MODE=success \ + MOCK_INSTALL_MODE=success \ + MOCK_ZROK_MODE=success \ + MOCK_LOG="$MOCK_LOG" \ + MOCK_INSTALLED_BIN="$MOCK_INSTALLED_BIN" \ + PATH="$PATH" \ + bash "$TEST_TEMP_DIR/install_zrok_harness.sh" + + [ "$status" -eq 0 ] + [ -x "$MOCK_INSTALLED_BIN" ] + grep -q "releases/tags/v1.1.11" "$MOCK_LOG" + grep -q "linux_amd64.tar.gz" "$MOCK_LOG" + grep -q "install:-m 0755" "$MOCK_LOG" +} + +@test "P0: install_zrok fails with metadata fetch error when API call fails" { + run env \ + MOCK_CURL_MODE=fail \ + MOCK_LOG="$MOCK_LOG" \ + MOCK_INSTALLED_BIN="$MOCK_INSTALLED_BIN" \ + PATH="$PATH" \ + bash "$TEST_TEMP_DIR/install_zrok_harness.sh" + + [ "$status" -ne 0 ] + [[ "$output" == *"Failed to fetch Zrok release metadata"* ]] + [[ "$output" == *"v1.1.11"* ]] +} + +@test "P0: install_zrok fails when pinned tag has no linux amd64 asset" { + run env \ + MOCK_CURL_MODE=missing_asset \ + MOCK_LOG="$MOCK_LOG" \ + MOCK_INSTALLED_BIN="$MOCK_INSTALLED_BIN" \ + PATH="$PATH" \ + bash "$TEST_TEMP_DIR/install_zrok_harness.sh" + + [ "$status" -ne 0 ] + [[ "$output" == *"Failed to resolve linux amd64 asset"* ]] + [[ "$output" == *"v1.1.11"* ]] +} + +@test "P0: install_zrok fails when resolved asset URL is not HTTPS" { + run env \ + MOCK_CURL_MODE=non_https_asset \ + MOCK_LOG="$MOCK_LOG" \ + MOCK_INSTALLED_BIN="$MOCK_INSTALLED_BIN" \ + PATH="$PATH" \ + bash "$TEST_TEMP_DIR/install_zrok_harness.sh" + + [ "$status" -ne 0 ] + [[ "$output" == *"Resolved Zrok asset URL is not HTTPS"* ]] +} + +@test "P0: install_zrok fails with extraction error when tar extraction fails" { + run env \ + MOCK_CURL_MODE=success \ + MOCK_WGET_MODE=success \ + MOCK_TAR_MODE=fail \ + MOCK_LOG="$MOCK_LOG" \ + MOCK_INSTALLED_BIN="$MOCK_INSTALLED_BIN" \ + PATH="$PATH" \ + bash "$TEST_TEMP_DIR/install_zrok_harness.sh" + + [ "$status" -ne 0 ] + [[ "$output" == *"Failed to extract Zrok archive"* ]] +} + +@test "P0: install_zrok fails when multiple zrok binaries are found" { + run env \ + MOCK_CURL_MODE=success \ + MOCK_WGET_MODE=success \ + MOCK_TAR_MODE=multi \ + MOCK_LOG="$MOCK_LOG" \ + MOCK_INSTALLED_BIN="$MOCK_INSTALLED_BIN" \ + PATH="$PATH" \ + bash "$TEST_TEMP_DIR/install_zrok_harness.sh" + + [ "$status" -ne 0 ] + [[ "$output" == *"Multiple zrok binaries found"* ]] +} + +@test "P0: install_zrok fails with download error when archive fetch fails" { + run env \ + MOCK_CURL_MODE=success \ + MOCK_WGET_MODE=fail \ + MOCK_LOG="$MOCK_LOG" \ + MOCK_INSTALLED_BIN="$MOCK_INSTALLED_BIN" \ + PATH="$PATH" \ + bash "$TEST_TEMP_DIR/install_zrok_harness.sh" + + [ "$status" -ne 0 ] + [[ "$output" == *"Failed to download Zrok archive"* ]] +} + +@test "P0: install_zrok fails when binary install command fails" { + run env \ + MOCK_CURL_MODE=success \ + MOCK_WGET_MODE=success \ + MOCK_TAR_MODE=success \ + MOCK_INSTALL_MODE=fail \ + MOCK_LOG="$MOCK_LOG" \ + MOCK_INSTALLED_BIN="$MOCK_INSTALLED_BIN" \ + PATH="$PATH" \ + bash "$TEST_TEMP_DIR/install_zrok_harness.sh" + + [ "$status" -ne 0 ] + [[ "$output" == *"Failed to install zrok binary"* ]] +} + +@test "P0: install_zrok fails with post-install validation error when zrok version fails" { + run env \ + MOCK_CURL_MODE=success \ + MOCK_WGET_MODE=success \ + MOCK_TAR_MODE=success \ + MOCK_INSTALL_MODE=success \ + MOCK_ZROK_MODE=fail \ + MOCK_LOG="$MOCK_LOG" \ + MOCK_INSTALLED_BIN="$MOCK_INSTALLED_BIN" \ + PATH="$PATH" \ + bash "$TEST_TEMP_DIR/install_zrok_harness.sh" + + [ "$status" -ne 0 ] + [[ "$output" == *"Post-install validation failed"* ]] +}