diff --git a/CHANGELOG.md b/CHANGELOG.md index 5662a14..cf7284a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,11 @@ All notable changes to this project will be documented in this file. This project adheres to Semantic Versioning. +## [Unreleased] +### Added +- `install.sh` bootstrap installer (installs kctl-env into `~/.kctl-env` or `$KCTL_ENV_ROOT`) +- README “Quick install” section + ## [v0.1.0] - 2026-01-07 ### Added - Initial scaffold: bin/kubectl shim with env/.kubectl-version/global/auto resolution and TTL cache @@ -15,3 +20,5 @@ All notable changes to this project will be documented in this file. This projec - Git repository initialized, branch protections configured (squash/rebase, linear history, delete branch on merge) [v0.1.0]: https://github.com/senet/kctl-env/releases/tag/v0.1.0 + +[Unreleased]: https://github.com/senet/kctl-env/compare/v0.1.0...HEAD diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 425b137..f9af004 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -38,6 +38,22 @@ git tag -a v0.1.1 -m "v0.1.1 release" git push origin v0.1.1 ``` +5. Generate and publish checksums for the release: + ```sh + # Generate SHA256 checksum for the source tarball (Linux / GNU coreutils) + curl -fsSL https://github.com/senet/kctl-env/archive/refs/tags/v0.1.1.tar.gz | sha256sum > v0.1.1.tar.gz.sha256 + # On macOS (BSD userland), use shasum instead: + # curl -fsSL https://github.com/senet/kctl-env/archive/refs/tags/v0.1.1.tar.gz | shasum -a 256 > v0.1.1.tar.gz.sha256 + + # Create the GitHub release and attach the checksum asset + # Note: this repo uses immutable releases, so you must attach assets at + # creation time (you can't upload them later). + # Note: GitHub automatically provides the source tarball at: + # https://github.com/senet/kctl-env/archive/refs/tags/v0.1.1.tar.gz + # We only need to upload the checksum file as an additional asset. + gh release create v0.1.1 --title "v0.1.1" --notes "Release v0.1.1" v0.1.1.tar.gz.sha256 + ``` + This enables secure installation with SHA256 verification. ## Branch protections (recommended) diff --git a/README.md b/README.md index 7a57762..b731312 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,29 @@ Pure Bash, zero-deps kubectl version manager with fast shims and tfenv-style UX. ## Installation +### Quick install (recommended) + +Install from the `main` branch (recommended until the next tagged release that includes `install.sh`): + +```sh +curl -fsSL https://raw.githubusercontent.com/senet/kctl-env/main/install.sh | bash -s -- main +``` + +Pin a specific version (recommended for reproducibility and supply-chain security after the next release): + +```sh +curl -fsSL https://raw.githubusercontent.com/senet/kctl-env/vX.Y.Z/install.sh | bash -s -- vX.Y.Z +``` + +If you're testing from a feature branch, replace `main` in the URL (URL-encoding the branch name if it contains `/`) and pass the branch name as the ref: + +```sh +curl -fsSL https://raw.githubusercontent.com/senet/kctl-env/feat%2Feasy-install/install.sh | bash -s -- feat/easy-install +``` + + +**Security note**: Tagged releases (v*) require SHA256 checksum verification by default and will fail if the checksum asset is missing, unless you explicitly set `KCTL_ENV_SKIP_VERIFY=1` to skip verification. Branch installations skip verification as GitHub does not provide checksums for auto-generated archives. + 1. Clone or extract kctl-env to a directory (e.g., `~/.kctl-env`). 2. Add the `bin/` directory to your `PATH`: diff --git a/bin/kctl-env b/bin/kctl-env index 70e72d0..ff02268 100755 --- a/bin/kctl-env +++ b/bin/kctl-env @@ -1,7 +1,10 @@ #!/usr/bin/env bash set -euo pipefail -KCTL_ENV_ROOT="${KCTL_ENV_ROOT:-$HOME/.kctl-env}" +script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" +default_root="$(cd -- "$script_dir/.." && pwd)" + +KCTL_ENV_ROOT="${KCTL_ENV_ROOT:-$default_root}" LIBEXEC="$KCTL_ENV_ROOT/libexec" usage() { diff --git a/bin/kubectl b/bin/kubectl index 47caede..7204bef 100755 --- a/bin/kubectl +++ b/bin/kubectl @@ -5,7 +5,10 @@ set -euo pipefail # --- Config --- -KCTL_ENV_ROOT="${KCTL_ENV_ROOT:-$HOME/.kctl-env}" +script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" +default_root="$(cd -- "$script_dir/.." && pwd)" + +KCTL_ENV_ROOT="${KCTL_ENV_ROOT:-$default_root}" KCTL_CACHE="$KCTL_ENV_ROOT/cache" KCTL_VERSIONS="$KCTL_ENV_ROOT/versions" KCTL_GLOBAL_VERSION_FILE="$KCTL_ENV_ROOT/version" diff --git a/install.sh b/install.sh new file mode 100755 index 0000000..4a5a5af --- /dev/null +++ b/install.sh @@ -0,0 +1,211 @@ +#!/usr/bin/env bash +# kctl-env bootstrap installer +# - Requires standard Unix tools: bash, curl, tar, coreutils (incl. install, mktemp, head, basename), find, awk +# - SHA256 verification uses sha256sum (Linux), shasum (macOS), or openssl (fallback) +# - Installs into $KCTL_ENV_ROOT (default: ~/.kctl-env) +# - Preserves existing runtime dirs (versions/, cache/) + +set -euo pipefail + +REPO_OWNER="senet" +REPO_NAME="kctl-env" + +KCTL_ENV_ROOT="${KCTL_ENV_ROOT:-$HOME/.kctl-env}" +KCTL_ENV_REF="${KCTL_ENV_REF:-}" +KCTL_ENV_SKIP_VERIFY="${KCTL_ENV_SKIP_VERIFY:-}" + +usage() { + cat < + +Arguments: + ref Git ref to install (required; tag like v0.1.1, or branch like main) + +Environment: + KCTL_ENV_ROOT Install root (default: ~/.kctl-env) + KCTL_ENV_REF Alternative to passing ref as argument (required if ref not provided) + +Examples: + ./install.sh v0.1.1 + KCTL_ENV_ROOT="$HOME/.kctl-env" ./install.sh main + KCTL_ENV_REF=v0.1.1 ./install.sh +EOF +} + +ref_from_input="${1:-}" +if [[ "${ref_from_input:-}" == "-h" || "${ref_from_input:-}" == "--help" ]]; then + usage + exit 0 +fi + +ref="${ref_from_input:-$KCTL_ENV_REF}" +if [[ -z "$ref" ]]; then + echo "Error: No ref specified. For supply-chain security, you must explicitly specify a version tag or branch." >&2 + echo "Usage: $0 (e.g., v0.1.1 or main)" >&2 + echo "See: curl -fsSL https://raw.githubusercontent.com/${REPO_OWNER}/${REPO_NAME}/main/install.sh | bash -s -- " >&2 + exit 1 +fi + +archive_url="" +case "$ref" in + v*) archive_url="https://github.com/${REPO_OWNER}/${REPO_NAME}/archive/refs/tags/${ref}.tar.gz" ;; + *) archive_url="https://github.com/${REPO_OWNER}/${REPO_NAME}/archive/refs/heads/${ref}.tar.gz" ;; +esac + +require_cmd() { + command -v "$1" >/dev/null 2>&1 || { echo "Missing required command: $1" >&2; exit 1; } +} + +# Compute SHA256 hash using available tools +# Returns hash on stdout, exits non-zero on error +compute_sha256() { + local file="$1" + if command -v sha256sum >/dev/null 2>&1; then + sha256sum "$file" | awk '{print $1}' + elif command -v shasum >/dev/null 2>&1; then + shasum -a 256 "$file" | awk '{print $1}' + elif command -v openssl >/dev/null 2>&1; then + openssl dgst -sha256 "$file" | awk '{print $NF}' + else + echo "Error: No SHA256 tool found (tried: sha256sum, shasum, openssl)" >&2 + exit 1 + fi +} + +require_cmd install +require_cmd mktemp +require_cmd curl +require_cmd find +require_cmd head +require_cmd tar +require_cmd awk +require_cmd basename +require_cmd tr + +tmpdir="$(mktemp -d "${TMPDIR:-/tmp}/kctl-env.XXXXXX")" +trap 'rm -rf "$tmpdir"' EXIT + +archive="$tmpdir/src.tar.gz" + +echo "Downloading $archive_url" +curl -fsSL "$archive_url" -o "$archive" + +# Verify integrity for tagged releases +# For tags (v*), try to fetch and verify SHA256 checksum from GitHub releases +# For branches, skip checksum (GitHub doesn't provide checksums for auto-generated tarballs) +if [[ "$ref" == v* ]]; then + if [[ -n "${KCTL_ENV_SKIP_VERIFY:-}" ]]; then + echo "Warning: Checksum verification explicitly skipped via KCTL_ENV_SKIP_VERIFY" >&2 + else + checksum_url="https://github.com/${REPO_OWNER}/${REPO_NAME}/releases/download/${ref}/${ref}.tar.gz.sha256" + echo "Verifying checksum..." + if curl -fsSL "$checksum_url" -o "$tmpdir/checksum.sha256" 2>/dev/null; then + # Extract just the hash (first field) and verify manually + expected_hash="$(awk 'NR==1{print $1; exit}' "$tmpdir/checksum.sha256" | tr '[:upper:]' '[:lower:]')" + actual_hash="$(compute_sha256 "$archive" | tr '[:upper:]' '[:lower:]')" + + # Validate that hashes were extracted successfully + if [[ -z "$expected_hash" || -z "$actual_hash" ]]; then + echo "Error: Failed to extract checksum values" >&2 + echo "The checksum file may be empty or malformed" >&2 + exit 1 + fi + + if [[ ! "$expected_hash" =~ ^[0-9a-f]{64}$ ]]; then + echo "Error: Checksum file is malformed (expected SHA256 hex)." >&2 + echo "Got: $expected_hash" >&2 + exit 1 + fi + + if [[ "$expected_hash" != "$actual_hash" ]]; then + echo "Checksum verification failed for $ref" >&2 + echo "Expected: $expected_hash" >&2 + echo "Actual: $actual_hash" >&2 + echo "This may indicate a compromised download or release." >&2 + exit 1 + fi + echo "Checksum verified successfully" + else + echo "Error: No checksum found for $ref at $checksum_url" >&2 + echo "For security, this installer requires SHA256 verification for tagged releases." >&2 + echo "If you still want to install this older release without checksums, you can re-run with:" >&2 + echo " Local file: KCTL_ENV_SKIP_VERIFY=1 ./install.sh $ref" >&2 + echo " Via curl: KCTL_ENV_SKIP_VERIFY=1 bash -s -- $ref" >&2 + exit 1 + fi + fi +else + echo "Note: Checksum verification skipped for branch '$ref' (not available for auto-generated archives)" +fi + +# Extract +mkdir -p "$tmpdir/src" +tar -xzf "$archive" -C "$tmpdir/src" + +# Determine extracted directory (choose first top-level subdirectory deterministically) +src_root="" +for d in "$tmpdir/src"/*; do + if [[ -d "$d" ]]; then + src_root="$d" + break + fi +done +if [[ -z "${src_root:-}" || ! -d "$src_root" ]]; then + echo "Failed to locate extracted source directory" >&2 + exit 1 +fi + +# Create target directories (preserve versions/cache if already exist) +mkdir -p "$KCTL_ENV_ROOT" \ + "$KCTL_ENV_ROOT/bin" \ + "$KCTL_ENV_ROOT/libexec" \ + "$KCTL_ENV_ROOT/etc" \ + "$KCTL_ENV_ROOT/packaging" \ + "$KCTL_ENV_ROOT/scripts" \ + "$KCTL_ENV_ROOT/versions" \ + "$KCTL_ENV_ROOT/cache" + +# Install core scripts +install -m 0755 "$src_root/bin/kctl-env" "$KCTL_ENV_ROOT/bin/kctl-env" +install -m 0755 "$src_root/bin/kubectl" "$KCTL_ENV_ROOT/bin/kubectl" + +# libexec scripts +for f in "$src_root"/libexec/*; do + [[ -f "$f" ]] || continue + install -m 0755 "$f" "$KCTL_ENV_ROOT/libexec/$(basename "$f")" +done + +# Completion scripts (optional) +if [[ -f "$src_root/etc/kctl-env-completion.bash" ]]; then + install -m 0644 "$src_root/etc/kctl-env-completion.bash" "$KCTL_ENV_ROOT/etc/kctl-env-completion.bash" +fi +if [[ -f "$src_root/etc/kctl-env-completion.zsh" ]]; then + install -m 0644 "$src_root/etc/kctl-env-completion.zsh" "$KCTL_ENV_ROOT/etc/kctl-env-completion.zsh" +fi + +# Scripts (optional) +if [[ -f "$src_root/scripts/release.sh" ]]; then + install -m 0755 "$src_root/scripts/release.sh" "$KCTL_ENV_ROOT/scripts/release.sh" +fi + +# Docs + metadata (best-effort) +for f in README.md CHANGELOG.md VERSION; do + if [[ -f "$src_root/$f" ]]; then + install -m 0644 "$src_root/$f" "$KCTL_ENV_ROOT/$f" + fi +done + +echo +echo "Installed kctl-env into: $KCTL_ENV_ROOT" +echo +echo "Next steps:" +echo " 1) Add to PATH:" +echo " export PATH=\"$KCTL_ENV_ROOT/bin:\$PATH\"" +echo " 2) Install kubectl:" +echo " kctl-env install latest" +echo " 3) Select version:" +echo " kctl-env use latest" +echo diff --git a/libexec/kctl-env-list-remote b/libexec/kctl-env-list-remote index a2d0a3e..91c34b3 100755 --- a/libexec/kctl-env-list-remote +++ b/libexec/kctl-env-list-remote @@ -1,10 +1,57 @@ #!/usr/bin/env bash set -euo pipefail -# List available kubectl releases from dl.k8s.io -# Zero-deps: parse HTML index +# List available kubectl releases. +# +# Kubernetes' dl.k8s.io no longer exposes a browsable release index, so we use +# the GitHub tags API and parse it with POSIX tools (no jq). +# +# Notes: +# - Unauthenticated GitHub API requests are rate-limited. +# - Increase limits by exporting GITHUB_TOKEN. -curl -fsSL https://dl.k8s.io/release/ | - grep -Eo 'v[0-9]+\.[0-9]+\.[0-9]+' | - sort -Vr | - uniq +MAX_PAGES="${KCTL_LIST_REMOTE_MAX_PAGES:-5}" +PER_PAGE=100 + +api_base="https://api.github.com/repos/kubernetes/kubernetes/tags" + +curl_args=(-sSLf) +if [[ -n "${GITHUB_TOKEN:-}" ]]; then + curl_args+=(-H "Authorization: Bearer ${GITHUB_TOKEN}") +fi + +page=1 +while [[ "$page" -le "$MAX_PAGES" ]]; do + json="$(curl "${curl_args[@]}" "${api_base}?per_page=${PER_PAGE}&page=${page}")" || { + echo "kctl-env: failed to query GitHub tags API (page ${page})." >&2 + echo "Hint: export GITHUB_TOKEN to avoid rate limits, or set KCTL_LIST_REMOTE_MAX_PAGES lower." >&2 + exit 1 + } + + # Extract stable semver tags like v1.29.0 + tags="$(printf '%s' "$json" | + grep -Eo '"name"[[:space:]]*:[[:space:]]*"v[0-9]+\.[0-9]+\.[0-9]+"' | + sed -E 's/.*"(v[0-9]+\.[0-9]+\.[0-9]+)".*/\1/' + )" + + [[ -z "$tags" ]] && break + printf '%s\n' "$tags" + page=$((page + 1)) +done | + if sort -V /dev/null 2>&1; then + # Prefer GNU sort's version sort when available. + sort -Vr | uniq + else + # POSIX fallback: sort semantic versions vMAJOR.MINOR.PATCH numerically. + awk -F. ' + { + ver = $0 + # Strip leading "v" from major component. + sub(/^v/, "", $1) + # Print: original major minor patch + printf "%s %d %d %d\n", ver, $1, $2, $3 + } + ' | + sort -k2,2nr -k3,3nr -k4,4nr | + awk '!seen[$1]++ { print $1 }' + fi diff --git a/scripts/release.sh b/scripts/release.sh index 76261f9..32c0467 100644 --- a/scripts/release.sh +++ b/scripts/release.sh @@ -52,3 +52,15 @@ git add VERSION "$changelog_file" "$spec_file" git commit -m "release: bump to v$new_version" echo "Release files updated and committed. Open a PR if on a release branch." +echo +echo "After merging and creating the tag, remember to:" +echo " 1. Create a GitHub release for v$new_version" +echo " 2. Generate and attach checksum for the source tarball:" +echo " # On Linux / systems with GNU coreutils:" +echo " curl -fsSL https://github.com/senet/kctl-env/archive/refs/tags/v$new_version.tar.gz | sha256sum > v$new_version.tar.gz.sha256" +echo " # On macOS (no sha256sum by default):" +echo " curl -fsSL https://github.com/senet/kctl-env/archive/refs/tags/v$new_version.tar.gz | shasum -a 256 > v$new_version.tar.gz.sha256" +echo " 3. Create the release and attach the checksum asset (immutable releases):" +echo " gh release create v$new_version --title \"v$new_version\" --notes \"Release v$new_version\" v$new_version.tar.gz.sha256" +echo " This enables secure installation with: curl -fsSL https://raw.githubusercontent.com/senet/kctl-env/v$new_version/install.sh | bash -s -- v$new_version" +