From 1d1d6c605cf7c35281991159d681d75410903b55 Mon Sep 17 00:00:00 2001 From: Paul Payne Date: Sun, 12 Jul 2026 21:40:15 +0000 Subject: [PATCH] Adds dist. --- VERSION | 1 + dist/.gitignore | 30 ++ dist/Makefile | 164 +++++++++++ dist/README.md | 76 +++++ dist/debian/DEBIAN/control | 12 + dist/debian/DEBIAN/postinst | 262 ++++++++++++++++++ dist/debian/DEBIAN/postrm | 80 ++++++ dist/debian/DEBIAN/prerm | 28 ++ .../etc/nginx/sites-available/wild-central | 34 +++ .../wild-central-nftables-reload.service | 9 + .../etc/systemd/system/wild-central.service | 28 ++ .../etc/wild-central/config.yaml.example | 42 +++ dist/scripts/build-package.sh | 71 +++++ dist/scripts/create-gitea-release.sh | 189 +++++++++++++ dist/scripts/deploy-apt-repository.sh | 138 +++++++++ dist/scripts/package-deb.sh | 49 ++++ web/BUILDING_WILD_APP.md | 4 +- web/README.md | 10 +- 18 files changed, 1219 insertions(+), 8 deletions(-) create mode 100644 VERSION create mode 100644 dist/.gitignore create mode 100644 dist/Makefile create mode 100644 dist/README.md create mode 100644 dist/debian/DEBIAN/control create mode 100644 dist/debian/DEBIAN/postinst create mode 100644 dist/debian/DEBIAN/postrm create mode 100644 dist/debian/DEBIAN/prerm create mode 100644 dist/debian/etc/nginx/sites-available/wild-central create mode 100644 dist/debian/etc/systemd/system/wild-central-nftables-reload.service create mode 100644 dist/debian/etc/systemd/system/wild-central.service create mode 100644 dist/debian/etc/wild-central/config.yaml.example create mode 100755 dist/scripts/build-package.sh create mode 100755 dist/scripts/create-gitea-release.sh create mode 100755 dist/scripts/deploy-apt-repository.sh create mode 100755 dist/scripts/package-deb.sh diff --git a/VERSION b/VERSION new file mode 100644 index 0000000..6e8bf73 --- /dev/null +++ b/VERSION @@ -0,0 +1 @@ +0.1.0 diff --git a/dist/.gitignore b/dist/.gitignore new file mode 100644 index 0000000..c52045d --- /dev/null +++ b/dist/.gitignore @@ -0,0 +1,30 @@ +# Local dev secrets +.envrc + +# Build artifacts +build/ +dist/ +*.deb +__debug* + +# APT repository build artifacts +.aptly/ +.aptly.conf + +# Go build cache +*.o +*.a +*.so + +# Test binary +*.test + +# Coverage +*.out +*.cover +*.coverage + +# Go workspace +go.work +go.work.sum +gpg/wild-central.gpg diff --git a/dist/Makefile b/dist/Makefile new file mode 100644 index 0000000..4b58a1f --- /dev/null +++ b/dist/Makefile @@ -0,0 +1,164 @@ +# Default target +.DEFAULT_GOAL := help + +# Build configuration +VERSION := $(shell cat ../VERSION 2>/dev/null || echo "0.0.0-dev") +BUILD_DIR := build +DIST_DIR := dist +DEB_DIR := debian-package + +# Git information +GIT_COMMIT := $(shell git rev-parse --short HEAD 2>/dev/null || echo "unknown") +BUILD_TIME := $(shell date -u '+%Y-%m-%d_%H:%M:%S') +EDGE_VERSION := 0.0.0~edge.$(shell date -u '+%Y%m%d%H%M%S').$(GIT_COMMIT) + +# Source paths +API_SOURCE := .. +WEB_SOURCE := ../web + +# Output files +ARM64_BINARY := $(BUILD_DIR)/wild-central-arm64 +AMD64_BINARY := $(BUILD_DIR)/wild-central-amd64 +WEB_DIST_MARKER := $(BUILD_DIR)/.web-built +ARM64_DEB := $(DIST_DIR)/packages/wild-central_$(VERSION)_arm64.deb +AMD64_DEB := $(DIST_DIR)/packages/wild-central_$(VERSION)_amd64.deb + +# Gitea release configuration +GITEA_URL := https://git.civilsociety.dev +GITEA_OWNER := wild-cloud +GITEA_REPO := wild-central +RELEASE_TAG := v$(VERSION) + +.PHONY: help clean version build build-all package package-all repo deploy-repo release release-edge + +help: + @echo "Wild Central Package Builder" + @echo "" + @echo "This directory builds .deb packages for Wild Central." + @echo "For development, use the parent directory:" + @echo " - make dev (Go daemon)" + @echo " - cd web && pnpm dev (React frontend)" + @echo "" + @echo "Build targets:" + @echo " build - Build for current architecture (amd64)" + @echo " build-all - Build all architectures" + @echo "" + @echo "Package targets:" + @echo " package - Create .deb package (amd64)" + @echo " package-all - Create all .deb packages" + @echo "" + @echo "Repository targets:" + @echo " deploy-repo - Deploy repository to server" + @echo "" + @echo "Release targets:" + @echo " release - Tag HEAD as v$$(cat ../VERSION), publish stable Gitea release + APT" + @echo " release-edge - Publish rolling edge Gitea release + APT (no version bump needed)" + @echo "" + @echo "Utilities:" + @echo " clean - Remove all build artifacts" + @echo " version - Show build information" + @echo "" + @echo " Current stable version: $(VERSION) (from ../VERSION)" + @echo " Current edge version: $(EDGE_VERSION)" + @echo "" + @echo "Environment variables:" + @echo " GITEA_TOKEN - Gitea API token (required for release)" + @echo " APTLY_URL - Aptly instance URL (required for APT deployment)" + @echo " APTLY_PASS - Aptly API password" + @echo " Run: source .envrc" + +clean: + @echo "Cleaning build artifacts..." + @rm -rf $(BUILD_DIR) $(DIST_DIR) $(DEB_DIR)-* $(DEB_DIR) + +version: + @echo "Version: $(VERSION)" + @echo "Git Commit: $(GIT_COMMIT)" + @echo "Build Time: $(BUILD_TIME)" + +# Web app build - track with marker file +$(WEB_DIST_MARKER): $(shell find $(WEB_SOURCE)/src -type f 2>/dev/null | head -100) $(WEB_SOURCE)/package.json + @echo "Building web application..." + @ARCH=amd64 VERSION=$(VERSION) ./scripts/build-package.sh + @touch $(WEB_DIST_MARKER) + +# ARM64 binary +$(ARM64_BINARY): $(shell find $(API_SOURCE) -name '*.go' -not -path '*/dist/*' -not -path '*/web/*' 2>/dev/null | head -100) + @echo "Building arm64 binary..." + @mkdir -p $(BUILD_DIR) + @cd $(API_SOURCE) && GOOS=linux GOARCH=arm64 go build -ldflags="-X main.Version=$(VERSION)" -o dist/$(ARM64_BINARY) . + +# AMD64 binary +$(AMD64_BINARY): $(shell find $(API_SOURCE) -name '*.go' -not -path '*/dist/*' -not -path '*/web/*' 2>/dev/null | head -100) + @echo "Building amd64 binary..." + @mkdir -p $(BUILD_DIR) + @cd $(API_SOURCE) && GOOS=linux GOARCH=amd64 go build -ldflags="-X main.Version=$(VERSION)" -o dist/$(AMD64_BINARY) . + +# ARM64 package +$(ARM64_DEB): $(ARM64_BINARY) $(WEB_DIST_MARKER) + @echo "Creating arm64 .deb package..." + @rm -f $(DIST_DIR)/packages/wild-central_*_arm64.deb + @./scripts/package-deb.sh arm64 $(ARM64_BINARY) $(VERSION) + +# AMD64 package +$(AMD64_DEB): $(AMD64_BINARY) $(WEB_DIST_MARKER) + @echo "Creating amd64 .deb package..." + @rm -f $(DIST_DIR)/packages/wild-central_*_amd64.deb + @./scripts/package-deb.sh amd64 $(AMD64_BINARY) $(VERSION) + +# Convenience targets +build: $(AMD64_BINARY) $(WEB_DIST_MARKER) + +build-all: $(ARM64_BINARY) $(AMD64_BINARY) $(WEB_DIST_MARKER) + +package: $(AMD64_DEB) + +package-all: $(ARM64_DEB) $(AMD64_DEB) + +deploy-repo: $(ARM64_DEB) $(AMD64_DEB) + @echo "Deploying APT repository..." + @if [ -z "$(APTLY_URL)" ] || [ -z "$(APTLY_PASS)" ]; then \ + echo "Error: APTLY_URL and APTLY_PASS must be set"; \ + echo " Run: source .envrc"; \ + exit 1; \ + fi + @./scripts/deploy-apt-repository.sh + +release: $(ARM64_DEB) $(AMD64_DEB) + @echo "Creating stable release $(RELEASE_TAG)..." + @if [ -z "$(GITEA_TOKEN)" ]; then \ + echo "Error: GITEA_TOKEN environment variable not set"; \ + echo " Get a token from $(GITEA_URL)/user/settings/applications"; \ + echo " Or run: source .envrc"; \ + exit 1; \ + fi + @if git tag | grep -q "^$(RELEASE_TAG)$$"; then \ + echo "Tag $(RELEASE_TAG) already exists — updating release assets only"; \ + else \ + echo "Tagging HEAD as $(RELEASE_TAG)..."; \ + git tag -a $(RELEASE_TAG) -m "Wild Central $(VERSION)"; \ + git push origin $(RELEASE_TAG); \ + fi + @./scripts/create-gitea-release.sh "$(GITEA_URL)" "$(GITEA_OWNER)" "$(GITEA_REPO)" "$(RELEASE_TAG)" "$(VERSION)" "$(GITEA_TOKEN)" stable + @if [ -n "$(APTLY_URL)" ] && [ -n "$(APTLY_PASS)" ]; then \ + ./scripts/deploy-apt-repository.sh; \ + else \ + echo "Skipping APT deployment (APTLY_URL/APTLY_PASS not set)"; \ + fi + +release-edge: $(ARM64_DEB) $(AMD64_DEB) + @echo "Creating edge release ($(EDGE_VERSION))..." + @if [ -z "$(GITEA_TOKEN)" ]; then \ + echo "Error: GITEA_TOKEN environment variable not set"; \ + echo " Or run: source .envrc"; \ + exit 1; \ + fi + @echo "Force-pushing edge tag to HEAD..." + @git tag -f edge + @git push origin edge --force + @./scripts/create-gitea-release.sh "$(GITEA_URL)" "$(GITEA_OWNER)" "$(GITEA_REPO)" "edge" "$(EDGE_VERSION)" "$(GITEA_TOKEN)" edge + @if [ -n "$(APTLY_URL)" ] && [ -n "$(APTLY_PASS)" ]; then \ + DIST=edge ./scripts/deploy-apt-repository.sh; \ + else \ + echo "Skipping APT deployment (APTLY_URL/APTLY_PASS not set)"; \ + fi diff --git a/dist/README.md b/dist/README.md new file mode 100644 index 0000000..b4d44c7 --- /dev/null +++ b/dist/README.md @@ -0,0 +1,76 @@ +# Packaging Wild Central + +## Installation + +### APT Repository (recommended) + +```bash +# Import the signing key +curl -fsSL https://aptly.cloud2.payne.io/wild-central.gpg \ + | sudo tee /usr/share/keyrings/wild-central.gpg > /dev/null + +# Add the repository +sudo tee /etc/apt/sources.list.d/wild-central.sources < +Description: Wild Central - Networking and Coordination Platform + Manages DNS, gateway routing, firewall, VPN, TLS certificates, and tunnels + for Wild Cloud and other self-hosted services on the local network. diff --git a/dist/debian/DEBIAN/postinst b/dist/debian/DEBIAN/postinst new file mode 100644 index 0000000..d8665e6 --- /dev/null +++ b/dist/debian/DEBIAN/postinst @@ -0,0 +1,262 @@ +#!/bin/bash + +set -e + +case "$1" in + configure) + echo "Configuring wild-central..." + + # Create wildcloud user if it doesn't exist + if ! id wildcloud >/dev/null 2>&1; then + useradd --system --home-dir /var/lib/wild-central --create-home --shell /bin/false wildcloud + fi + + # Create required directories + mkdir -p /var/lib/wild-central + mkdir -p /var/log/wild-central + mkdir -p /etc/wild-central + + # Set ownership + chown -R wildcloud:wildcloud /var/lib/wild-central + chown wildcloud:wildcloud /var/log/wild-central + + # Set up dnsmasq directories and permissions + mkdir -p /etc/dnsmasq.d + mkdir -p /etc/dnsmasq.d/wild-cloud-instances + + # Give wildcloud group write access to dnsmasq.d directory + chgrp wildcloud /etc/dnsmasq.d + chmod 775 /etc/dnsmasq.d + + # Create or fix ownership of wild-cloud.conf + if [ ! -f /etc/dnsmasq.d/wild-cloud.conf ]; then + touch /etc/dnsmasq.d/wild-cloud.conf + echo "Created /etc/dnsmasq.d/wild-cloud.conf" + else + echo "Found existing /etc/dnsmasq.d/wild-cloud.conf - updating ownership" + fi + + chown wildcloud:wildcloud /etc/dnsmasq.d/wild-cloud.conf + chmod 644 /etc/dnsmasq.d/wild-cloud.conf + + # Set ownership and permissions for instance configs directory + chown wildcloud:wildcloud /etc/dnsmasq.d/wild-cloud-instances + chmod 755 /etc/dnsmasq.d/wild-cloud-instances + echo "Created /etc/dnsmasq.d/wild-cloud-instances/ for per-instance DNS configs" + + # Set up systemd-resolved configuration directory and file + mkdir -p /etc/systemd/resolved.conf.d + + if [ ! -f /etc/systemd/resolved.conf.d/wild-cloud.conf ]; then + touch /etc/systemd/resolved.conf.d/wild-cloud.conf + echo "Created /etc/systemd/resolved.conf.d/wild-cloud.conf" + else + echo "Found existing /etc/systemd/resolved.conf.d/wild-cloud.conf" + fi + chown wildcloud:wildcloud /etc/systemd/resolved.conf.d/wild-cloud.conf + chmod 644 /etc/systemd/resolved.conf.d/wild-cloud.conf + + # Ensure /etc/resolv.conf is a symlink to systemd-resolved stub + # Skip in Docker/container environments where resolv.conf might be bind-mounted + if [ ! -L /etc/resolv.conf ] || [ "$(readlink /etc/resolv.conf)" != "/run/systemd/resolve/stub-resolv.conf" ]; then + echo "Configuring /etc/resolv.conf to use systemd-resolved stub mode" + if [ -f /etc/resolv.conf ] && [ ! -L /etc/resolv.conf ]; then + cp /etc/resolv.conf /etc/resolv.conf.wild-backup 2>/dev/null || echo "Note: Could not backup /etc/resolv.conf (may be read-only)" + fi + if rm -f /etc/resolv.conf 2>/dev/null; then + ln -sf /run/systemd/resolve/stub-resolv.conf /etc/resolv.conf + echo "Set /etc/resolv.conf to use systemd-resolved stub mode" + else + echo "Note: /etc/resolv.conf is read-only or busy (container environment?), skipping symlink setup" + fi + fi + + # Set up HAProxy configuration directory + mkdir -p /etc/haproxy + if [ ! -f /etc/haproxy/haproxy.cfg ]; then + touch /etc/haproxy/haproxy.cfg + echo "Created /etc/haproxy/haproxy.cfg" + fi + chown wildcloud:wildcloud /etc/haproxy + chown wildcloud:wildcloud /etc/haproxy/haproxy.cfg + chmod 644 /etc/haproxy/haproxy.cfg + + # Set up HAProxy TLS certs directory + mkdir -p /etc/haproxy/certs + chown wildcloud:wildcloud /etc/haproxy/certs + chmod 700 /etc/haproxy/certs + echo "Configured HAProxy for Wild Central management" + + # Set up WireGuard configuration directory + mkdir -p /etc/wireguard + chown wildcloud:wildcloud /etc/wireguard + chmod 700 /etc/wireguard + if [ ! -f /etc/wireguard/wg0.conf ]; then + touch /etc/wireguard/wg0.conf + echo "Created /etc/wireguard/wg0.conf" + fi + chown wildcloud:wildcloud /etc/wireguard/wg0.conf + chmod 600 /etc/wireguard/wg0.conf + echo "Configured WireGuard for Wild Central management" + + # Set up nftables configuration directory + mkdir -p /etc/nftables.d + if [ ! -f /etc/nftables.d/wild-cloud.nft ]; then + touch /etc/nftables.d/wild-cloud.nft + echo "Created /etc/nftables.d/wild-cloud.nft" + fi + chown wildcloud:wildcloud /etc/nftables.d/wild-cloud.nft + chmod 644 /etc/nftables.d/wild-cloud.nft + + # Ensure nftables.conf includes wild-cloud rules + if [ -f /etc/nftables.conf ] && ! grep -q 'wild-cloud.nft' /etc/nftables.conf; then + echo 'include "/etc/nftables.d/wild-cloud.nft"' >> /etc/nftables.conf + echo "Added Wild Central nftables include to /etc/nftables.conf" + fi + echo "Configured nftables for Wild Central management" + + # Install CrowdSec if not already installed + if ! command -v cscli >/dev/null 2>&1; then + echo "Installing CrowdSec..." + curl -s https://packagecloud.io/install/repositories/crowdsec/crowdsec/script.deb.sh | os=ubuntu dist=noble bash + apt-get install -y crowdsec + echo "Installed CrowdSec" + else + echo "CrowdSec already installed: $(cscli version 2>/dev/null | head -1 || echo 'unknown')" + fi + + # Configure CrowdSec LAPI to listen on all interfaces + CROWDSEC_CONFIG=/etc/crowdsec/config.yaml + if [ -f "$CROWDSEC_CONFIG" ] && command -v yq >/dev/null 2>&1; then + yq e -i '.api.server.listen_uri = "0.0.0.0:8080"' "$CROWDSEC_CONFIG" + echo "Configured CrowdSec LAPI to listen on 0.0.0.0:8080" + fi + + # Ensure CrowdSec is running so the firewall bouncer can register with LAPI + if [ -d /run/systemd/system ]; then + systemctl start crowdsec 2>/dev/null || true + sleep 2 + fi + + # Install CrowdSec firewall bouncer + if ! command -v crowdsec-firewall-bouncer >/dev/null 2>&1; then + echo "Installing CrowdSec firewall bouncer..." + apt-get install -y crowdsec-firewall-bouncer + echo "Installed CrowdSec firewall bouncer" + else + echo "CrowdSec firewall bouncer already installed" + fi + + # Install Authelia if not already installed + if ! command -v authelia >/dev/null 2>&1; then + echo "Installing Authelia..." + curl -fsSL https://apt.authelia.com/organization/signing.asc | \ + gpg --dearmor -o /usr/share/keyrings/authelia-archive-keyring.gpg + echo "deb [signed-by=/usr/share/keyrings/authelia-archive-keyring.gpg] \ + https://apt.authelia.com/integration/apt/repo/stable/debian debian main" \ + > /etc/apt/sources.list.d/authelia.list + apt-get update -qq + apt-get install -y authelia + echo "Installed Authelia" + else + echo "Authelia already installed" + fi + + # Install HAProxy Lua scripts for Authelia auth-request forwarding + mkdir -p /etc/haproxy/lua + if [ ! -f /etc/haproxy/lua/haproxy-auth-request.lua ]; then + curl -fsSL -o /etc/haproxy/lua/haproxy-auth-request.lua \ + https://raw.githubusercontent.com/TimWolla/haproxy-auth-request/main/auth-request.lua + echo "Installed haproxy-auth-request.lua" + fi + if [ ! -f /etc/haproxy/lua/haproxy-lua-http.lua ]; then + curl -fsSL -o /etc/haproxy/lua/haproxy-lua-http.lua \ + https://raw.githubusercontent.com/haproxytech/haproxy-lua-http/master/http.lua + echo "Installed haproxy-lua-http.lua" + fi + apt-get install -y lua-json 2>/dev/null || true + + # Configure Authelia systemd override to use Wild Central config path + mkdir -p /etc/systemd/system/authelia.service.d + cat > /etc/systemd/system/authelia.service.d/wild-central.conf << 'AUTHELIA_EOF' +[Service] +ExecStart= +ExecStart=/usr/bin/authelia --config /var/lib/wild-central/authelia/configuration.yml +AUTHELIA_EOF + echo "Configured Authelia systemd override" + + # Create Authelia data directory + mkdir -p /var/lib/wild-central/authelia + chown wildcloud:wildcloud /var/lib/wild-central/authelia + chmod 700 /var/lib/wild-central/authelia + + # Install sudoers rules for privileged operations + cat > /etc/sudoers.d/wild-central << 'SUDOERS_EOF' +wildcloud ALL=(ALL) NOPASSWD: /usr/sbin/nft -c -f *, /usr/sbin/nft list table inet wild-cloud, /usr/bin/cscli bouncers *, /usr/bin/cscli machines *, /usr/bin/cscli decisions *, /usr/bin/cscli alerts *, /usr/bin/cscli version, /usr/bin/wg-quick up wg0, /usr/bin/wg-quick down wg0, /usr/bin/wg show wg0, /usr/bin/certbot *, /usr/sbin/nginx -t, /usr/bin/systemctl reload nginx +SUDOERS_EOF + chmod 440 /etc/sudoers.d/wild-central + echo "Installed sudoers rules for Wild Central" + + # Install polkit rules for service management + mkdir -p /etc/polkit-1/rules.d + cat > /etc/polkit-1/rules.d/50-wild-central.rules << 'POLKIT_EOF' +/* Allow wildcloud user to manage Wild Central services */ +polkit.addRule(function(action, subject) { + if (action.id == "org.freedesktop.systemd1.manage-units" && + (action.lookup("unit") == "dnsmasq.service" || + action.lookup("unit") == "systemd-resolved.service" || + action.lookup("unit") == "haproxy.service" || + action.lookup("unit") == "wild-central-nftables-reload.service" || + action.lookup("unit") == "crowdsec.service" || + action.lookup("unit") == "crowdsec-firewall-bouncer.service" || + action.lookup("unit") == "wg-quick@wg0.service" || + action.lookup("unit") == "cloudflared.service" || + action.lookup("unit") == "authelia.service" || + action.lookup("unit") == "wild-central.service") && + subject.user == "wildcloud") { + return polkit.Result.YES; + } +}); +POLKIT_EOF + chmod 644 /etc/polkit-1/rules.d/50-wild-central.rules + echo "Installed polkit rules for service management" + + # Enable nginx site (disable default site, enable wild-central) + if [ -f /etc/nginx/sites-enabled/default ]; then + rm -f /etc/nginx/sites-enabled/default + echo "Disabled default nginx site" + fi + if [ ! -L /etc/nginx/sites-enabled/wild-central ]; then + ln -sf /etc/nginx/sites-available/wild-central /etc/nginx/sites-enabled/wild-central + echo "Enabled wild-central nginx site" + fi + + # Reload nginx to pick up the new site + if systemctl is-active --quiet nginx; then + nginx -t && systemctl reload nginx + echo "Reloaded nginx configuration" + fi + + # Enable and start the service + if [ -d /run/systemd/system ]; then + systemctl daemon-reload + systemctl enable wild-central.service + echo "wild-central configured successfully" + echo "Start the service with: sudo systemctl start wild-central" + echo "View logs with: sudo journalctl -u wild-central -f" + else + echo "Note: systemd not detected (container environment?), skipping service enable" + echo "wild-central installed successfully" + fi + ;; + + abort-upgrade|abort-remove|abort-deconfigure) + ;; + + *) + echo "postinst called with unknown argument \`$1'" >&2 + exit 1 + ;; +esac + +exit 0 diff --git a/dist/debian/DEBIAN/postrm b/dist/debian/DEBIAN/postrm new file mode 100644 index 0000000..c08893d --- /dev/null +++ b/dist/debian/DEBIAN/postrm @@ -0,0 +1,80 @@ +#!/bin/bash + +set -e + +case "$1" in + purge) + echo "Purging wild-central configuration..." + + # Remove polkit rules + if [ -f /etc/polkit-1/rules.d/50-wild-central.rules ]; then + rm -f /etc/polkit-1/rules.d/50-wild-central.rules + echo "Removed polkit rules" + fi + + # Remove sudoers file + if [ -f /etc/sudoers.d/wild-central ]; then + rm -f /etc/sudoers.d/wild-central + fi + + # Remove dnsmasq configuration + if [ -f /etc/dnsmasq.d/wild-cloud.conf ]; then + rm -f /etc/dnsmasq.d/wild-cloud.conf + echo "Removed dnsmasq configuration" + fi + + # Remove systemd-resolved configuration + if [ -f /etc/systemd/resolved.conf.d/wild-cloud.conf ]; then + rm -f /etc/systemd/resolved.conf.d/wild-cloud.conf + echo "Removed systemd-resolved configuration" + systemctl restart systemd-resolved 2>/dev/null || true + fi + + # Restore /etc/resolv.conf if we have a backup + if [ -f /etc/resolv.conf.wild-backup ]; then + echo "Restoring original /etc/resolv.conf from backup" + rm -f /etc/resolv.conf + mv /etc/resolv.conf.wild-backup /etc/resolv.conf + fi + + # Remove configuration directory + if [ -d /etc/wild-central ]; then + rm -rf /etc/wild-central + fi + + # Remove log directory + if [ -d /var/log/wild-central ]; then + rm -rf /var/log/wild-central + fi + + # Remove lib directory + if [ -d /var/lib/wild-central ]; then + rm -rf /var/lib/wild-central + fi + + # Remove nginx site + if [ -L /etc/nginx/sites-enabled/wild-central ]; then + rm -f /etc/nginx/sites-enabled/wild-central + echo "Disabled wild-central nginx site" + fi + + # Only remove wildcloud user if wild-cloud package is not installed + if ! dpkg -s wild-cloud >/dev/null 2>&1; then + if id wildcloud >/dev/null 2>&1; then + userdel wildcloud || true + fi + fi + + echo "wild-central purged successfully" + ;; + + remove|upgrade|failed-upgrade|abort-install|abort-upgrade|disappear) + ;; + + *) + echo "postrm called with unknown argument \`$1'" >&2 + exit 1 + ;; +esac + +exit 0 diff --git a/dist/debian/DEBIAN/prerm b/dist/debian/DEBIAN/prerm new file mode 100644 index 0000000..8a01df9 --- /dev/null +++ b/dist/debian/DEBIAN/prerm @@ -0,0 +1,28 @@ +#!/bin/bash + +set -e + +case "$1" in + remove|upgrade|deconfigure) + echo "Stopping wild-central service..." + + # Stop and disable the service + if systemctl is-active --quiet wild-central; then + systemctl stop wild-central + fi + + if systemctl is-enabled --quiet wild-central; then + systemctl disable wild-central + fi + ;; + + failed-upgrade) + ;; + + *) + echo "prerm called with unknown argument \`$1'" >&2 + exit 1 + ;; +esac + +exit 0 diff --git a/dist/debian/etc/nginx/sites-available/wild-central b/dist/debian/etc/nginx/sites-available/wild-central new file mode 100644 index 0000000..a7d1adf --- /dev/null +++ b/dist/debian/etc/nginx/sites-available/wild-central @@ -0,0 +1,34 @@ +server { + listen 80; + server_name _; + + # Wild Central Management Interface + root /var/www/html/wild-central; + index index.html; + + # API proxy to wild-central service + location /api/ { + proxy_pass http://localhost:5055; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + # SSE events proxy (long-lived connections) + location /api/v1/events { + proxy_pass http://localhost:5055; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_http_version 1.1; + proxy_set_header Connection ""; + proxy_buffering off; + proxy_cache off; + proxy_read_timeout 86400s; + } + + # Static files - SPA routing support + location / { + try_files $uri $uri/ /index.html; + } +} diff --git a/dist/debian/etc/systemd/system/wild-central-nftables-reload.service b/dist/debian/etc/systemd/system/wild-central-nftables-reload.service new file mode 100644 index 0000000..303c5cc --- /dev/null +++ b/dist/debian/etc/systemd/system/wild-central-nftables-reload.service @@ -0,0 +1,9 @@ +[Unit] +Description=Apply Wild Central nftables rules +Documentation=https://mywildcloud.org +After=network.target nftables.service + +[Service] +Type=oneshot +ExecStart=/usr/sbin/nft -f /etc/nftables.d/wild-cloud.nft +RemainAfterExit=no diff --git a/dist/debian/etc/systemd/system/wild-central.service b/dist/debian/etc/systemd/system/wild-central.service new file mode 100644 index 0000000..0705d9b --- /dev/null +++ b/dist/debian/etc/systemd/system/wild-central.service @@ -0,0 +1,28 @@ +[Unit] +Description=Wild Central Service +Documentation=https://mywildcloud.org +After=network.target +Wants=network.target + +[Service] +Type=simple +User=wildcloud +Group=wildcloud +ExecStart=/usr/bin/wild-central +Restart=always +RestartSec=5 +Environment=WILD_CENTRAL_DATA_DIR=/var/lib/wild-central +Environment=WILD_CENTRAL_STATIC_DIR=/var/www/html/wild-central +StandardOutput=journal +StandardError=journal +SyslogIdentifier=wild-central + +# Security settings +NoNewPrivileges=yes +PrivateTmp=yes +ProtectSystem=full +ProtectHome=yes +ReadWritePaths=/etc/wild-central /var/lib/wild-central /var/log/wild-central /etc/dnsmasq.d /etc/systemd/resolved.conf.d /etc/haproxy /etc/nftables.d /etc/wireguard + +[Install] +WantedBy=multi-user.target diff --git a/dist/debian/etc/wild-central/config.yaml.example b/dist/debian/etc/wild-central/config.yaml.example new file mode 100644 index 0000000..69e57c8 --- /dev/null +++ b/dist/debian/etc/wild-central/config.yaml.example @@ -0,0 +1,42 @@ +cloud: + router: + ip: 192.168.8.1 + dnsmasq: + ip: 192.168.8.50 + interface: eth0 + # DHCP server (disabled by default — enable to take over DHCP from your router) + # dhcp: + # enabled: false + # rangeStart: "192.168.8.100" + # rangeEnd: "192.168.8.200" + # leaseTime: "24h" + # gateway: "" # defaults to cloud.router.ip + # staticLeases: # managed via POST /api/v1/dnsmasq/dhcp/static + # - mac: "aa:bb:cc:dd:ee:ff" + # ip: "192.168.8.10" + # hostname: "node1" + + # HAProxy ingress proxy — routes external traffic by SNI. + # Use POST /api/v1/haproxy/generate to apply after changing this config. + haproxy: + # Optional custom TCP routes for non-Wild Cloud services on your network. + # customRoutes: + # - name: civil_ssh + # port: 2222 # external listen port + # backend: 192.168.8.10:22 + + # nftables host firewall — tracks ports HAProxy is serving. + # Set wanInterface to enable strict WAN filtering (drops all unrecognised inbound traffic). + # Leave unset for default accept-all policy (safe during initial setup). + # nftables: + # wanInterface: eth0 + + # Dynamic DNS — updates DNS A records when your public IP changes. + # API token goes in secrets.yaml under cloudflare.apiToken. + # ddns: + # enabled: false + # provider: cloudflare + # records: + # - cloud.example.com + # - cloud2.example.com + # intervalMinutes: 5 diff --git a/dist/scripts/build-package.sh b/dist/scripts/build-package.sh new file mode 100755 index 0000000..901b5ee --- /dev/null +++ b/dist/scripts/build-package.sh @@ -0,0 +1,71 @@ +#!/bin/bash +set -e + +# Build configuration +VERSION="${VERSION:-0.1.0}" +ARCH="${ARCH:-amd64}" +BUILD_DIR="build" +SRC_DIR="$BUILD_DIR/src" +BINARY_NAME="wild-central" + +# Source paths (relative to dist directory) +API_SOURCE=".." +WEBAPP_SOURCE="../web" + +echo "Building Wild Central v${VERSION} for ${ARCH}" +echo "================================================" + +# Clean and create build directories +echo "Preparing build directories..." +rm -rf "$SRC_DIR" +mkdir -p "$SRC_DIR" +mkdir -p "$BUILD_DIR" + +# Copy API source +echo "" +echo "Copying API source..." +rsync -a --exclude='dist/' --exclude='web/' "$API_SOURCE/" "$SRC_DIR/api/" +cd "$SRC_DIR/api" + +# Build the daemon +echo "Building daemon binary..." +if [ "$ARCH" = "arm64" ]; then + GOOS=linux GOARCH=arm64 go build -ldflags="-X main.Version=$VERSION" -o "../../$BINARY_NAME" . +elif [ "$ARCH" = "amd64" ]; then + GOOS=linux GOARCH=amd64 go build -ldflags="-X main.Version=$VERSION" -o "../../$BINARY_NAME" . +else + echo "Unsupported architecture: $ARCH" + exit 1 +fi + +cd - > /dev/null +echo "Daemon binary built: $BUILD_DIR/$BINARY_NAME" + +# Copy web app source +echo "" +echo "Copying web app source..." +cp -r "$WEBAPP_SOURCE" "$SRC_DIR/web" +cd "$SRC_DIR/web" + +# Remove any local env overrides +rm -f .env.local .env.*.local + +# Build the web app +echo "Building web application..." +pnpm install --frozen-lockfile + +# For production builds, use empty base URL so the app uses relative paths +echo "VITE_API_BASE_URL=" > .env.production.local + +pnpm run build --mode production || { + echo "Build with type checking failed, trying without type check..." + pnpm exec vite build +} + +cd - > /dev/null +echo "Web app built: $SRC_DIR/web/dist/" + +echo "" +echo "Build complete!" +echo " Binary: $BUILD_DIR/$BINARY_NAME" +echo " Web App: $SRC_DIR/web/dist/" diff --git a/dist/scripts/create-gitea-release.sh b/dist/scripts/create-gitea-release.sh new file mode 100755 index 0000000..9bb4a49 --- /dev/null +++ b/dist/scripts/create-gitea-release.sh @@ -0,0 +1,189 @@ +#!/bin/bash +set -euo pipefail + +GITEA_URL="$1" +OWNER="$2" +REPO="$3" +TAG="$4" +VERSION="$5" +TOKEN="$6" +CHANNEL="${7:-stable}" # "stable" or "edge" + +API_URL="${GITEA_URL}/api/v1" +PACKAGES_DIR="dist/packages" +BINARIES_DIR="dist/bin" +SKIP_RELEASE_CREATION=false + +echo "Release configuration:" +echo " Repository: ${OWNER}/${REPO}" +echo " Tag: ${TAG}" +echo " Version: ${VERSION}" +echo "" + +# Check if release already exists +echo "Checking if release ${TAG} exists..." +RELEASE_ID=$(curl -s -H "Authorization: token ${TOKEN}" \ + "${API_URL}/repos/${OWNER}/${REPO}/releases/tags/${TAG}" \ + | jq -r '.id // empty') + +if [ -n "$RELEASE_ID" ]; then + echo "Release ${TAG} exists (ID: ${RELEASE_ID})" + echo "Updating existing release assets..." + + # Get existing assets + EXISTING_ASSETS=$(curl -s -H "Authorization: token ${TOKEN}" \ + "${API_URL}/repos/${OWNER}/${REPO}/releases/${RELEASE_ID}/assets" \ + | jq -r '.[].id') + + # Delete old assets + for ASSET_ID in $EXISTING_ASSETS; do + ASSET_NAME=$(curl -s -H "Authorization: token ${TOKEN}" \ + "${API_URL}/repos/${OWNER}/${REPO}/releases/${RELEASE_ID}/assets/${ASSET_ID}" \ + | jq -r '.name') + + if [[ "$ASSET_NAME" == *.deb ]] || [[ "$ASSET_NAME" == wild-central-* ]] || [[ "$ASSET_NAME" == "SHA256SUMS" ]]; then + echo " Deleting old asset: ${ASSET_NAME}" + curl -s -X DELETE -H "Authorization: token ${TOKEN}" \ + "${API_URL}/repos/${OWNER}/${REPO}/releases/${RELEASE_ID}/assets/${ASSET_ID}" + fi + done + + echo "Cleaned up old package assets" + SKIP_RELEASE_CREATION=true +fi + +# Extract changelog section for this version +CHANGELOG_SECTION="" +if [ -f "../CHANGELOG.md" ]; then + CHANGELOG_SECTION=$(awk "/^## \[${VERSION}\]/{found=1; next} /^## \[/{if(found) exit} found{print}" ../CHANGELOG.md | sed '/^$/d') +fi + +INSTALL_DOCS="### Installation + +#### Full Installation (.deb package) + +Download the appropriate .deb package for your architecture: + +- **arm64**: \`wild-central_${VERSION}_arm64.deb\` — Raspberry Pi 4/5, ARM servers +- **amd64**: \`wild-central_${VERSION}_amd64.deb\` — x86_64 systems + +\`\`\`bash +wget ${GITEA_URL}/${OWNER}/${REPO}/releases/download/v${VERSION}/wild-central_${VERSION}_amd64.deb +sudo dpkg -i wild-central_${VERSION}_amd64.deb +sudo apt-get install -f +sudo systemctl enable --now wild-central +\`\`\` + +#### Standalone Binary + +- \`wild-central-{arch}\` — Daemon binary only (Docker/K8s) + +#### Verification + +\`\`\`bash +wget ${GITEA_URL}/${OWNER}/${REPO}/releases/download/v${VERSION}/SHA256SUMS +sha256sum -c SHA256SUMS +\`\`\`" + +if [ -n "$CHANGELOG_SECTION" ]; then + BODY="${CHANGELOG_SECTION} + +--- + +${INSTALL_DOCS}" +else + BODY="${INSTALL_DOCS}" +fi + +# Set release name and prerelease flag based on channel +if [ "$CHANNEL" = "edge" ]; then + RELEASE_NAME="Wild Central (edge) — ${VERSION}" + IS_PRERELEASE=true +else + RELEASE_NAME="Wild Central ${VERSION}" + IS_PRERELEASE=false +fi + +# Create new release if it doesn't exist +if [ "$SKIP_RELEASE_CREATION" != "true" ]; then + echo "Creating new release ${TAG}..." + RELEASE_DATA=$(jq -n \ + --arg tag "${TAG}" \ + --arg name "${RELEASE_NAME}" \ + --arg body "${BODY}" \ + --argjson prerelease "${IS_PRERELEASE}" \ + '{"tag_name": $tag, "name": $name, "body": $body, "draft": false, "prerelease": $prerelease}') + + RELEASE_RESPONSE=$(curl -s -X POST -H "Authorization: token ${TOKEN}" \ + -H "Content-Type: application/json" \ + -d "${RELEASE_DATA}" \ + "${API_URL}/repos/${OWNER}/${REPO}/releases") + + RELEASE_ID=$(echo "${RELEASE_RESPONSE}" | jq -r '.id') + + if [ -z "$RELEASE_ID" ] || [ "$RELEASE_ID" = "null" ]; then + echo "Failed to create release" + echo "${RELEASE_RESPONSE}" | jq . + exit 1 + fi + + echo "Created release ${TAG} (ID: ${RELEASE_ID})" +fi + +# Generate checksums +echo "" +echo "Generating checksums..." +cd dist +sha256sum packages/*.deb bin/wild-central-* > SHA256SUMS +echo "Created SHA256SUMS" +cd .. + +# Upload artifacts +echo "" +echo "Uploading release artifacts..." + +upload_file() { + local FILE="$1" + local FILENAME=$(basename "$FILE") + + echo " Uploading ${FILENAME}..." + + UPLOAD_RESPONSE=$(curl -s -X POST \ + -H "Authorization: token ${TOKEN}" \ + -F "attachment=@${FILE}" \ + "${API_URL}/repos/${OWNER}/${REPO}/releases/${RELEASE_ID}/assets?name=${FILENAME}") + + ASSET_ID=$(echo "${UPLOAD_RESPONSE}" | jq -r '.id') + + if [ -z "$ASSET_ID" ] || [ "$ASSET_ID" = "null" ]; then + echo " Failed to upload ${FILENAME}" + echo "${UPLOAD_RESPONSE}" | jq . + return 1 + else + echo " Uploaded ${FILENAME}" + return 0 + fi +} + +# Upload .deb packages +for DEB in ${PACKAGES_DIR}/wild-central_${VERSION}_*.deb; do + if [ ! -f "$DEB" ]; then + echo "No .deb files found for version ${VERSION} in ${PACKAGES_DIR}" + exit 1 + fi + upload_file "$DEB" +done + +# Upload standalone daemon binaries +for BINARY in ${BINARIES_DIR}/wild-central-*; do + if [ -f "$BINARY" ]; then + upload_file "$BINARY" + fi +done + +# Upload checksums +upload_file "dist/SHA256SUMS" + +echo "" +echo "Release complete!" +echo "View at: ${GITEA_URL}/${OWNER}/${REPO}/releases/tag/${TAG}" diff --git a/dist/scripts/deploy-apt-repository.sh b/dist/scripts/deploy-apt-repository.sh new file mode 100755 index 0000000..bbdaace --- /dev/null +++ b/dist/scripts/deploy-apt-repository.sh @@ -0,0 +1,138 @@ +#!/bin/bash +set -euo pipefail + +# Required environment variables: +# APTLY_URL - Aptly server URL (e.g. https://aptly.cloud2.payne.io) +# APTLY_PASS - Aptly API password +# Optional: +# APTLY_USER - API username (default: aptly) +# APTLY_GPG_KEY - GPG key fingerprint for signing; if unset, signing is skipped + +APTLY_URL="${APTLY_URL:?APTLY_URL is required}" +APTLY_PASS="${APTLY_PASS:?APTLY_PASS is required}" +APTLY_USER="${APTLY_USER:-aptly}" +APTLY_GPG_KEY="${APTLY_GPG_KEY:-}" + +VERSION=$(cat ../VERSION 2>/dev/null || echo "0.0.0-dev") +REPO_NAME="wild-central" +DIST="${DIST:-stable}" +COMPONENT="main" +PACKAGE_DIR="dist/packages" +SNAP_NAME="${REPO_NAME}-${VERSION}" +AUTH="${APTLY_USER}:${APTLY_PASS}" + +echo "Deploying Wild Central ${VERSION} to APT repository..." +echo " URL: ${APTLY_URL}" + +# Signing config +if [ -n "$APTLY_GPG_KEY" ]; then + SIGNING="{\"GpgKey\":\"${APTLY_GPG_KEY}\"}" +else + echo "APTLY_GPG_KEY not set — publishing without GPG signature" + SIGNING="{\"Skip\":true}" +fi + +api() { + local METHOD="$1" + local ENDPOINT="$2" + local DATA="${3:-}" + if [ -n "$DATA" ]; then + curl -sf --http1.1 -u "$AUTH" -X "$METHOD" \ + -H 'Content-Type: application/json' \ + -d "$DATA" \ + "${APTLY_URL}/api${ENDPOINT}" + else + curl -sf --http1.1 -u "$AUTH" -X "$METHOD" "${APTLY_URL}/api${ENDPOINT}" + fi +} + +# 1. Ensure local repo exists +echo "" +echo "Ensuring repository '${REPO_NAME}' exists..." +if ! api GET "/repos/${REPO_NAME}" > /dev/null 2>&1; then + api POST "/repos" \ + "{\"Name\":\"${REPO_NAME}\",\"DefaultDistribution\":\"${DIST}\",\"DefaultComponent\":\"${COMPONENT}\"}" \ + > /dev/null + echo " Created '${REPO_NAME}'" +else + echo " '${REPO_NAME}' already exists" +fi + +# 2. Upload .deb files +echo "" +echo "Uploading packages..." +UPLOAD_DIR="${REPO_NAME}-${VERSION}" +FOUND=0 +for DEB in "${PACKAGE_DIR}"/wild-central_"${VERSION}"_*.deb; do + [ -f "$DEB" ] || continue + FOUND=1 + FILENAME=$(basename "$DEB") + echo " ${FILENAME}..." + curl -sf --http1.1 -u "$AUTH" -X POST \ + -F "file=@${DEB}" \ + "${APTLY_URL}/api/files/${UPLOAD_DIR}" > /dev/null +done +if [ "$FOUND" -eq 0 ]; then + echo "No .deb files found for version ${VERSION} in ${PACKAGE_DIR}" + exit 1 +fi +echo " Uploaded" + +# 3. Add packages to repo +echo "" +echo "Adding packages to '${REPO_NAME}'..." +api POST "/repos/${REPO_NAME}/file/${UPLOAD_DIR}" > /dev/null +echo " Added" + +# 4. Clean up uploaded files from aptly's staging area +api DELETE "/files/${UPLOAD_DIR}" > /dev/null 2>&1 || true + +# 5. Create snapshot for this version +echo "" +echo "Snapshotting as '${SNAP_NAME}'..." +if api GET "/snapshots/${SNAP_NAME}" > /dev/null 2>&1; then + echo " Snapshot '${SNAP_NAME}' already exists, using as-is" +else + api POST "/repos/${REPO_NAME}/snapshots" \ + "{\"Name\":\"${SNAP_NAME}\",\"Description\":\"Wild Central ${VERSION}\"}" \ + > /dev/null + echo " Snapshot '${SNAP_NAME}' created" +fi + +# 6. Publish or update +echo "" +PUBLISH_PREFIX=$(api GET "/publish" \ + | jq -r ".[] | select(.Distribution == \"${DIST}\") | .Prefix" 2>/dev/null \ + | head -1) + +UPDATE_BODY="{\"Snapshots\":[{\"Component\":\"${COMPONENT}\",\"Name\":\"${SNAP_NAME}\"}],\"Signing\":${SIGNING},\"ForceOverwrite\":true}" + +if [ -z "$PUBLISH_PREFIX" ]; then + echo "Publishing '${DIST}' for the first time..." + api POST "/publish" \ + "{\"SourceKind\":\"snapshot\",\"Sources\":[{\"Component\":\"${COMPONENT}\",\"Name\":\"${SNAP_NAME}\"}],\"Distribution\":\"${DIST}\",\"Architectures\":[\"amd64\",\"arm64\"],\"Signing\":${SIGNING}}" \ + > /dev/null +else + echo "Updating published '${DIST}'..." + URL_PREFIX=$(echo "$PUBLISH_PREFIX" | sed 's/^\.$/:/') + api PUT "/publish/${URL_PREFIX}/${DIST}" "$UPDATE_BODY" > /dev/null +fi +echo " Published" + +echo "" +echo "Wild Central ${VERSION} is live." +echo "" +echo "To install on a new device:" +echo "" +echo " curl -fsSL ${APTLY_URL}/wild-central.gpg \\" +echo " | sudo tee /usr/share/keyrings/wild-central.gpg > /dev/null" +echo "" +echo " sudo tee /etc/apt/sources.list.d/wild-central.sources <