Compare commits
1 Commits
dev
...
da89b1135a
| Author | SHA1 | Date | |
|---|---|---|---|
| da89b1135a |
@@ -17,15 +17,15 @@ jobs:
|
||||
GITEA_SERVER_URL: ${{ gitea.server_url }}
|
||||
GITEA_REPOSITORY: ${{ gitea.repository }}
|
||||
GITEA_SHA: ${{ gitea.sha }}
|
||||
GITEA_ACTOR: ${{ gitea.actor }}
|
||||
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
||||
GITEA_REGISTRY: ${{ secrets.GITEA_REGISTRY }}
|
||||
GITEA_REGISTRY_USERNAME: ${{ secrets.GITEA_REGISTRY_USERNAME }}
|
||||
GITEA_REGISTRY_TOKEN: ${{ secrets.GITEA_REGISTRY_TOKEN }}
|
||||
GITEA_PACKAGE_NAMESPACE: ${{ secrets.GITEA_PACKAGE_NAMESPACE }}
|
||||
|
||||
steps:
|
||||
- name: Check out repository
|
||||
uses: https://dock-it.dev/actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
uses: https://dock-it.dev/actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
@@ -52,8 +52,7 @@ jobs:
|
||||
|
||||
registry_username="${GITEA_REGISTRY_USERNAME}"
|
||||
if [ -z "$registry_username" ]; then
|
||||
echo "The repository secret GITEA_REGISTRY_USERNAME is required for container registry login."
|
||||
exit 1
|
||||
registry_username="${GITEA_ACTOR}"
|
||||
fi
|
||||
|
||||
image_ref="${registry_host}/${package_namespace}/${app_name}"
|
||||
@@ -72,22 +71,10 @@ jobs:
|
||||
set -euo pipefail
|
||||
|
||||
if [ -z "$GITEA_TOKEN" ]; then
|
||||
echo "The repository secret GITEA_TOKEN is required to publish releases."
|
||||
echo "The repository secret GITEA_TOKEN is required to publish releases and packages."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
registry_token="${GITEA_REGISTRY_TOKEN}"
|
||||
if [ -z "$registry_token" ]; then
|
||||
registry_token="${GITEA_TOKEN}"
|
||||
fi
|
||||
|
||||
if [ -z "$GITEA_REGISTRY_USERNAME" ]; then
|
||||
echo "The repository secret GITEA_REGISTRY_USERNAME is required to publish container packages."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "REGISTRY_TOKEN=${registry_token}" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Install release dependencies
|
||||
shell: bash
|
||||
run: |
|
||||
@@ -99,7 +86,7 @@ jobs:
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
printf '%s' "$REGISTRY_TOKEN" | docker login "$REGISTRY_HOST" --username "$REGISTRY_USERNAME" --password-stdin
|
||||
printf '%s' "$GITEA_TOKEN" | docker login "$REGISTRY_HOST" --username "$REGISTRY_USERNAME" --password-stdin
|
||||
|
||||
- name: Build container image
|
||||
shell: bash
|
||||
|
||||
@@ -12,10 +12,10 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: https://dock-it.dev/actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
uses: https://dock-it.dev/actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
|
||||
- name: Set up Go
|
||||
uses: https://dock-it.dev/actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6
|
||||
uses: https://dock-it.dev/actions/setup-go@v5
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
cache: true
|
||||
|
||||
@@ -11,7 +11,7 @@ COPY web ./web
|
||||
|
||||
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -trimpath -ldflags="-s -w" -o /out/maintainarr ./cmd/maintainarr
|
||||
|
||||
FROM debian:bookworm-slim@sha256:96e378d7e6531ac9a15ad505478fcc2e69f371b10f5cdf87857c4b8188404716
|
||||
FROM debian:bookworm-slim
|
||||
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y --no-install-recommends ca-certificates tzdata \
|
||||
|
||||
161
README.md
161
README.md
@@ -1,45 +1,150 @@
|
||||

|
||||
|
||||
# Maintainarr
|
||||
|
||||
Maintainarr is a self-hosted dashboard for managing Linux VMs from one place.
|
||||
Maintainarr is a self-hosted Go application for managing a single organization's Linux VM fleet from one dashboard.
|
||||
|
||||
## Install
|
||||
It ships with:
|
||||
|
||||
```bash
|
||||
curl -fsSL https://dock-it.dev/Idea-Studios/Maintainarr/raw/branch/main/install.sh | sudo bash
|
||||
```
|
||||
- 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
|
||||
|
||||
The installer will:
|
||||
## Branch Model
|
||||
|
||||
- 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`
|
||||
- `main`: stable branch
|
||||
- `dev`: active feature branch
|
||||
|
||||
## Open
|
||||
The repository has been initialized with both `main` and `dev`. Work should land in `dev` first.
|
||||
|
||||
After install, open:
|
||||
## 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
|
||||
http://your-server-ip:8080
|
||||
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
|
||||
```
|
||||
|
||||
The first registered user becomes the admin.
|
||||
## Run
|
||||
|
||||
## Manual Run
|
||||
|
||||
```bash
|
||||
cd /opt/maintainarr/src
|
||||
```powershell
|
||||
go run ./cmd/maintainarr
|
||||
```
|
||||
|
||||
## Important Paths
|
||||
Default address: `http://localhost:8080`
|
||||
|
||||
- config: `/etc/maintainarr/maintainarr.env`
|
||||
- data: `/var/lib/maintainarr`
|
||||
- source: `/opt/maintainarr/src`
|
||||
- binary: `/opt/maintainarr/bin/maintainarr`
|
||||
## 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`
|
||||
Defaults to `gitea.actor`
|
||||
- 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
|
||||
|
||||
4
go.mod
4
go.mod
@@ -10,7 +10,7 @@ require (
|
||||
github.com/robfig/cron/v3 v3.0.1
|
||||
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
|
||||
golang.org/x/crypto v0.53.0
|
||||
modernc.org/sqlite v1.53.0
|
||||
modernc.org/sqlite v1.52.0
|
||||
)
|
||||
|
||||
require (
|
||||
@@ -22,7 +22,7 @@ require (
|
||||
github.com/ncruces/go-strftime v1.0.0 // indirect
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||
golang.org/x/sys v0.46.0 // indirect
|
||||
modernc.org/libc v1.73.4 // indirect
|
||||
modernc.org/libc v1.72.3 // indirect
|
||||
modernc.org/mathutil v1.7.1 // indirect
|
||||
modernc.org/memory v1.11.0 // indirect
|
||||
)
|
||||
|
||||
4
go.sum
4
go.sum
@@ -35,13 +35,9 @@ golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw=
|
||||
golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
modernc.org/libc v1.72.3 h1:ZnDF4tXn4NBXFutMMQC4vtbTFSXhhKzR73fv0beZEAU=
|
||||
modernc.org/libc v1.72.3/go.mod h1:dn0dZNnnn1clLyvRxLxYExxiKRZIRENOfqQ8XEeg4Qs=
|
||||
modernc.org/libc v1.73.4 h1:+ra4Ui8ngyt8HDcO1FTDPWlkAh6yOdaO2yAoh8MddQA=
|
||||
modernc.org/libc v1.73.4/go.mod h1:DXZ3eO8qMCNn2SnmTNCiC71nJ9Rcq3PsnpU6Vc4rWK8=
|
||||
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
|
||||
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
|
||||
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
|
||||
modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
|
||||
modernc.org/sqlite v1.52.0 h1:p4dhYh2tXZCiyaqHwRVJDjIGKWyXayiQpThxgDzJaxo=
|
||||
modernc.org/sqlite v1.52.0/go.mod h1:tcNzv5p84E0skkmJn038y+hWJbLQXQqEnQfeh5r2JLM=
|
||||
modernc.org/sqlite v1.53.0 h1:20WG8N9q4ji/dEqGk4uiI0c6OPjSeLTNYGFCc3+7c1M=
|
||||
modernc.org/sqlite v1.53.0/go.mod h1:xoEpOIpGrgT48H5iiyt/YXPCZPEzlfmfFwtk8Lklw8s=
|
||||
|
||||
494
install.sh
494
install.sh
@@ -1,494 +0,0 @@
|
||||
#!/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}"
|
||||
INSTALL_COMMAND="curl -fsSL https://dock-it.dev/Idea-Studios/Maintainarr/raw/branch/main/install.sh | sudo bash"
|
||||
|
||||
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'
|
||||
}
|
||||
|
||||
env_value() {
|
||||
local key="$1"
|
||||
awk -F= -v target="$key" '$1 == target { sub(/^[^=]+=*/, "", $0); print $0; exit }' "$ENV_FILE"
|
||||
}
|
||||
|
||||
extract_port() {
|
||||
local addr="$1"
|
||||
|
||||
if [[ "$addr" =~ :([0-9]+)$ ]]; then
|
||||
echo "${BASH_REMATCH[1]}"
|
||||
return
|
||||
fi
|
||||
|
||||
echo "8080"
|
||||
}
|
||||
|
||||
detect_local_ip() {
|
||||
local ip=""
|
||||
|
||||
if command_exists hostname; then
|
||||
ip="$(hostname -I 2>/dev/null | awk '{print $1}')"
|
||||
fi
|
||||
|
||||
if [[ -z "$ip" ]] && command_exists ip; then
|
||||
ip="$(ip route get 1.1.1.1 2>/dev/null | awk '/src/ {for (i=1;i<=NF;i++) if ($i=="src") {print $(i+1); exit}}')"
|
||||
fi
|
||||
|
||||
if [[ -z "$ip" ]]; then
|
||||
ip="127.0.0.1"
|
||||
fi
|
||||
|
||||
echo "$ip"
|
||||
}
|
||||
|
||||
print_summary() {
|
||||
local local_ip="$1"
|
||||
local port="$2"
|
||||
local auto_updates="$3"
|
||||
local start_on_boot="$4"
|
||||
local addr base_url
|
||||
|
||||
addr="$(env_value "MAINTAINARR_ADDR")"
|
||||
base_url="$(env_value "MAINTAINARR_BASE_URL")"
|
||||
|
||||
printf '\n'
|
||||
printf 'Maintainarr install complete\n'
|
||||
printf 'Local URL: http://%s:%s\n' "$local_ip" "$port"
|
||||
printf 'Base URL: %s\n' "$base_url"
|
||||
printf '\n'
|
||||
printf 'Settings\n'
|
||||
printf 'MAINTAINARR_ADDR=%s\n' "$addr"
|
||||
printf 'MAINTAINARR_DB_PATH=%s\n' "$(env_value "MAINTAINARR_DB_PATH")"
|
||||
printf 'MAINTAINARR_LOG_ARCHIVE_DIR=%s\n' "$(env_value "MAINTAINARR_LOG_ARCHIVE_DIR")"
|
||||
printf 'MAINTAINARR_ORG_NAME=%s\n' "$(env_value "MAINTAINARR_ORG_NAME")"
|
||||
printf 'MAINTAINARR_BASE_URL=%s\n' "$base_url"
|
||||
printf 'MAINTAINARR_THEME=%s\n' "$(env_value "MAINTAINARR_THEME")"
|
||||
printf 'MAINTAINARR_THEME_MODE=%s\n' "$(env_value "MAINTAINARR_THEME_MODE")"
|
||||
printf 'MAINTAINARR_REFRESH_CRON=%s\n' "$(env_value "MAINTAINARR_REFRESH_CRON")"
|
||||
printf 'AUTO_UPDATES=%s\n' "$auto_updates"
|
||||
printf 'START_ON_BOOT=%s\n' "$start_on_boot"
|
||||
printf '\n'
|
||||
printf 'Paths\n'
|
||||
printf 'Config: %s\n' "$ENV_FILE"
|
||||
printf 'Data: %s\n' "$DATA_DIR"
|
||||
printf 'Source: %s\n' "$SRC_DIR"
|
||||
printf 'Binary: %s\n' "$BIN_PATH"
|
||||
printf '\n'
|
||||
printf 'Re-run installer\n'
|
||||
printf '%s\n' "$INSTALL_COMMAND"
|
||||
}
|
||||
|
||||
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" <<EOF
|
||||
MAINTAINARR_ADDR=:8080
|
||||
MAINTAINARR_DB_PATH=$DATA_DIR/maintainarr.db
|
||||
MAINTAINARR_LOG_ARCHIVE_DIR=$DATA_DIR/log-archives
|
||||
MAINTAINARR_SESSION_KEY=$session_key
|
||||
MAINTAINARR_ENCRYPTION_KEY=$encryption_key
|
||||
MAINTAINARR_ORG_NAME=Maintainarr
|
||||
MAINTAINARR_BASE_URL=http://localhost:8080
|
||||
MAINTAINARR_THEME=blue
|
||||
MAINTAINARR_THEME_MODE=dark
|
||||
MAINTAINARR_REFRESH_CRON=@every 5s
|
||||
EOF
|
||||
|
||||
chmod 640 "$ENV_FILE"
|
||||
}
|
||||
|
||||
write_update_script() {
|
||||
cat > "$UPDATE_SCRIPT" <<EOF
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
export PATH="/usr/local/go/bin:/usr/local/bin:/usr/bin:/bin"
|
||||
|
||||
APP_USER="$APP_USER"
|
||||
SRC_DIR="$SRC_DIR"
|
||||
BIN_PATH="$BIN_PATH"
|
||||
REPO_BRANCH="$REPO_BRANCH"
|
||||
|
||||
if [[ ! -d "\$SRC_DIR/.git" ]]; then
|
||||
echo "Maintainarr source directory not found: \$SRC_DIR" >&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" <<EOF
|
||||
[Unit]
|
||||
Description=Maintainarr service
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=$APP_USER
|
||||
Group=$APP_USER
|
||||
WorkingDirectory=$SRC_DIR
|
||||
EnvironmentFile=$ENV_FILE
|
||||
ExecStart=$BIN_PATH
|
||||
Restart=on-failure
|
||||
RestartSec=5
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOF
|
||||
}
|
||||
|
||||
enable_start_on_boot() {
|
||||
if ! command_exists systemctl; then
|
||||
log "systemd is not available; skipping boot-start setup"
|
||||
return
|
||||
fi
|
||||
|
||||
write_systemd_service
|
||||
systemctl daemon-reload
|
||||
systemctl enable --now maintainarr
|
||||
log "Enabled and started maintainarr.service"
|
||||
}
|
||||
|
||||
configure_auto_updates() {
|
||||
write_update_script
|
||||
|
||||
cat > "$CRON_FILE" <<EOF
|
||||
SHELL=/bin/bash
|
||||
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
|
||||
17 3 * * * root $UPDATE_SCRIPT >> /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() {
|
||||
local auto_updates="disabled"
|
||||
local start_on_boot="disabled"
|
||||
local addr port local_ip
|
||||
|
||||
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
|
||||
auto_updates="enabled"
|
||||
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
|
||||
start_on_boot="enabled"
|
||||
else
|
||||
log "Skipped boot-start setup"
|
||||
log "Run it manually with: $BIN_PATH"
|
||||
fi
|
||||
|
||||
addr="$(env_value "MAINTAINARR_ADDR")"
|
||||
port="$(extract_port "$addr")"
|
||||
local_ip="$(detect_local_ip)"
|
||||
|
||||
print_summary "$local_ip" "$port" "$auto_updates" "$start_on_boot"
|
||||
}
|
||||
|
||||
main "$@"
|
||||
Reference in New Issue
Block a user