diff --git a/.env.example b/.env.example index 2d8d58a..39e1cbd 100644 --- a/.env.example +++ b/.env.example @@ -25,3 +25,10 @@ ICECAST_MOUNT=/radio # OP25 container (usually no need to change) OP25_API_URL=http://localhost:8001 OP25_TERMINAL_URL=http://localhost:8081 + +# Container registry — set these to pull pre-built images instead of building locally. +# Must match the DOCKER_ORG variable and repo name configured in Gitea. +# Leave blank to always build locally. +IMAGE_REGISTRY=git.vpn.cusano.net +DOCKER_ORG= +DOCKER_REPO= diff --git a/.gitea/workflows/build-edge-node.yml b/.gitea/workflows/build-edge-node.yml new file mode 100644 index 0000000..970c450 --- /dev/null +++ b/.gitea/workflows/build-edge-node.yml @@ -0,0 +1,51 @@ +name: Build edge-node + +on: + push: + branches: [main, master] + paths: + - "drb-edge-node/**" + +jobs: + build: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + env: + CONTAINER_NAME: edge-node + steps: + - uses: actions/checkout@v4 + + - uses: docker/setup-qemu-action@v3 + + - uses: docker/setup-buildx-action@v3 + with: + config-inline: | + [registry."git.vpn.cusano.net"] + http = false + insecure = false + + - uses: docker/login-action@v3 + with: + registry: git.vpn.cusano.net + username: ${{ gitea.actor }} + password: ${{ secrets.GITHUB_COM_TOKEN }} + + - name: Get version + id: meta + run: | + echo "REPO_NAME=$(echo ${GITHUB_REPOSITORY} | awk -F'/' '{print $2}')" >> $GITHUB_OUTPUT + echo "VERSION=$(git describe --tags --always | sed 's/^v//')" >> $GITHUB_OUTPUT + + - uses: docker/build-push-action@v6 + with: + context: ./drb-edge-node + file: ./drb-edge-node/Dockerfile + platforms: linux/arm64 + push: true + tags: | + git.vpn.cusano.net/${{ vars.DOCKER_ORG }}/${{ steps.meta.outputs.REPO_NAME }}/${{ env.CONTAINER_NAME }}:${{ steps.meta.outputs.VERSION }} + git.vpn.cusano.net/${{ vars.DOCKER_ORG }}/${{ steps.meta.outputs.REPO_NAME }}/${{ env.CONTAINER_NAME }}:latest + cache-from: type=registry,ref=git.vpn.cusano.net/${{ vars.DOCKER_ORG }}/${{ steps.meta.outputs.REPO_NAME }}/${{ env.CONTAINER_NAME }}:buildcache + cache-to: type=registry,ref=git.vpn.cusano.net/${{ vars.DOCKER_ORG }}/${{ steps.meta.outputs.REPO_NAME }}/${{ env.CONTAINER_NAME }}:buildcache,mode=max diff --git a/.gitea/workflows/build-icecast.yml b/.gitea/workflows/build-icecast.yml new file mode 100644 index 0000000..f3fa556 --- /dev/null +++ b/.gitea/workflows/build-icecast.yml @@ -0,0 +1,51 @@ +name: Build icecast + +on: + push: + branches: [main, master] + paths: + - "icecast/**" + +jobs: + build: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + env: + CONTAINER_NAME: icecast + steps: + - uses: actions/checkout@v4 + + - uses: docker/setup-qemu-action@v3 + + - uses: docker/setup-buildx-action@v3 + with: + config-inline: | + [registry."git.vpn.cusano.net"] + http = false + insecure = false + + - uses: docker/login-action@v3 + with: + registry: git.vpn.cusano.net + username: ${{ gitea.actor }} + password: ${{ secrets.GITHUB_COM_TOKEN }} + + - name: Get version + id: meta + run: | + echo "REPO_NAME=$(echo ${GITHUB_REPOSITORY} | awk -F'/' '{print $2}')" >> $GITHUB_OUTPUT + echo "VERSION=$(git describe --tags --always | sed 's/^v//')" >> $GITHUB_OUTPUT + + - uses: docker/build-push-action@v6 + with: + context: ./icecast + file: ./icecast/Dockerfile + platforms: linux/arm64 + push: true + tags: | + git.vpn.cusano.net/${{ vars.DOCKER_ORG }}/${{ steps.meta.outputs.REPO_NAME }}/${{ env.CONTAINER_NAME }}:${{ steps.meta.outputs.VERSION }} + git.vpn.cusano.net/${{ vars.DOCKER_ORG }}/${{ steps.meta.outputs.REPO_NAME }}/${{ env.CONTAINER_NAME }}:latest + cache-from: type=registry,ref=git.vpn.cusano.net/${{ vars.DOCKER_ORG }}/${{ steps.meta.outputs.REPO_NAME }}/${{ env.CONTAINER_NAME }}:buildcache + cache-to: type=registry,ref=git.vpn.cusano.net/${{ vars.DOCKER_ORG }}/${{ steps.meta.outputs.REPO_NAME }}/${{ env.CONTAINER_NAME }}:buildcache,mode=max diff --git a/.gitea/workflows/build-op25.yml b/.gitea/workflows/build-op25.yml new file mode 100644 index 0000000..c26e8fd --- /dev/null +++ b/.gitea/workflows/build-op25.yml @@ -0,0 +1,57 @@ +name: Build op25 + +on: + push: + branches: [main, master] + paths: + - "op25-container/**" + +jobs: + build: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + env: + CONTAINER_NAME: op25-client + steps: + - uses: actions/checkout@v4 + + - uses: docker/setup-qemu-action@v3 + + - uses: docker/setup-buildx-action@v3 + with: + config-inline: | + [registry."git.vpn.cusano.net"] + http = false + insecure = false + + - uses: docker/login-action@v3 + with: + registry: git.vpn.cusano.net + username: ${{ gitea.actor }} + password: ${{ secrets.GITHUB_COM_TOKEN }} + + - name: Validate build configuration + uses: docker/build-push-action@v6 + with: + call: check + + - name: Get version + id: meta + run: | + echo "REPO_NAME=$(echo ${GITHUB_REPOSITORY} | awk -F'/' '{print $2}')" >> $GITHUB_OUTPUT + echo "VERSION=$(git describe --tags --always | sed 's/^v//')" >> $GITHUB_OUTPUT + + - uses: docker/build-push-action@v6 + with: + context: ./op25-container + file: ./op25-container/Dockerfile + platforms: linux/arm64 + push: true + tags: | + git.vpn.cusano.net/${{ vars.DOCKER_ORG }}/${{ steps.meta.outputs.REPO_NAME }}/${{ env.CONTAINER_NAME }}:${{ steps.meta.outputs.VERSION }} + git.vpn.cusano.net/${{ vars.DOCKER_ORG }}/${{ steps.meta.outputs.REPO_NAME }}/${{ env.CONTAINER_NAME }}:stable + # Registry cache — avoids full recompile of GnuRadio/OP25 when only app code changes + cache-from: type=registry,ref=git.vpn.cusano.net/${{ vars.DOCKER_ORG }}/${{ steps.meta.outputs.REPO_NAME }}/${{ env.CONTAINER_NAME }}:buildcache + cache-to: type=registry,ref=git.vpn.cusano.net/${{ vars.DOCKER_ORG }}/${{ steps.meta.outputs.REPO_NAME }}/${{ env.CONTAINER_NAME }}:buildcache,mode=max diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml new file mode 100644 index 0000000..a831c20 --- /dev/null +++ b/.gitea/workflows/ci.yml @@ -0,0 +1,38 @@ +name: CI + +on: + push: + pull_request: + +jobs: + test: + runs-on: ubuntu-latest + defaults: + run: + working-directory: drb-edge-node + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Install dependencies + run: pip install -r requirements.txt pytest + + - name: Run tests + run: pytest tests -v + + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Lint + run: | + pip install flake8 + flake8 drb-edge-node/app --max-line-length=120 diff --git a/Makefile b/Makefile index 07469de..52e74e9 100644 --- a/Makefile +++ b/Makefile @@ -1,16 +1,26 @@ -.PHONY: setup test up down logs +.PHONY: setup test up up-prebuilt pull down logs setup: - @[ -f .env ] && echo ".env already exists, skipping." || (cp .env.example .env && echo "Created .env — fill in your values before running 'make up'.") + @bash setup.sh # Run pytest inside the running edge-node container. # Requires: docker compose up (or at least the edge-node image built). test: docker compose run --no-deps --rm edge-node pytest -v +# Build all images locally and start. up: docker compose up -d +# Pull pre-built images from the registry and start (no local build). +# Requires IMAGE_REGISTRY, DOCKER_ORG, DOCKER_REPO set in .env. +up-prebuilt: + docker compose pull + docker compose up --no-build -d + +pull: + docker compose pull + down: docker compose down diff --git a/docker-compose.yml b/docker-compose.yml index de54923..e32fdc4 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,5 +1,6 @@ services: icecast: + image: ${IMAGE_REGISTRY:-git.vpn.cusano.net}/${DOCKER_ORG:-}/${DOCKER_REPO:-}/icecast:latest build: ./icecast restart: unless-stopped network_mode: host @@ -8,6 +9,7 @@ services: ICECAST_ADMIN_PASSWORD: ${ICECAST_ADMIN_PASSWORD:-admin} op25: + image: ${IMAGE_REGISTRY:-git.vpn.cusano.net}/${DOCKER_ORG:-}/${DOCKER_REPO:-}/op25-client:stable build: ./op25-container restart: unless-stopped privileged: true @@ -24,6 +26,7 @@ services: - icecast edge-node: + image: ${IMAGE_REGISTRY:-git.vpn.cusano.net}/${DOCKER_ORG:-}/${DOCKER_REPO:-}/edge-node:latest build: ./drb-edge-node restart: unless-stopped network_mode: host diff --git a/drb-edge-node/Dockerfile b/drb-edge-node/Dockerfile index dc1754e..4977ce2 100644 --- a/drb-edge-node/Dockerfile +++ b/drb-edge-node/Dockerfile @@ -1,4 +1,4 @@ -FROM python:3.11-slim +FROM python:3.14-slim RUN apt-get update && apt-get install -y \ ffmpeg \ diff --git a/setup.sh b/setup.sh new file mode 100644 index 0000000..26e4a2f --- /dev/null +++ b/setup.sh @@ -0,0 +1,101 @@ +#!/usr/bin/env bash +# Interactive first-time setup for a DRB edge node. +# Writes .env and optionally builds + starts the stack. +set -e + +GREEN='\033[0;32m'; YELLOW='\033[1;33m'; CYAN='\033[0;36m'; NC='\033[0m' +cd "$(dirname "$0")" + +echo -e "${CYAN}DRB Edge Node Setup${NC}" +echo "-------------------" + +if [ -f .env ]; then + echo -e "${YELLOW}Warning: .env already exists.${NC}" + read -rp "Overwrite? [y/N] " yn + [[ "$yn" =~ ^[Yy]$ ]] || { echo "Aborted."; exit 0; } +fi + +# --- Node identity --- +echo "" +echo "Unique node ID — no spaces (e.g. node-ossining, node-002)" +read -rp "NODE_ID: " NODE_ID +while [[ ! "$NODE_ID" =~ ^[a-zA-Z0-9_-]+$ ]]; do + echo " Use letters, numbers, dashes, underscores only." + read -rp "NODE_ID: " NODE_ID +done + +echo "" +read -rp "Node display name [$NODE_ID]: " NODE_NAME +NODE_NAME="${NODE_NAME:-$NODE_ID}" + +# --- GPS --- +echo "" +echo "GPS coordinates (decimal degrees — used for the map)" +read -rp "Latitude [0.0]: " NODE_LAT; NODE_LAT="${NODE_LAT:-0.0}" +read -rp "Longitude [0.0]: " NODE_LON; NODE_LON="${NODE_LON:-0.0}" + +# --- C2 server --- +echo "" +echo "C2 server — hostname or IP of the machine running the server stack" +read -rp "C2 server host: " C2_HOST; C2_HOST="${C2_HOST:-localhost}" +read -rp "C2 API port [8888]: " C2_PORT; C2_PORT="${C2_PORT:-8888}" + +# --- MQTT --- +echo "" +echo "MQTT credentials (must match MQTT_NODE_USER/PASS in the server .env)" +read -rp "MQTT port [1883]: " MQTT_PORT; MQTT_PORT="${MQTT_PORT:-1883}" +read -rp "MQTT username [drb-node]: " MQTT_USER; MQTT_USER="${MQTT_USER:-drb-node}" +read -rsp "MQTT password: " MQTT_PASS; echo ""; MQTT_PASS="${MQTT_PASS:-change-me-node}" + +# --- Icecast --- +echo "" +echo "Icecast passwords (local container)" +read -rsp "Source password [hackme]: " ICECAST_SOURCE; echo ""; ICECAST_SOURCE="${ICECAST_SOURCE:-hackme}" +read -rsp "Admin password [admin]: " ICECAST_ADMIN; echo ""; ICECAST_ADMIN="${ICECAST_ADMIN:-admin}" + +# --- Write .env --- +cat > .env <