diff --git a/kasmvnc/README.md b/kasmvnc/README.md index 9c3b28db..bceef3e0 100644 --- a/kasmvnc/README.md +++ b/kasmvnc/README.md @@ -9,7 +9,8 @@ tags: [helper, vnc, desktop] # KasmVNC -Automatically install [KasmVNC](https://kasmweb.com/kasmvnc) in a workspace, and create an app to access it via the dashboard. +Automatically install [KasmVNC](https://kasmweb.com/kasmvnc) in a workspace, and +create an app to access it via the dashboard. ```tf module "kasmvnc" { @@ -18,7 +19,12 @@ module "kasmvnc" { version = "1.0.23" agent_id = coder_agent.example.id desktop_environment = "xfce" + subdomain = true } ``` -> **Note:** This module only works on workspaces with a pre-installed desktop environment. As an example base image you can use `codercom/enterprise-desktop` image. +> **Note:** This module only works on workspaces with a one of the following +> desktop environments pre-installed: `xfce`, `kde`, `gnome`, `lxde`, `lxqt`. + +> The `codercom/enterprise-desktop` base image contains `xfce` and can be used +> as an example image. diff --git a/kasmvnc/main.tf b/kasmvnc/main.tf index 4265f3c7..c2867323 100644 --- a/kasmvnc/main.tf +++ b/kasmvnc/main.tf @@ -9,51 +9,30 @@ terraform { } } -variable "agent_id" { - type = string - description = "The ID of a Coder agent." -} - -variable "port" { - type = number - description = "The port to run KasmVNC on." - default = 6800 -} - -variable "kasm_version" { - type = string - description = "Version of KasmVNC to install." - default = "1.3.2" -} - -variable "desktop_environment" { - type = string - description = "Specifies the desktop environment of the workspace. This should be pre-installed on the workspace." - validation { - condition = contains(["xfce", "kde", "gnome", "lxde", "lxqt"], var.desktop_environment) - error_message = "Invalid desktop environment. Please specify a valid desktop environment." - } +locals { + template_data = object({ + PORT = var.port, + DESKTOP_ENVIRONMENT = var.desktop_environment, + KASM_VERSION = var.kasm_version + SUBDOMAIN = tostring(var.subdomain) + }) } resource "coder_script" "kasm_vnc" { agent_id = var.agent_id display_name = "KasmVNC" icon = "/icon/kasmvnc.svg" - script = templatefile("${path.module}/run.sh", { - PORT : var.port, - DESKTOP_ENVIRONMENT : var.desktop_environment, - KASM_VERSION : var.kasm_version - }) run_on_start = true + script = templatefile("${path.module}/run.sh.tftpl", locals.template_data) } resource "coder_app" "kasm_vnc" { agent_id = var.agent_id slug = "kasm-vnc" - display_name = "kasmVNC" + display_name = "KasmVNC" url = "http://localhost:${var.port}" icon = "/icon/kasmvnc.svg" - subdomain = true + subdomain = var.subdomain share = "owner" healthcheck { url = "http://localhost:${var.port}/app" diff --git a/kasmvnc/path_vnc.html b/kasmvnc/path_vnc.html new file mode 100644 index 00000000..287bdbfc --- /dev/null +++ b/kasmvnc/path_vnc.html @@ -0,0 +1,73 @@ + + + + Path-Sharing Bounce Page + + + + +

Path-Sharing Bounce Page

+

+ This application is being served via path sharing. + If you are not redirected, check the + Javascript console in your browser's developer tools + for more information. +

+ + + diff --git a/kasmvnc/run.sh b/kasmvnc/run.tftpl similarity index 55% rename from kasmvnc/run.sh rename to kasmvnc/run.tftpl index c285b050..096003b6 100644 --- a/kasmvnc/run.sh +++ b/kasmvnc/run.tftpl @@ -3,6 +3,14 @@ # Exit on error, undefined variables, and pipe failures set -euo pipefail +info() { printf "💁 INFO: %s\n" "$@"; } +warn() { printf "😱 WARNING: %s\n" "$@" ;} +error() { printf "💀 ERROR: %s\n" "$@"; exit 1; } +debug() { + if [ "${DEBUG}" != "Y" ]; then return; fi + printf "🦺 DEBUG: %s\n" "$@" +} + # Function to check if vncserver is already installed check_installed() { if command -v vncserver &> /dev/null; then @@ -29,14 +37,12 @@ download_file() { # shellcheck disable=SC2034 download_tool=(busybox wget -O-) else - echo "ERROR: No download tool available (curl, wget, or busybox required)" - exit 1 + error "No download tool available (curl, wget, or busybox required)" fi # shellcheck disable=SC2288 "$${download_tool[@]}" "$url" > "$output" || { - echo "ERROR: Failed to download $url" - exit 1 + error "Failed to download $url" } } @@ -79,16 +85,14 @@ install_rpm() { # shellcheck disable=SC2034 package_manager=(rpm -i) else - echo "ERROR: No supported package manager available (dnf, zypper, yum, or rpm required)" - exit 1 + error "No supported package manager available (dnf, zypper, yum, or rpm required)" fi download_file "$url" "$kasmrpm" # shellcheck disable=SC2288 sudo "$${package_manager[@]}" "$kasmrpm" || { - echo "ERROR: Failed to install $kasmrpm" - exit 1 + error "Failed to install $kasmrpm" } rm "$kasmrpm" @@ -107,8 +111,8 @@ install_alpine() { # Detect system information if [[ ! -f /etc/os-release ]]; then - echo "ERROR: Cannot detect OS: /etc/os-release not found" - exit 1 + error "Cannot detect OS: /etc/os-release not found" + fi # shellcheck disable=SC1091 @@ -124,10 +128,11 @@ elif [[ "$ID" == "fedora" ]]; then distro_version="$(grep -oP '\(\K[\w ]+' /etc/fedora-release | tr '[:upper:]' '[:lower:]' | tr -d ' ')" fi -echo "Detected Distribution: $distro" -echo "Detected Version: $distro_version" -echo "Detected Codename: $codename" -echo "Detected Architecture: $arch" +echo "🕵 Detected Operating System Information" +echo " 🔎 Distribution: $distro" +echo " 🔎 Version: $distro_version" +echo " 🔎 Codename: $codename" +echo " 🔎 Architecture: $arch" # Map arch to package arch case "$arch" in @@ -145,8 +150,7 @@ case "$arch" in : # This is effectively a noop ;; *) - echo "ERROR: Unsupported architecture: $arch" - exit 1 + error "Unsupported architecture: $arch" ;; esac @@ -154,8 +158,7 @@ esac if ! check_installed; then # Check for NOPASSWD sudo (required) if ! command -v sudo &> /dev/null || ! sudo -n true 2> /dev/null; then - echo "ERROR: sudo NOPASSWD access required!" - exit 1 + error "sudo NOPASSWD access required!" fi base_url="https://github.com/kasmtech/KasmVNC/releases/download/v${KASM_VERSION}" @@ -190,14 +193,14 @@ else kasm_config_file="$HOME/.vnc/kasmvnc.yaml" SUDO= - echo "WARNING: Sudo access not available, using user config dir!" + warn "Sudo access not available, using user config dir!" if [[ -f "$kasm_config_file" ]]; then - echo "WARNING: Custom user KasmVNC config exists, not overwriting!" - echo "WARNING: Ensure that you manually configure the appropriate settings." + warn "Custom user KasmVNC config exists, not overwriting!" + warm "Ensure that you manually configure the appropriate settings." kasm_config_file="/dev/stderr" else - echo "WARNING: This may prevent custom user KasmVNC settings from applying!" + warn "This may prevent custom user KasmVNC settings from applying!" mkdir -p "$HOME/.vnc" fi fi @@ -213,8 +216,71 @@ network: pem_key: udp: public_ip: 127.0.0.1 +logging: + log_writer_name: all + log_dest: logfile + level: 30 EOF +get_http_dir() { + # determine the served file path + # Start with the default + httpd_directory="/usr/share/kasmvnc/www" + + # Check the system configuration path + if [[ -e /etc/kasmvnc/kasmvnc.yaml ]]; then + d=($(grep -E "^\s*httpd_directory:.*$" /etc/kasmvnc/kasmvnc.yaml)) + # If this grep is successful, it will return: + # httpd_directory: /usr/share/kasmvnc/www + if [[ $${#d[@]} -eq 2 && -d "$${d[1]}" ]]; then + httpd_directory="$${d[1]}" + fi + fi + + # Check the home directory for overriding values + if [[ -e "$HOME/.vnc/kasmvnc.yaml" ]]; then + d=($(grep -E "^\s*httpd_directory:.*$" /etc/kasmvnc/kasmvnc.yaml)) + if [[ $${#d[@]} -eq 2 && -d "$${d[1]}" ]]; then + httpd_directory="$${d[1]}" + fi + fi + echo $httpd_directory +} + +fix_server_index_file(){ + local fname=$${FUNCNAME[0]} # gets current function name + if [[ $# -ne 1 ]]; then + error "$fname requires exactly 1 parameter:\n\tpath to KasmVNC httpd_directory" + fi + local httpdir="$1" + if [[ ! -d "$httpdir" ]]; then + error "$fname: $httpdir is not a directory" + fi + pushd "$httpdir" > /dev/null + + cat < /tmp/path_vnc.html +${file(path.module)} +EOH + $SUDO mv /tmp/path_vnc.html . + # check for the switcheroo + if [[ -f "index.html" && -L "vnc.html" ]]; then + $SUDO mv $httpdir/index.html $httpdir/vnc.html + fi + $SUDO ln -s -f path_vnc.html index.html + popd > /dev/null +} + +patch_kasm_http_files(){ + homedir=$(get_http_dir) + fix_server_index_file "$homedir" +} + +if [[ "${SUBDOMAIN}" == "false" ]]; then + info "🩹 Patching up webserver files to support path-sharing..." + patch_kasm_http_files +fi + + # This password is not used since we start the server without auth. # The server is protected via the Coder session token / tunnel # and does not listen publicly @@ -222,14 +288,47 @@ echo -e "password\npassword\n" | vncpasswd -wo -u "$USER" # Start the server printf "🚀 Starting KasmVNC server...\n" -vncserver -select-de "${DESKTOP_ENVIRONMENT}" -disableBasicAuth > /tmp/kasmvncserver.log 2>&1 & -pid=$! - -# Wait for server to start -sleep 5 -grep -v '^[[:space:]]*$' /tmp/kasmvncserver.log | tail -n 10 -if ps -p $pid | grep -q "^$pid"; then - echo "ERROR: Failed to start KasmVNC server. Check full logs at /tmp/kasmvncserver.log" - exit 1 +vncserver -select-de "${DESKTOP_ENVIRONMENT}" -disableBasicAuth > /tmp/kasmvncserver.log & + +# Kasm writes the pid and the log into ~/.vnc. We can check them for liveness +is_started() { + debug "ls for pidfile: $(ls -alh ~/.vnc/$(hostname):1.pid 2>&1)" + + pidfile="$${HOME}/.vnc/$(hostname):1.pid" + if [[ ! -f $pidfile ]]; then + debug "is_started(): no pidfile found" + return 1 + fi + pid=$(cat $${pidfile}) + debug "$(ps $pid)" + if kill -0 $pid; then + debug "is_started(): found a live PID, setting active" + declare -gx active="Y" + return 0 + else + debug "is_started(): PID is not active" + return 1 + fi + warning "is_started(): REACHED THE END WITHOUT HITTING A CASE" + return 1 +} + +# Use a sleep based polling timer to see when Kasm comes up. +waited=0 +is_started && debug "is Started: true" || debug "is_started: false" +[[ waited -le 30 ]] && debug "waited -le 30: true" || debug "waited -le 30: false" +while [[ waited -le 30 ]] && ! is_started; do + sleep 1 + waited=$((waited+1)) + if [[ waited -ne 0 && $((waited % 5)) -eq 0 ]]; then + echo "⏳ Waiting for KasmVNC to start ($waited seconds...)" + fi +is_started && debug "is Started: true" || debug "is_started: false" +[[ waited -le 30 ]] && debug "waited -le 30: true" || debug "waited -le 30: false" +done + +if [[ "$active" == "" ]]; then + error "timed out waiting for KasmVNC to start." fi -printf "🚀 KasmVNC server started successfully!\n" + +printf "🚀 KasmVNC server started successfully in $waited seconds!\n" \ No newline at end of file diff --git a/kasmvnc/variables.tf b/kasmvnc/variables.tf new file mode 100644 index 00000000..ebc7e740 --- /dev/null +++ b/kasmvnc/variables.tf @@ -0,0 +1,31 @@ +variable "agent_id" { + type = string + description = "The ID of a Coder agent." +} + +variable "port" { + type = number + description = "The port to run KasmVNC on." + default = 6800 +} + +variable "kasm_version" { + type = string + description = "Version of KasmVNC to install." + default = "1.3.2" +} + +variable "desktop_environment" { + type = string + description = "Specifies the desktop environment of the workspace. This must be pre-installed on the workspace." + validation { + condition = contains(["xfce", "kde", "gnome", "lxde", "lxqt"], var.desktop_environment) + error_message = "Invalid desktop environment. Please specify a valid desktop environment." + } +} + +variable "subdomain" { + type = bool + description = "Are subdomains enabled on this Coder cluster" + default = true +}