mirror of
https://github.com/lukaszraczylo/kportal.git
synced 2026-06-05 23:03:40 +00:00
0a8c872b01
Releases are signed by the lukaszraczylo/shared-actions reusable workflow, so the Sigstore certificate subject is the workflow URL rather than this repo. The previous regex 'https://github.com/lukaszraczylo/kportal/.*' never matched, so any user with cosign installed would see verification fail and abort the install. Pin cert-identity to the exact workflow URL: ^https://github\.com/lukaszraczylo/shared-actions/\.github/workflows/go-release\.yaml@refs/heads/main$ Override via COSIGN_CERT_IDENTITY_REGEXP for forks of the release pipeline. Same fix applied to README's manual verification example. Verified end-to-end against release v0.2.90: cosign verify-blob ... -> Verified OK
301 lines
8.9 KiB
Bash
Executable File
301 lines
8.9 KiB
Bash
Executable File
#!/bin/bash
|
|
|
|
set -e
|
|
|
|
# kportal installation script
|
|
# Usage: curl -fsSL https://raw.githubusercontent.com/lukaszraczylo/kportal/main/install.sh | bash
|
|
#
|
|
# Environment overrides:
|
|
# INSTALL_DIR - target install directory (default: /usr/local/bin)
|
|
# KPORTAL_VERSION - install a specific version instead of latest (e.g. 1.2.3)
|
|
# DRY_RUN=1 - download and verify but do not install (for local testing)
|
|
# SKIP_COSIGN=1 - skip cosign signature verification even if cosign is present
|
|
|
|
REPO="lukaszraczylo/kportal"
|
|
INSTALL_DIR="${INSTALL_DIR:-/usr/local/bin}"
|
|
DRY_RUN="${DRY_RUN:-0}"
|
|
SKIP_COSIGN="${SKIP_COSIGN:-0}"
|
|
|
|
# Colors
|
|
RED='\033[0;31m'
|
|
GREEN='\033[0;32m'
|
|
YELLOW='\033[1;33m'
|
|
BLUE='\033[0;34m'
|
|
NC='\033[0m' # No Color
|
|
|
|
# Print functions
|
|
print_info() {
|
|
echo -e "${BLUE}i${NC} $1"
|
|
}
|
|
|
|
print_success() {
|
|
echo -e "${GREEN}OK${NC} $1"
|
|
}
|
|
|
|
print_error() {
|
|
echo -e "${RED}X${NC} $1" >&2
|
|
}
|
|
|
|
print_warning() {
|
|
echo -e "${YELLOW}!${NC} $1"
|
|
}
|
|
|
|
# Detect OS
|
|
detect_os() {
|
|
case "$(uname -s)" in
|
|
Linux*) echo "linux";;
|
|
Darwin*) echo "darwin";;
|
|
MINGW*|MSYS*|CYGWIN*) echo "windows";;
|
|
*) echo "unknown";;
|
|
esac
|
|
}
|
|
|
|
# Detect architecture
|
|
detect_arch() {
|
|
case "$(uname -m)" in
|
|
x86_64|amd64) echo "amd64";;
|
|
aarch64|arm64) echo "arm64";;
|
|
armv7l) echo "arm";;
|
|
*) echo "unknown";;
|
|
esac
|
|
}
|
|
|
|
# Get latest version from GitHub
|
|
get_latest_version() {
|
|
curl -fsSL "https://api.github.com/repos/${REPO}/releases/latest" |
|
|
grep '"tag_name":' |
|
|
sed -E 's/.*"v([^"]+)".*/\1/'
|
|
}
|
|
|
|
# Compute sha256 of a file. Uses shasum which is available on macOS and Linux.
|
|
compute_sha256() {
|
|
local file="$1"
|
|
if command -v shasum >/dev/null 2>&1; then
|
|
shasum -a 256 "${file}" | awk '{ print $1 }'
|
|
elif command -v sha256sum >/dev/null 2>&1; then
|
|
sha256sum "${file}" | awk '{ print $1 }'
|
|
else
|
|
print_error "Neither 'shasum' nor 'sha256sum' is available; cannot verify checksum"
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
# Verify the archive against checksums.txt (SHA-256). Aborts on mismatch.
|
|
verify_checksum() {
|
|
local archive="$1"
|
|
local checksums_file="$2"
|
|
|
|
print_info "Verifying SHA-256 checksum..."
|
|
|
|
local expected
|
|
# Match the archive name as the second whitespace-separated field.
|
|
# checksums.txt format produced by goreleaser: "<sha256> <filename>"
|
|
expected=$(awk -v name="${archive}" '$2 == name { print $1; exit }' "${checksums_file}")
|
|
|
|
if [ -z "${expected}" ]; then
|
|
print_error "Checksum for ${archive} not found in checksums.txt"
|
|
print_error "Refusing to install unverified binary."
|
|
exit 1
|
|
fi
|
|
|
|
local actual
|
|
actual=$(compute_sha256 "${archive}")
|
|
|
|
if [ "${expected}" != "${actual}" ]; then
|
|
print_error "Checksum mismatch for ${archive}"
|
|
print_error " expected: ${expected}"
|
|
print_error " actual: ${actual}"
|
|
print_error "Aborting installation. The downloaded archive may be corrupted or tampered with."
|
|
exit 1
|
|
fi
|
|
|
|
print_success "SHA-256 checksum OK"
|
|
}
|
|
|
|
# Optional: verify cosign signature on the checksums file. Silently skipped
|
|
# when cosign is not installed or the signature artefact is not present.
|
|
verify_cosign_signature() {
|
|
local checksums_file="$1"
|
|
local sig_file="$2"
|
|
|
|
if [ "${SKIP_COSIGN}" = "1" ]; then
|
|
return 0
|
|
fi
|
|
|
|
if ! command -v cosign >/dev/null 2>&1; then
|
|
# cosign not installed; supply-chain integrity still rests on SHA-256
|
|
return 0
|
|
fi
|
|
|
|
if [ ! -f "${sig_file}" ]; then
|
|
# No sig artefact downloaded; skip silently
|
|
return 0
|
|
fi
|
|
|
|
print_info "Verifying cosign signature on checksums.txt..."
|
|
# Releases are signed by the shared-actions reusable workflow, so the
|
|
# cert subject is the workflow URL — NOT this repo. Override with
|
|
# COSIGN_CERT_IDENTITY_REGEXP if you fork the release pipeline.
|
|
local cert_identity_regexp="${COSIGN_CERT_IDENTITY_REGEXP:-^https://github\.com/lukaszraczylo/shared-actions/\.github/workflows/go-release\.yaml@refs/heads/main$}"
|
|
if cosign verify-blob \
|
|
--certificate-identity-regexp "${cert_identity_regexp}" \
|
|
--certificate-oidc-issuer "https://token.actions.githubusercontent.com" \
|
|
--bundle "${sig_file}" \
|
|
"${checksums_file}" >/dev/null 2>&1; then
|
|
print_success "cosign signature OK"
|
|
else
|
|
print_error "cosign signature verification FAILED for checksums.txt"
|
|
print_error "Aborting installation."
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
# Main installation
|
|
main() {
|
|
echo ""
|
|
echo "kportal installation script"
|
|
echo "Kubernetes port forwarding made easy"
|
|
echo ""
|
|
|
|
# Detect system
|
|
OS=$(detect_os)
|
|
ARCH=$(detect_arch)
|
|
|
|
if [ "$OS" = "unknown" ] || [ "$ARCH" = "unknown" ]; then
|
|
print_error "Unsupported operating system or architecture"
|
|
print_info "OS: $(uname -s), Arch: $(uname -m)"
|
|
exit 1
|
|
fi
|
|
|
|
print_info "Detected: ${OS}/${ARCH}"
|
|
|
|
# Get version
|
|
if [ -n "${KPORTAL_VERSION:-}" ]; then
|
|
VERSION="${KPORTAL_VERSION#v}"
|
|
print_info "Using requested version: v${VERSION}"
|
|
else
|
|
print_info "Fetching latest version..."
|
|
VERSION=$(get_latest_version)
|
|
if [ -z "$VERSION" ]; then
|
|
print_error "Failed to fetch latest version"
|
|
exit 1
|
|
fi
|
|
print_success "Latest version: v${VERSION}"
|
|
fi
|
|
|
|
# Construct download URLs
|
|
if [ "$OS" = "windows" ]; then
|
|
ARCHIVE="kportal-${VERSION}-${OS}-${ARCH}.zip"
|
|
else
|
|
ARCHIVE="kportal-${VERSION}-${OS}-${ARCH}.tar.gz"
|
|
fi
|
|
|
|
BASE_URL="https://github.com/${REPO}/releases/download/v${VERSION}"
|
|
DOWNLOAD_URL="${BASE_URL}/${ARCHIVE}"
|
|
CHECKSUMS_FILE="kportal-${VERSION}-checksums.txt"
|
|
CHECKSUMS_URL="${BASE_URL}/${CHECKSUMS_FILE}"
|
|
SIG_FILE="${CHECKSUMS_FILE}.sigstore.json"
|
|
SIG_URL="${BASE_URL}/${SIG_FILE}"
|
|
|
|
# Create temporary directory
|
|
TMP_DIR=$(mktemp -d)
|
|
# shellcheck disable=SC2064
|
|
trap "rm -rf '${TMP_DIR}'" EXIT
|
|
|
|
# Download archive
|
|
print_info "Downloading ${ARCHIVE}..."
|
|
if ! curl -fsSL -o "${TMP_DIR}/${ARCHIVE}" "${DOWNLOAD_URL}"; then
|
|
print_error "Failed to download kportal archive"
|
|
print_info "URL: ${DOWNLOAD_URL}"
|
|
exit 1
|
|
fi
|
|
|
|
# Download checksums
|
|
print_info "Downloading checksums.txt..."
|
|
if ! curl -fsSL -o "${TMP_DIR}/${CHECKSUMS_FILE}" "${CHECKSUMS_URL}"; then
|
|
print_error "Failed to download checksums file"
|
|
print_info "URL: ${CHECKSUMS_URL}"
|
|
print_error "Refusing to install without checksum verification."
|
|
exit 1
|
|
fi
|
|
|
|
# Try to download cosign signature bundle (best-effort, non-fatal if absent)
|
|
if curl -fsSL -o "${TMP_DIR}/${SIG_FILE}" "${SIG_URL}" 2>/dev/null; then
|
|
:
|
|
else
|
|
rm -f "${TMP_DIR}/${SIG_FILE}"
|
|
fi
|
|
|
|
# Verify archive checksum
|
|
cd "${TMP_DIR}"
|
|
verify_checksum "${ARCHIVE}" "${CHECKSUMS_FILE}"
|
|
|
|
# Optional cosign signature verification on checksums file
|
|
verify_cosign_signature "${CHECKSUMS_FILE}" "${SIG_FILE}"
|
|
|
|
# Extract archive
|
|
print_info "Extracting archive..."
|
|
if [ "$OS" = "windows" ]; then
|
|
unzip -q "${ARCHIVE}"
|
|
BINARY="kportal.exe"
|
|
else
|
|
tar -xzf "${ARCHIVE}"
|
|
BINARY="kportal"
|
|
fi
|
|
|
|
# Check if binary exists
|
|
if [ ! -f "${BINARY}" ]; then
|
|
print_error "Binary not found after extraction"
|
|
exit 1
|
|
fi
|
|
|
|
# Make binary executable
|
|
chmod +x "${BINARY}"
|
|
|
|
if [ "${DRY_RUN}" = "1" ]; then
|
|
print_success "Dry run successful. Verified archive at ${TMP_DIR}/${ARCHIVE}"
|
|
print_info "Skipping install step (DRY_RUN=1)"
|
|
return 0
|
|
fi
|
|
|
|
# Install binary
|
|
print_info "Installing kportal to ${INSTALL_DIR}..."
|
|
|
|
# Check if we need sudo
|
|
if [ ! -w "${INSTALL_DIR}" ]; then
|
|
print_warning "Installation directory requires sudo access"
|
|
if command -v sudo >/dev/null 2>&1; then
|
|
sudo mv "${BINARY}" "${INSTALL_DIR}/${BINARY}"
|
|
else
|
|
print_error "sudo not found. Please run with appropriate permissions"
|
|
exit 1
|
|
fi
|
|
else
|
|
mv "${BINARY}" "${INSTALL_DIR}/${BINARY}"
|
|
fi
|
|
|
|
# Verify installation (portable: awk instead of GNU-only grep -oP)
|
|
if command -v kportal >/dev/null 2>&1; then
|
|
INSTALLED_VERSION=$(kportal --version 2>/dev/null | awk '/^kportal version/ { print $3; exit }')
|
|
if [ -z "${INSTALLED_VERSION}" ]; then
|
|
INSTALLED_VERSION="unknown"
|
|
fi
|
|
print_success "kportal v${INSTALLED_VERSION} installed successfully!"
|
|
else
|
|
print_warning "kportal installed but not found in PATH"
|
|
print_info "You may need to add ${INSTALL_DIR} to your PATH"
|
|
fi
|
|
|
|
echo ""
|
|
print_success "Installation complete!"
|
|
echo ""
|
|
echo "Get started:"
|
|
echo " 1. Create a config file: touch .kportal.yaml"
|
|
echo " 2. Run: kportal"
|
|
echo ""
|
|
echo "Documentation: https://lukaszraczylo.github.io/kportal"
|
|
echo ""
|
|
}
|
|
|
|
main "$@"
|