0a21f22076
- ServiceStoreManager: manifest allowlist (git.pic.ngo/roof/*), volume
denylist, ACCEPT-only iptables rules, ${SERVICE_IP}-only dest_ip
- IP allocator: pool 172.20.0.20-254, skips CONTAINER_OFFSETS VIPs
- Compose overlay: docker-compose.services.yml auto-included via DCF
- Flask blueprint at /api/store: list, install, remove, refresh
- Store.jsx: full install/remove UI with spinners and toast notifications
- 95 new unit tests for ServiceStoreManager (all passing)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
342 lines
13 KiB
Makefile
342 lines
13 KiB
Makefile
# Personal Internet Cell - Makefile
|
|
# Provides easy commands for managing the cell
|
|
|
|
.PHONY: help start stop restart status logs clean setup check-deps init-peers \
|
|
update reinstall uninstall install \
|
|
build build-api build-webui \
|
|
start-core start-dns start-api start-wg start-webui \
|
|
backup restore \
|
|
test test-all test-unit test-coverage test-api test-cli \
|
|
test-phase1 test-phase2 test-phase3 test-phase4 test-all-phases \
|
|
test-integration test-integration-readonly \
|
|
test-e2e-deps test-e2e-api test-e2e-ui test-e2e-wg test-e2e \
|
|
reset-test-admin-pass \
|
|
show-admin-password reset-admin-password \
|
|
show-routes add-peer list-peers
|
|
|
|
# Detect docker compose command (v2 plugin preferred, fallback to v1 standalone)
|
|
DC := $(shell docker compose version >/dev/null 2>&1 && echo "docker compose" || echo "docker-compose")
|
|
|
|
# Full compose command: includes docker-compose.services.yml when it exists
|
|
DCF = $(DC) $(if $(wildcard docker-compose.services.yml),-f docker-compose.yml -f docker-compose.services.yml,-f docker-compose.yml)
|
|
|
|
# Default target
|
|
help:
|
|
@echo "Personal Internet Cell - Management Commands"
|
|
@echo ""
|
|
@echo "First install:"
|
|
@echo " check-deps - Install all required system packages (python3, docker, etc.)"
|
|
@echo " setup - Generate keys, write configs, create data dirs"
|
|
@echo " Env vars: CELL_NAME=mycell CELL_DOMAIN=cell VPN_ADDRESS=10.0.0.1/24 WG_PORT=51820"
|
|
@echo " init-peers - Reset peer list to empty"
|
|
@echo ""
|
|
@echo "Lifecycle:"
|
|
@echo " start - Start all services"
|
|
@echo " stop - Stop all services"
|
|
@echo " restart - Restart all services"
|
|
@echo " status - Show container status + API health"
|
|
@echo " logs - Follow logs from all services"
|
|
@echo " logs-<svc> - Follow logs for one service (e.g. make logs-api)"
|
|
@echo " shell-<svc> - Open shell in a container (e.g. make shell-api)"
|
|
@echo ""
|
|
@echo "Updates & reinstall:"
|
|
@echo " update - git pull + rebuild + restart (deploy latest code)"
|
|
@echo " reinstall - Full wipe and fresh install from current git checkout"
|
|
@echo " uninstall - Stop + remove containers; prompts whether to also delete data"
|
|
@echo ""
|
|
@echo "Build:"
|
|
@echo " build - Rebuild API image"
|
|
@echo " build-api - Rebuild API image (no cache)"
|
|
@echo " build-webui - Rebuild Web UI image (no cache)"
|
|
@echo ""
|
|
@echo "Individual services:"
|
|
@echo " start-dns - Start DNS only"
|
|
@echo " start-api - Start API only"
|
|
@echo " start-wg - Start WireGuard only"
|
|
@echo ""
|
|
@echo "Maintenance:"
|
|
@echo " backup - Backup config + data to backups/"
|
|
@echo " restore - List available backups"
|
|
@echo " clean - Remove containers and volumes (keeps config/data dirs)"
|
|
@echo " show-admin-password - Print the admin password (reads setup file or prompts to reset)"
|
|
@echo " reset-admin-password - Generate a new random admin password and print it"
|
|
@echo ""
|
|
@echo "Tests:"
|
|
@echo " test - Run all tests"
|
|
@echo " test-coverage - Run tests with HTML coverage report"
|
|
@echo " test-integration - Full integration tests (needs running stack)"
|
|
@echo " test-integration-readonly - Read-only integration tests (safe to run anytime)"
|
|
@echo ""
|
|
@echo "Peers:"
|
|
@echo " list-peers - List configured WireGuard peers"
|
|
@echo " show-routes - Show WireGuard routing table"
|
|
|
|
# ── Dependencies & setup ──────────────────────────────────────────────────────
|
|
|
|
check-deps:
|
|
@sudo sh scripts/check_deps.sh
|
|
|
|
setup: check-deps
|
|
@echo "Setting up Personal Internet Cell..."
|
|
@sudo chown -R $$(id -u):$$(id -g) config/ data/ 2>/dev/null || true
|
|
CELL_NAME=$(or $(CELL_NAME),mycell) \
|
|
CELL_DOMAIN=$(or $(CELL_DOMAIN),cell) \
|
|
VPN_ADDRESS=$(or $(VPN_ADDRESS),10.0.0.1/24) \
|
|
WG_PORT=$(or $(WG_PORT),51820) \
|
|
WG_PRIVATE_KEY="$(WG_PRIVATE_KEY)" \
|
|
WG_PUBLIC_KEY="$(WG_PUBLIC_KEY)" \
|
|
python3 scripts/setup_cell.py
|
|
|
|
init-peers:
|
|
@echo "Initializing peer configuration..."
|
|
@echo '[]' > data/api/peers.json
|
|
@echo "Peer configuration initialized."
|
|
|
|
# ── Lifecycle ─────────────────────────────────────────────────────────────────
|
|
|
|
start:
|
|
@echo "Starting Personal Internet Cell..."
|
|
PUID=$$(id -u) PGID=$$(id -g) $(DCF) --profile full up -d --build
|
|
@echo "Services started. Check status with 'make status'"
|
|
|
|
stop:
|
|
@echo "Stopping Personal Internet Cell..."
|
|
PUID=$$(id -u) PGID=$$(id -g) $(DCF) --profile full down
|
|
@echo "Services stopped."
|
|
|
|
restart:
|
|
@echo "Restarting Personal Internet Cell..."
|
|
PUID=$$(id -u) PGID=$$(id -g) $(DC) restart
|
|
@echo "Services restarted."
|
|
|
|
status:
|
|
@echo "Personal Internet Cell Status:"
|
|
@echo "================================"
|
|
$(DCF) ps
|
|
@echo ""
|
|
@echo "API Status:"
|
|
@curl -s http://localhost:3000/health || echo "API not responding"
|
|
|
|
logs:
|
|
$(DCF) logs -f
|
|
|
|
logs-%:
|
|
$(DCF) logs -f $*
|
|
|
|
shell-%:
|
|
docker exec -it cell-$* /bin/bash 2>/dev/null || docker exec -it cell-$* /bin/sh
|
|
|
|
# ── Updates & reinstall ───────────────────────────────────────────────────────
|
|
|
|
update:
|
|
@echo "Pulling latest code..."
|
|
@git stash --include-untracked --quiet 2>/dev/null || true
|
|
git pull
|
|
@git stash pop --quiet 2>/dev/null || true
|
|
@if [ ! -f config/mail/mailserver.env ]; then \
|
|
echo "Config missing — running setup first..."; \
|
|
$(MAKE) setup; \
|
|
fi
|
|
@echo "Rebuilding and restarting services..."
|
|
PUID=$$(id -u) PGID=$$(id -g) $(DCF) --profile full up -d --build
|
|
@echo "Update complete. Run 'make status' to verify."
|
|
|
|
reinstall:
|
|
@echo "Reinstalling Personal Internet Cell from scratch..."
|
|
PUID=$$(id -u) PGID=$$(id -g) $(DCF) --profile full down -v 2>/dev/null || true
|
|
@sudo rm -rf config/ data/
|
|
@$(MAKE) setup
|
|
@$(MAKE) start
|
|
@echo "Reinstall complete."
|
|
|
|
install:
|
|
@if [ -f /opt/pic/.installed ] && [ "$(FORCE)" != "1" ]; then \
|
|
echo "Already installed. Run 'make update' to update, or 'make install FORCE=1' to reinstall."; \
|
|
exit 0; \
|
|
fi
|
|
@echo "Running setup..."
|
|
@$(MAKE) setup
|
|
@echo "Installing systemd unit..."
|
|
@sudo cp scripts/pic.service /etc/systemd/system/pic.service
|
|
@-sudo systemctl daemon-reload && sudo systemctl enable pic
|
|
@sudo mkdir -p /opt/pic
|
|
@sudo touch /opt/pic/.installed
|
|
@echo "Installation complete. Run 'make start-core' to start core services."
|
|
|
|
uninstall:
|
|
@echo ""
|
|
@echo "This will stop and remove all containers."
|
|
@echo ""
|
|
@printf "Also delete config/ and data/? This cannot be undone. [y/N/cancel]: "; \
|
|
read ans; \
|
|
case "$$ans" in \
|
|
y|Y) \
|
|
echo "Stopping containers and removing images..."; \
|
|
PUID=$$(id -u) PGID=$$(id -g) $(DCF) --profile full down -v --rmi all 2>/dev/null || true; \
|
|
echo "Deleting config/ and data/..."; \
|
|
sudo rm -rf config/ data/; \
|
|
echo "Uninstall complete. Git repo and scripts remain."; \
|
|
;; \
|
|
n|N|"") \
|
|
echo "Stopping and removing containers (keeping images and data)..."; \
|
|
PUID=$$(id -u) PGID=$$(id -g) $(DCF) --profile full down 2>/dev/null || true; \
|
|
echo "Done. Images, config/ and data/ are untouched. Run 'make start' to bring it back up."; \
|
|
;; \
|
|
*) \
|
|
echo "Cancelled."; \
|
|
;; \
|
|
esac
|
|
@-sudo systemctl disable pic 2>/dev/null || true
|
|
@-sudo rm -f /etc/systemd/system/pic.service
|
|
@-sudo rm -f /opt/pic/.installed
|
|
@echo "Note: Data volumes were not deleted. To remove all data, manually delete config/ and data/."
|
|
|
|
# ── Build ─────────────────────────────────────────────────────────────────────
|
|
|
|
build:
|
|
@echo "Building API service..."
|
|
$(DC) build api
|
|
|
|
build-api:
|
|
@echo "Rebuilding API (no cache)..."
|
|
$(DC) build --no-cache api
|
|
$(DC) up -d api
|
|
|
|
build-webui:
|
|
@echo "Rebuilding Web UI (no cache)..."
|
|
$(DC) build --no-cache webui
|
|
$(DC) up -d webui
|
|
|
|
# ── Individual services ───────────────────────────────────────────────────────
|
|
|
|
start-core:
|
|
@echo "Starting core services (caddy, dns, wireguard, api, webui)..."
|
|
PUID=$$(id -u) PGID=$$(id -g) $(DCF) --profile core up -d --build
|
|
@echo "Core services started. Run 'make start' to also bring up optional services."
|
|
|
|
start-dns:
|
|
$(DC) --profile core up -d dns
|
|
|
|
start-api:
|
|
$(DC) --profile core up -d api
|
|
|
|
start-wg:
|
|
$(DC) --profile core up -d wireguard
|
|
|
|
start-webui:
|
|
$(DC) --profile core up -d webui
|
|
|
|
# ── Maintenance ───────────────────────────────────────────────────────────────
|
|
|
|
clean:
|
|
@echo "Removing containers and volumes..."
|
|
$(DC) down -v
|
|
docker system prune -f
|
|
@echo "Done. config/ and data/ are untouched."
|
|
|
|
backup:
|
|
@echo "Creating backup..."
|
|
@mkdir -p backups
|
|
@sudo tar -czf backups/cell-backup-$(shell date +%Y%m%d-%H%M%S).tar.gz \
|
|
config/ data/ docker-compose.yml Makefile README.md
|
|
@sudo chown $$(id -u):$$(id -g) backups/cell-backup-*.tar.gz
|
|
@echo "Backup created in backups/."
|
|
|
|
restore:
|
|
@echo "Available backups:"
|
|
@ls -lh backups/cell-backup-*.tar.gz 2>/dev/null || echo "No backups found."
|
|
@echo ""
|
|
@echo "To restore: tar -xzf backups/cell-backup-YYYYMMDD-HHMMSS.tar.gz"
|
|
|
|
# ── Tests ─────────────────────────────────────────────────────────────────────
|
|
|
|
test:
|
|
@echo "Running unit tests..."
|
|
python3 -m pytest tests/ --ignore=tests/e2e --ignore=tests/integration -q
|
|
|
|
test-all: test test-integration test-e2e-api test-e2e-ui
|
|
@echo "All test suites complete."
|
|
|
|
test-unit:
|
|
python3 -m pytest tests/ --ignore=tests/e2e --ignore=tests/integration -q
|
|
|
|
test-coverage:
|
|
python3 -m pytest tests/ --ignore=tests/e2e --ignore=tests/integration \
|
|
--cov=api --cov-report=html --cov-report=term-missing --cov-fail-under=70 -v
|
|
|
|
test-integration:
|
|
@echo "Running full integration tests (requires running PIC stack)..."
|
|
PIC_HOST=$${PIC_HOST:-localhost} python3 -m pytest tests/integration/ -v
|
|
|
|
test-webui:
|
|
@echo "Running webui unit tests (requires node; builds a disposable container)..."
|
|
docker run --rm -v "$(PWD)/webui:/app" -w /app node:18-alpine \
|
|
sh -c "npm install --silent && npm test"
|
|
|
|
test-integration-readonly:
|
|
@echo "Running read-only integration tests (no peer creation)..."
|
|
PIC_HOST=$${PIC_HOST:-localhost} python3 -m pytest tests/integration/test_live_api.py tests/integration/test_webui.py -v
|
|
|
|
test-api:
|
|
python3 -m pytest tests/test_api_endpoints.py -v
|
|
|
|
test-cli:
|
|
python3 -m pytest tests/test_cli_tool.py -v
|
|
|
|
# ── E2E tests ─────────────────────────────────────────────────────────────────
|
|
# Run `make test-e2e-deps` once to install dependencies, then use the other targets.
|
|
# Admin password is read from data/api/.test_admin_pass (written by reset-test-admin-pass).
|
|
# Override: make test-e2e-api PIC_ADMIN_PASS=mypassword
|
|
|
|
test-e2e-deps:
|
|
sudo pip3 install --break-system-packages -r tests/e2e/requirements.txt
|
|
sudo python3 -m playwright install --with-deps chromium
|
|
|
|
test-e2e-api:
|
|
@PIC_HOST=$${PIC_HOST:-localhost} python3 -m pytest tests/e2e/api -v
|
|
|
|
test-e2e-ui:
|
|
@PIC_HOST=$${PIC_HOST:-localhost} python3 -m pytest tests/e2e/ui -v
|
|
|
|
test-e2e-wg:
|
|
@PIC_HOST=$${PIC_HOST:-localhost} sudo -E python3 -m pytest tests/e2e/wg -v -p no:xdist
|
|
|
|
test-e2e: test-e2e-api test-e2e-ui test-e2e-wg
|
|
|
|
reset-test-admin-pass:
|
|
ifndef PIC_TEST_ADMIN_PASS
|
|
$(error Usage: make reset-test-admin-pass PIC_TEST_ADMIN_PASS=newpassword)
|
|
endif
|
|
python3 scripts/reset_admin_password.py "$(PIC_TEST_ADMIN_PASS)"
|
|
|
|
# ── Admin password management ──────────────────────────────────────────────────
|
|
|
|
show-admin-password:
|
|
@sudo python3 scripts/reset_admin_password.py --show
|
|
|
|
reset-admin-password:
|
|
@docker exec cell-api python3 /app/scripts/reset_admin_password.py --generate
|
|
|
|
|
|
# ── Network / peers ───────────────────────────────────────────────────────────
|
|
|
|
show-routes:
|
|
@docker exec cell-wireguard wg show 2>/dev/null || echo "WireGuard not running"
|
|
|
|
list-peers:
|
|
@curl -s http://localhost:3000/api/peers | python3 -m json.tool || echo "API not responding"
|
|
|
|
add-peer:
|
|
@if [ -n "$(PEER_NAME)" ] && [ -n "$(PEER_IP)" ] && [ -n "$(PEER_KEY)" ]; then \
|
|
curl -X POST http://localhost:3000/api/peers \
|
|
-H "Content-Type: application/json" \
|
|
-d '{"name":"$(PEER_NAME)","ip":"$(PEER_IP)","public_key":"$(PEER_KEY)"}'; \
|
|
else \
|
|
echo "Usage: make add-peer PEER_NAME=name PEER_IP=10.0.0.x PEER_KEY=<pubkey>"; \
|
|
fi
|
|
|
|
# ── Dev ───────────────────────────────────────────────────────────────────────
|
|
|
|
dev:
|
|
$(DC) -f docker-compose.yml -f docker-compose.dev.yml up -d
|