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

1
VERSION Normal file
View File

@@ -0,0 +1 @@
0.1.0

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

View File

@@ -1,6 +1,6 @@
# Building Wild App
# Building Wild Central Web App
This document describes the architecture and tooling used to build the Wild App, the web-based interface for managing Wild Cloud instances, hosted on Wild Central.
This document describes the architecture and tooling used to build the Wild Central web app, the browser-based interface for managing Wild Central and its connected services.
## Principles

View File

@@ -1,13 +1,13 @@
# Wild Cloud Web App
# Wild Central Web App
The Wild Cloud Web App is a web-based interface for managing Wild Cloud instances. It allows users to view and control their Wild Cloud environments, including deploying applications, monitoring resources, and configuring settings.
The Wild Central Web App is a web-based interface for managing Wild Central and its connected services. It allows users to configure networking (DNS, gateway, VPN, firewall, TLS), manage Wild Cloud instances, deploy applications, and monitor resources.
## Prerequisites
Before starting the web app, ensure the Wild Central API is running:
```bash
cd ../api
cd ..
make dev
```
@@ -57,11 +57,9 @@ pnpm run check # Run lint, type-check, and tests
### `VITE_API_BASE_URL`
The base URL of the Wild Central API server.
The base URL of the Wild Central API.
- **Default:** `http://localhost:5055`
- **Example:** `http://192.168.1.100:5055`
- **Usage:** Set in `.env` file (see `.env.example` for template)
This variable is used by the API client to connect to the Wild Central API. If not set, it defaults to `http://localhost:5055`.