diff --git a/README.md b/README.md index 308e14b..d0a0b6d 100644 --- a/README.md +++ b/README.md @@ -1,152 +1,45 @@ +![Maintainarr Banner](web/static/img/maintainarr_banner.png) + # Maintainarr -Maintainarr is a self-hosted Go application for managing a single organization's Linux VM fleet from one dashboard. +Maintainarr is a self-hosted dashboard for managing Linux VMs from one place. -It ships with: +## Install -- SQLite-backed persistence -- Daily compressed command log archives -- User registration and login -- OTP-based 2FA setup and verification -- Role model with `admin`, `editor`, and `viewer` -- Sidebar dashboard with theme tokens compatible with [UI Colors](https://uicolors.app/generate) -- VM chip grid with distro icon, CPU/RAM bars, IP, and uptime -- Node overview pages with action buttons and SSH web console -- Wake-on-LAN, restart, shutdown, stat refresh, and apt-upgrade action hooks -- VM groups and recurring/triggered automation job scaffolding -- Go-first backend structure intended for adding more fleet features without rewrites - -## Branch Model - -- `main`: stable branch -- `dev`: active feature branch - -The repository has been initialized with both `main` and `dev`. Work should land in `dev` first. - -## Stack - -- Go `1.25` -- `chi` router -- `modernc.org/sqlite` for embedded SQLite -- `gorilla/sessions` for cookie sessions -- `pquerna/otp` for TOTP 2FA -- `golang.org/x/crypto/ssh` for remote command and console transport -- Embedded Go HTML templates plus local static assets - -## Project Layout - -```text -cmd/maintainarr entrypoint -internal/app app bootstrap and routing -internal/config environment configuration -internal/db sqlite connection and schema bootstrap -internal/handlers HTTP handlers and websocket console -internal/middleware auth and role enforcement -internal/models domain models -internal/services auth, crypto, nodes, scheduler, repository -internal/views embedded templates and view helpers -web/static CSS and browser-side JS +```bash +curl -fsSL https://dock-it.dev/Idea-Studios/Maintainarr/raw/branch/main/install.sh | sudo bash ``` -## Run +The installer will: -```powershell +- install missing system packages +- install Go if needed +- download or reuse Maintainarr under `/opt/maintainarr` +- create config in `/etc/maintainarr/maintainarr.env` +- create data in `/var/lib/maintainarr` +- ask about auto-updates with `cron` +- ask about starting at boot with `systemd` + +## Open + +After install, open: + +```text +http://your-server-ip:8080 +``` + +The first registered user becomes the admin. + +## Manual Run + +```bash +cd /opt/maintainarr/src go run ./cmd/maintainarr ``` -Default address: `http://localhost:8080` +## Important Paths -## Container - -```powershell -docker build -t maintainarr . -docker run --rm -p 8080:8080 -v ${PWD}/data:/app/data maintainarr -``` - -## First User - -The first registered user becomes the initial `admin`. - -No example users, groups, nodes, or jobs are seeded. - -## Environment - -```env -MAINTAINARR_ADDR=:8080 -MAINTAINARR_DB_PATH=data/maintainarr.db -MAINTAINARR_LOG_ARCHIVE_DIR=data/log-archives -MAINTAINARR_SESSION_KEY=change-me-session-key-please -MAINTAINARR_ENCRYPTION_KEY=change-me-encryption-key-32bytes -MAINTAINARR_ORG_NAME=Maintainarr -MAINTAINARR_BASE_URL=http://localhost:8080 -MAINTAINARR_THEME=blue -MAINTAINARR_THEME_MODE=dark -MAINTAINARR_REFRESH_CRON=@every 5s -``` - -## Release Automation - -- Push or merge into `main` to trigger `.gitea/workflows/release.yml` -- The workflow builds a Docker image, publishes it to the Gitea container registry, and creates or updates a Gitea release -- It tags the image as `latest`, `main`, and the short commit SHA -- Required secret: `GITEA_TOKEN` -- Optional secret: `GITEA_REGISTRY` - Defaults to the host from `gitea.server_url` -- Optional secret: `GITEA_REGISTRY_USERNAME` - Required for container registry login -- Optional secret: `GITEA_REGISTRY_TOKEN` - Defaults to `GITEA_TOKEN` -- Optional secret: `GITEA_PACKAGE_NAMESPACE` - Defaults to the repository owner from `gitea.repository` - -## Roles - -- `admin`: full access, intended for user management and future organization settings -- `editor`: can operate nodes and create automation jobs -- `viewer`: read-only access - -## Theme System - -The dashboard is styled around `uicolors.app` style primary scale variables: - -```css ---color-primary-50 ---color-primary-100 ---color-primary-200 ---color-primary-300 ---color-primary-400 ---color-primary-500 ---color-primary-600 ---color-primary-700 ---color-primary-800 ---color-primary-900 ---color-primary-950 -``` - -Replace those values in `web/static/css/app.css` or feed them through a future admin theme editor to recolor the interface. - -## Current Scope - -This repository now includes the base architecture for: - -- auth and sessions -- role-aware route protection -- dashboard rendering -- node actions -- SSH-backed console transport -- SQLite schema for future fleet features -- recurring automation scheduling - -## Next Expansions - -The backend shape is ready for: - -- user management screens -- node create/edit flows -- SSH key auth -- richer live metrics collection -- audit logs -- package policy models -- triggered event ingestion -- per-group maintenance windows -- multi-step provisioning workflows +- config: `/etc/maintainarr/maintainarr.env` +- data: `/var/lib/maintainarr` +- source: `/opt/maintainarr/src` +- binary: `/opt/maintainarr/bin/maintainarr` diff --git a/install.sh b/install.sh new file mode 100755 index 0000000..3403a0e --- /dev/null +++ b/install.sh @@ -0,0 +1,416 @@ +#!/usr/bin/env bash + +set -euo pipefail + +REPO_URL="https://dock-it.dev/Idea-Studios/Maintainarr.git" +REPO_BRANCH="${MAINTAINARR_BRANCH:-main}" +APP_USER="${MAINTAINARR_USER:-maintainarr}" +INSTALL_ROOT="${MAINTAINARR_INSTALL_ROOT:-/opt/maintainarr}" +SRC_DIR="$INSTALL_ROOT/src" +BIN_DIR="$INSTALL_ROOT/bin" +BIN_PATH="$BIN_DIR/maintainarr" +DATA_DIR="${MAINTAINARR_DATA_DIR:-/var/lib/maintainarr}" +CONFIG_DIR="${MAINTAINARR_CONFIG_DIR:-/etc/maintainarr}" +ENV_FILE="$CONFIG_DIR/maintainarr.env" +SYSTEMD_UNIT="/etc/systemd/system/maintainarr.service" +UPDATE_SCRIPT="/usr/local/bin/maintainarr-update" +CRON_FILE="/etc/cron.d/maintainarr-update" +GO_VERSION="${MAINTAINARR_GO_VERSION:-1.26.4}" + +log() { + printf '[maintainarr] %s\n' "$*" +} + +fail() { + printf '[maintainarr] ERROR: %s\n' "$*" >&2 + exit 1 +} + +require_root() { + if [[ "${EUID}" -ne 0 ]]; then + fail "Run this installer as root, for example: curl -fsSL ... | sudo bash" + fi +} + +command_exists() { + command -v "$1" >/dev/null 2>&1 +} + +apt_package_installed() { + dpkg -s "$1" >/dev/null 2>&1 +} + +version_ge() { + [[ "$(printf '%s\n%s\n' "$2" "$1" | sort -V | head -n1)" == "$2" ]] +} + +detect_arch() { + case "$(uname -m)" in + x86_64|amd64) + echo "amd64" + ;; + aarch64|arm64) + echo "arm64" + ;; + *) + fail "Unsupported architecture: $(uname -m)" + ;; + esac +} + +pkg_install() { + if command_exists apt-get; then + export DEBIAN_FRONTEND=noninteractive + apt-get install -y "$@" + return + fi + + if command_exists dnf; then + dnf install -y "$@" + return + fi + + if command_exists yum; then + yum install -y "$@" + return + fi + + if command_exists pacman; then + pacman -Sy --noconfirm "$@" + return + fi + + if command_exists zypper; then + zypper --non-interactive install "$@" + return + fi + + if command_exists apk; then + apk add --no-cache "$@" + return + fi + + fail "No supported package manager found" +} + +install_base_packages() { + if command_exists apt-get; then + local packages=() + local package + + for package in ca-certificates curl git tar cron; do + if ! apt_package_installed "$package"; then + packages+=("$package") + fi + done + + if [[ "${#packages[@]}" -eq 0 ]]; then + log "System packages already present: ca-certificates curl git tar cron" + return + fi + + log "Installing missing system packages: ${packages[*]}" + export DEBIAN_FRONTEND=noninteractive + apt-get update + apt-get install -y "${packages[@]}" + return + fi + + if command_exists dnf; then + pkg_install ca-certificates curl git tar cronie + return + fi + + if command_exists yum; then + pkg_install ca-certificates curl git tar cronie + return + fi + + if command_exists pacman; then + pkg_install ca-certificates curl git tar cronie + return + fi + + if command_exists zypper; then + pkg_install ca-certificates curl git tar cron + return + fi + + if command_exists apk; then + pkg_install ca-certificates curl git tar dcron + return + fi + + fail "Unable to install prerequisites for this distribution" +} + +install_go() { + local current_version="" + + if command_exists go; then + current_version="$(go version | awk '{print $3}' | sed 's/^go//')" + elif [[ -x /usr/local/go/bin/go ]]; then + current_version="$(/usr/local/go/bin/go version | awk '{print $3}' | sed 's/^go//')" + fi + + if [[ -n "$current_version" ]] && version_ge "$current_version" "$GO_VERSION"; then + log "Go $current_version already satisfies the requirement" + return + fi + + local arch + arch="$(detect_arch)" + local archive="/tmp/go${GO_VERSION}.linux-${arch}.tar.gz" + + log "Installing Go $GO_VERSION" + curl -fsSL "https://go.dev/dl/go${GO_VERSION}.linux-${arch}.tar.gz" -o "$archive" + rm -rf /usr/local/go + tar -C /usr/local -xzf "$archive" + ln -sf /usr/local/go/bin/go /usr/local/bin/go + ln -sf /usr/local/go/bin/gofmt /usr/local/bin/gofmt +} + +tty_available() { + [[ -r /dev/tty && -w /dev/tty ]] +} + +prompt_yes_no() { + local prompt="$1" + local default_answer="$2" + local reply="" + local suffix="[y/N]" + + if [[ "$default_answer" == "y" ]]; then + suffix="[Y/n]" + fi + + if ! tty_available; then + [[ "$default_answer" == "y" ]] + return + fi + + while true; do + printf '%s %s ' "$prompt" "$suffix" > /dev/tty + IFS= read -r reply < /dev/tty || true + reply="${reply:-$default_answer}" + + case "$reply" in + y|Y|yes|YES) + return 0 + ;; + n|N|no|NO) + return 1 + ;; + *) + printf 'Please answer yes or no.\n' > /dev/tty + ;; + esac + done +} + +ensure_user() { + if id -u "$APP_USER" >/dev/null 2>&1; then + return + fi + + local nologin_shell + nologin_shell="$(command -v nologin || true)" + nologin_shell="${nologin_shell:-/usr/sbin/nologin}" + useradd --system --home "$INSTALL_ROOT" --create-home --shell "$nologin_shell" "$APP_USER" +} + +clone_or_update_repo() { + mkdir -p "$INSTALL_ROOT" "$BIN_DIR" + + if [[ -d "$SRC_DIR/.git" ]]; then + log "Updating source in $SRC_DIR" + git -C "$SRC_DIR" fetch --tags origin + git -C "$SRC_DIR" checkout "$REPO_BRANCH" + git -C "$SRC_DIR" pull --ff-only origin "$REPO_BRANCH" + return + fi + + if [[ -f "$SRC_DIR/go.mod" && -d "$SRC_DIR/cmd/maintainarr" ]]; then + log "Existing Maintainarr source found in $SRC_DIR; skipping download" + return + fi + + log "Cloning source into $SRC_DIR" + git clone --branch "$REPO_BRANCH" --single-branch "$REPO_URL" "$SRC_DIR" +} + +build_binary() { + log "Building Maintainarr" + mkdir -p "$BIN_DIR" + ( + cd "$SRC_DIR" + export PATH="/usr/local/go/bin:/usr/local/bin:$PATH" + export CGO_ENABLED=0 + go build -o "$BIN_PATH" ./cmd/maintainarr + ) + chmod 755 "$BIN_PATH" +} + +random_hex() { + local bytes="$1" + od -An -N"$bytes" -tx1 /dev/urandom | tr -d ' \n' +} + +write_env_file() { + mkdir -p "$CONFIG_DIR" "$DATA_DIR/log-archives" + + if [[ -f "$ENV_FILE" ]]; then + log "Keeping existing environment file at $ENV_FILE" + return + fi + + local session_key + local encryption_key + session_key="$(random_hex 32)" + encryption_key="$(random_hex 16)" + + cat > "$ENV_FILE" < "$UPDATE_SCRIPT" <&2 + exit 1 +fi + +git -C "\$SRC_DIR" fetch --tags origin +git -C "\$SRC_DIR" checkout "\$REPO_BRANCH" +git -C "\$SRC_DIR" pull --ff-only origin "\$REPO_BRANCH" + +( + cd "\$SRC_DIR" + export CGO_ENABLED=0 + go build -o "\$BIN_PATH" ./cmd/maintainarr +) + +chown "\$APP_USER:\$APP_USER" "\$BIN_PATH" + +if command -v systemctl >/dev/null 2>&1 && systemctl is-enabled --quiet maintainarr 2>/dev/null; then + systemctl restart maintainarr +fi +EOF + + chmod 755 "$UPDATE_SCRIPT" +} + +write_systemd_service() { + cat > "$SYSTEMD_UNIT" < "$CRON_FILE" <> /var/log/maintainarr-update.log 2>&1 +EOF + + chmod 644 "$CRON_FILE" + + if command_exists systemctl; then + if systemctl list-unit-files | grep -q '^cron\.service'; then + systemctl enable --now cron >/dev/null 2>&1 || true + elif systemctl list-unit-files | grep -q '^crond\.service'; then + systemctl enable --now crond >/dev/null 2>&1 || true + fi + fi + + log "Installed daily update job in $CRON_FILE" +} + +main() { + require_root + install_base_packages + install_go + ensure_user + clone_or_update_repo + write_env_file + build_binary + write_update_script + + mkdir -p "$DATA_DIR" + chown -R "$APP_USER:$APP_USER" "$INSTALL_ROOT" "$DATA_DIR" + chown root:"$APP_USER" "$ENV_FILE" + + if prompt_yes_no "Enable automatic daily updates with cron?" "n"; then + configure_auto_updates + else + rm -f "$CRON_FILE" + log "Skipped cron auto-update setup" + fi + + if prompt_yes_no "Start Maintainarr at boot with systemd?" "y"; then + enable_start_on_boot + else + log "Skipped boot-start setup" + log "Run it manually with: $BIN_PATH" + fi + + log "Install complete" + log "Config: $ENV_FILE" + log "Data: $DATA_DIR" + log "Source: $SRC_DIR" + log "Binary: $BIN_PATH" +} + +main "$@"