Adds dist.

This commit is contained in:
2026-07-12 21:40:15 +00:00
parent 518cdbbce5
commit 1d1d6c605c
18 changed files with 1219 additions and 8 deletions

30
dist/.gitignore vendored Normal file
View File

@@ -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

164
dist/Makefile vendored Normal file
View File

@@ -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

76
dist/README.md vendored Normal file
View File

@@ -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 <<EOF
Types: deb
URIs: https://aptly.cloud2.payne.io
Suites: stable
Components: main
Signed-By: /usr/share/keyrings/wild-central.gpg
EOF
sudo apt update && sudo apt install wild-central
```
Use `Suites: edge` for rolling builds from HEAD.
### Manual .deb install
```bash
sudo dpkg -i wild-central_*.deb
sudo apt-get install -f
```
## Quick Start
1. **Start the service**:
```bash
sudo systemctl enable --now wild-central
```
2. **Access the web interface**: open `http://your-server-ip` in your browser
3. **Configure** (optional):
```bash
sudo cp /etc/wild-central/config.yaml.example /etc/wild-central/config.yaml
sudo nano /etc/wild-central/config.yaml
sudo systemctl restart wild-central
```
## Release Artifacts
Each release includes:
- `wild-central_{VERSION}_{arch}.deb` — full install (daemon + web UI)
- `wild-central-{arch}` — standalone daemon binary
- `SHA256SUMS` — checksums for all artifacts
## Developer Tooling
```bash
make help # Show all targets and current version info
make package-all # Build all .deb packages
make release # Stable release: tag + Gitea + APT stable
make release-edge # Edge release: force-tag + Gitea + APT edge
make deploy-repo # Deploy packages to APT repository only
```
Directory structure:
```
build/ Intermediate build artifacts
dist/bin/ Standalone binaries
dist/packages/ .deb packages
```
Environment variables (see `.envrc`):
- `GITEA_TOKEN` — required for `make release` / `make release-edge`
- `APTLY_URL` / `APTLY_PASS` — required for APT deployment

12
dist/debian/DEBIAN/control vendored Normal file
View File

@@ -0,0 +1,12 @@
Package: wild-central
Version: VERSION_PLACEHOLDER
Section: net
Priority: optional
Architecture: ARCH_PLACEHOLDER
Homepage: https://mywildcloud.org
Pre-Depends: dnsmasq, haproxy, nftables, wireguard-tools, certbot, python3-certbot-dns-cloudflare
Depends: adduser, nginx
Maintainer: Wild Cloud Team <paul@payne.io>
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.

262
dist/debian/DEBIAN/postinst vendored Normal file
View File

@@ -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

80
dist/debian/DEBIAN/postrm vendored Normal file
View File

@@ -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

28
dist/debian/DEBIAN/prerm vendored Normal file
View File

@@ -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

View File

@@ -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;
}
}

View File

@@ -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

View File

@@ -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

View File

@@ -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

71
dist/scripts/build-package.sh vendored Executable file
View File

@@ -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/"

189
dist/scripts/create-gitea-release.sh vendored Executable file
View File

@@ -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}"

138
dist/scripts/deploy-apt-repository.sh vendored Executable file
View File

@@ -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 <<EOF"
echo " Types: deb"
echo " URIs: ${APTLY_URL}"
echo " Suites: ${DIST}"
echo " Components: ${COMPONENT}"
echo " Signed-By: /usr/share/keyrings/wild-central.gpg"
echo " EOF"
echo ""
echo " sudo apt update && sudo apt install wild-central"

49
dist/scripts/package-deb.sh vendored Executable file
View File

@@ -0,0 +1,49 @@
#!/bin/bash
set -euo pipefail
ARCH="$1"
BINARY_PATH="$2"
VERSION="$3"
BUILD_DIR="build"
DIST_DIR="dist"
DEB_DIR="debian-package"
WEB_DIST="${BUILD_DIR}/src/web/dist"
echo "Creating .deb package for ${ARCH}..."
# Create directories
mkdir -p "${DIST_DIR}/bin" "${DIST_DIR}/packages"
# Copy debian package structure
cp -r debian/ "${BUILD_DIR}/${DEB_DIR}-${ARCH}/"
# Copy binary to correct location
mkdir -p "${BUILD_DIR}/${DEB_DIR}-${ARCH}/usr/bin"
cp "${BINARY_PATH}" "${BUILD_DIR}/${DEB_DIR}-${ARCH}/usr/bin/wild-central"
# Copy static web files from built web app
mkdir -p "${BUILD_DIR}/${DEB_DIR}-${ARCH}/var/www/html/wild-central"
if [ -d "${WEB_DIST}" ]; then
cp -r "${WEB_DIST}"/* "${BUILD_DIR}/${DEB_DIR}-${ARCH}/var/www/html/wild-central/"
echo "Copied web app files"
else
echo "Warning: Web app dist not found at ${WEB_DIST}"
fi
# Set script permissions
chmod 755 "${BUILD_DIR}/${DEB_DIR}-${ARCH}/DEBIAN/postinst"
chmod 755 "${BUILD_DIR}/${DEB_DIR}-${ARCH}/DEBIAN/prerm"
chmod 755 "${BUILD_DIR}/${DEB_DIR}-${ARCH}/DEBIAN/postrm"
# Substitute placeholders in control file
sed -i "s/VERSION_PLACEHOLDER/${VERSION}/g" "${BUILD_DIR}/${DEB_DIR}-${ARCH}/DEBIAN/control"
sed -i "s/ARCH_PLACEHOLDER/${ARCH}/g" "${BUILD_DIR}/${DEB_DIR}-${ARCH}/DEBIAN/control"
# Build package and copy to dist directories
dpkg-deb --root-owner-group --build "${BUILD_DIR}/${DEB_DIR}-${ARCH}" "${BUILD_DIR}/wild-central_${VERSION}_${ARCH}.deb"
cp "${BINARY_PATH}" "${DIST_DIR}/bin/wild-central-${ARCH}"
cp "${BUILD_DIR}/wild-central_${VERSION}_${ARCH}.deb" "${DIST_DIR}/packages/"
echo "Package created: ${DIST_DIR}/packages/wild-central_${VERSION}_${ARCH}.deb"
echo "Daemon binary copied: ${DIST_DIR}/bin/wild-central-${ARCH}"