Files
malta-workshop/common.nix

768 lines
21 KiB
Nix
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

{ pkgs, lib ? pkgs.lib, cloudServerNames, isLiveIso ? false, ... }:
let
# Only include isoImage config when building ISO
isoConfig = lib.optionalAttrs isLiveIso {
isoImage = {
makeEfiBootable = true;
makeUsbBootable = true;
};
};
# Complete Co-op Cloud recipe list
allRecipes = [
# Tier 1 - Production Ready (Score 5)
"gitea"
"mealie"
"nextcloud"
# Tier 2 - Stable (Score 4)
"gotosocial"
"wordpress"
# Tier 3 - Community (Score 3)
"adapt_authoring"
"agora"
"alerta"
"amusewiki"
"authentik"
"babybuddy"
"backup-bot"
"backup-bot-two"
"base-row"
"baserow"
"bonfire"
"botamusique"
"caddy"
"cal"
"calibre-web"
"capsul"
"civicrm-backdrop"
"civicrm-wordpress"
"collabora"
"compy"
"container"
"croc"
"cryptpad"
"custom-html"
"custom-html-tiny"
"custom-php"
"dashy"
"discourse"
"distribution"
"docker-hub-rss"
"dokuwiki"
"drone"
"drone-docker-runner"
"drutopia"
"element-web"
"engelsystem"
"etherpad"
"fab-manager"
"farmos"
"federatedwiki"
"filerun"
"filestash"
"firefly-iii"
"firefly-iii-importer"
"fluffychat"
"focalboard"
"foodsoft"
"forgejo-runner"
"funkwhale"
"gancio"
"garage"
"ghost"
"gitlab"
"go-neb"
"go-ssb-room"
"grafana"
"grist"
"h5ai"
"hedgedoc"
"hometown"
"hugo"
"icecast"
"immich"
"indentificator"
"invidious"
"invoiceninja"
"jellyfin"
"jellyseerr"
"jitsi"
"jupyter-lab"
"kanboard"
"karrot"
"keycloak"
"keycloak-collective-portal"
"keyoxide"
"kimai"
"kutt"
"laplace"
"lasuite-docs"
"lauti"
"lemmy"
"levelfly"
"liberaforms"
"limesurvey"
"listmonk"
"loomio"
"mailman3"
"mailu"
"mastodon"
"matomo"
"mattermost"
"mattermost-lts"
"maubot"
"mediawiki"
"minecraft"
"minetest"
"miniflux"
"minio"
"mobilizon"
"monica"
"monitoring"
"monitoring-lite"
"monitoring-ng"
"mrbs"
"mumble"
"mycorrhiza"
"n8n"
"navidrome"
"netdata"
"nitter"
"nocodb"
"notea"
"ntfy"
"oasis"
"ohmyform"
"onlyoffice"
"open-dispatch"
"open-inventory"
"osticket"
"outline"
"owncast"
"parasol-static-site"
"peertube"
"pelican"
"penpot"
"photoprism"
"phpservermon"
"pixelfed"
"plausible"
"portainer"
"postfix-relay"
"pretix"
"privatebin"
"projectsend"
"prowlarr"
"qbit"
"radarr"
"radicale"
"rallly"
"rauthy"
"redmine"
"renovate"
"restic-rest-server"
"rocketchat"
"rsshub"
"rstudio"
"rustdesk-server"
"screensy"
"seafile"
"selfoss"
"sextant"
"shlink"
"singlelink"
"snikket"
"snowflake"
"sonarr"
"statping"
"statuspal"
"strapi"
"stream-share"
"swarm-cronjob"
"swarmpit"
"synapse-admin"
"traefik"
"traefik-cert-dumper"
"traefik-forward-auth"
"uptime-kuma"
"vaultwarden"
"vikunja"
"voila"
"vroom"
"wallabag"
"weblate"
"wekan"
"woodpecker"
"wordpress-bedrock"
"workadventure"
"writefreely"
"xwiki"
"zammad"
"znc"
"zulip"
];
in
isoConfig // {
system.stateVersion = "25.05";
# SSH Configuration
services.openssh = {
enable = true;
settings = {
PermitRootLogin = "no";
PasswordAuthentication = true;
PubkeyAuthentication = true;
};
ports = [ 22 ];
};
# Network Configuration
networking = {
wireless.enable = false;
networkmanager = {
enable = true;
dns = "none"; # We use dnsmasq
};
hostName = if isLiveIso then "workshop-live" else "workshop-vm";
hosts."127.0.0.1" = [ "workshop.local" "localhost" ];
nameservers = lib.mkForce [ "127.0.0.1" ];
firewall.enable = false; # Workshop environment
};
# DNS Configuration - Wildcard *.workshop.local -> 127.0.0.1
services.dnsmasq = {
enable = true;
settings = {
address = "/.workshop.local/127.0.0.1";
server = [ "8.8.8.8" "1.1.1.1" ];
listen-address = [ "127.0.0.1" ];
bind-interfaces = true;
cache-size = 1000;
local = "/workshop.local/";
domain-needed = true;
bogus-priv = true;
};
};
# Disable systemd-resolved (conflicts with dnsmasq)
services.resolved.enable = false;
# Container Runtime
virtualisation.docker.enable = true;
# User Configuration
users = {
users.root.password = "root";
users.workshop = {
isNormalUser = true;
shell = pkgs.bash;
extraGroups = [ "networkmanager" "wheel" "docker" ];
password = "workshop";
};
};
# SSH key generation for workshop user
systemd.services.workshop-ssh-keygen = {
description = "Generate SSH key for workshop user for passwordless localhost access";
wantedBy = [ "multi-user.target" ];
after = [ "network.target" ];
path = with pkgs; [ openssh coreutils gnugrep ];
script = ''
USER_HOME=/home/workshop
SSH_DIR=$USER_HOME/.ssh
KEY_FILE=$SSH_DIR/id_ed25519
AUTH_KEYS_FILE=$SSH_DIR/authorized_keys
mkdir -p $SSH_DIR
chown workshop:workshop $SSH_DIR
chmod 700 $SSH_DIR
if [ ! -f "$KEY_FILE" ]; then
echo "Generating SSH key for workshop user..."
ssh-keygen -t ed25519 -f $KEY_FILE -N "" -C "workshop@workshop-vm"
chown workshop:workshop $KEY_FILE $KEY_FILE.pub
chmod 600 $KEY_FILE
chmod 644 $KEY_FILE.pub
fi
PUB_KEY=$(cat $KEY_FILE.pub)
if ! grep -qF -- "$PUB_KEY" "$AUTH_KEYS_FILE" 2>/dev/null; then
echo "Adding public key to authorized_keys..."
echo "$PUB_KEY" >> $AUTH_KEYS_FILE
fi
chown workshop:workshop $AUTH_KEYS_FILE
chmod 600 $AUTH_KEYS_FILE
'';
serviceConfig = {
Type = "oneshot";
User = "root";
RemainAfterExit = true;
};
};
services.getty.autologinUser = "workshop";
security.sudo.wheelNeedsPassword = false;
# System Packages
environment.systemPackages = with pkgs; [
openssh
curl
git
networkmanager
docker
docker-compose
bash
wget
jq
tree
nano
dnsutils
dig
gnutar
];
# System Setup Service (Root Tasks)
systemd.services.workshop-system-setup = {
description = "System-level checks for network, DNS, and Docker";
wantedBy = [ "multi-user.target" ];
after = [ "network-online.target" "docker.service" "dnsmasq.service" ];
wants = [ "network-online.target" ];
path = with pkgs; [ bash curl dnsutils docker gnugrep shadow coreutils ];
script = ''
# Wait for network and services
echo "Waiting for services to start..."
for i in {1..30}; do
if curl -s --max-time 3 google.com >/dev/null 2>&1; then
echo " External network ready"
break
fi
sleep 2
done
# Test DNS resolution
for i in {1..20}; do
if nslookup test.workshop.local 127.0.0.1 >/dev/null 2>&1; then
echo " Wildcard DNS ready"
break
fi
echo "🔄 Waiting for DNS... (attempt $i)"
sleep 2
done
# Test Docker
for i in {1..10}; do
if docker info >/dev/null 2>&1; then
echo " Docker ready"
break
fi
sleep 2
done
# Initialize Docker Swarm
echo "🔄 Checking Docker Swarm status..."
if ! docker info | grep -q "Swarm: active"; then
echo "🔥 Initializing Docker Swarm..."
docker swarm init --advertise-addr 127.0.0.1 2>/dev/null || true
if docker info | grep -q "Swarm: active"; then
echo " Docker Swarm initialized."
else
echo " Docker Swarm initialization failed."
fi
else
echo " Docker Swarm already active."
fi
# Ensure workshop user is in docker group
echo "🔄 Ensuring workshop user is in docker group..."
usermod -aG docker workshop
if id -nG workshop | grep -q "docker"; then
echo " workshop user is in docker group."
else
echo " Failed to add workshop user to docker group."
fi
# Final DNS resolution test
if nslookup test.workshop.local 127.0.0.1; then
echo "🎉 System services ready!"
else
echo " DNS may need manual restart: systemctl restart dnsmasq"
fi
'';
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
User = "root";
Environment = [
"TERM=xterm-256color"
"HOME=/root"
];
};
};
# Abra Installation Service (System-wide)
systemd.services.workshop-abra-install = {
description = "Install abra CLI system-wide";
wantedBy = [ "multi-user.target" ];
after = [ "workshop-system-setup.service" ];
wants = [ "workshop-system-setup.service" ];
path = with pkgs; [ bash wget curl coreutils gnutar ncurses gzip file gnugrep docker ];
script = ''
# Set proper environment
export TERM=xterm-256color
export HOME=/root
# Check if abra is already installed
if [ -x "/root/.local/bin/abra" ]; then
echo " abra already installed"
/root/.local/bin/abra --version
exit 0
fi
echo "🚀 Installing abra system-wide..."
# Install to /usr/local/bin (default behavior)
curl -fsSL https://install.abra.coopcloud.tech | bash
# Add to bashrc only once
if ! grep -q "/root/.local/bin" /root/.bashrc 2>/dev/null; then
echo 'export PATH="$PATH:/root/.local/bin"' >> /root/.bashrc
echo " Added /root/.local/bin to PATH in /root/.bashrc"
fi
# Verify
if [ -x "/root/.local/bin/abra" ]; then
echo " abra installed to /root/.local/bin/abra"
fi
'';
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
User = "root";
Environment = [
"TERM=xterm-256color"
"HOME=/root"
];
};
};
# System Packages
environment.systemPackages = with pkgs; [
openssh
curl
git
networkmanager
docker
docker-compose
bash
wget
jq
tree
nano
dnsutils
dig
gnutar
openssl # Add this for certificate generation
];
# Enhanced Bash Configuration with All Features
programs.bash.interactiveShellInit =
let
recipeList = builtins.concatStringsSep " " allRecipes;
serverList = builtins.concatStringsSep " " cloudServerNames;
in
''
# Workshop Environment Welcome
echo "🚀 CODE CRISPIES Workshop Environment"
echo "Mode: Local Development (Offline Co-op Cloud)"
echo ""
# DNS Health Check
if command -v nslookup >/dev/null 2>&1; then
if nslookup test.workshop.local 127.0.0.1 >/dev/null 2>&1; then
echo " DNS wildcard ready: *.workshop.local 127.0.0.1"
else
echo " DNS not working! Run: sudo systemctl restart dnsmasq"
fi
fi
# Ensure /root/.local/bin is in PATH (safety net)
if [[ ":$PATH:" != *":/root/.local/bin:"* ]]; then
echo " adding abra to PATH"
export PATH="$PATH:/root/.local/bin"
fi
# Check abra installation
if sudo abra >/dev/null 2>&1; then
echo " abra ready: $(sudo which abra)"
source <(sudo abra autocomplete bash) 2>/dev/null || true
echo " abra autocomplete enabled"
else
echo " abra not found! Check: systemctl status workshop-abra-install"
fi
# Bash Completion Configuration
_workshop_completion() {
local cur prev
COMPREPLY=()
cur="''${COMP_WORDS[COMP_CWORD]}"
prev="''${COMP_WORDS[COMP_CWORD-1]}"
case "$prev" in
deploy|browser)
COMPREPLY=($(compgen -W "${recipeList}" -- "$cur"))
return 0
;;
connect)
COMPREPLY=($(compgen -W "${serverList}" -- "$cur"))
return 0
;;
esac
}
complete -F _workshop_completion deploy browser connect abra
# Core Workshop Functions
setup() {
echo "🔧 Setting up LOCAL Co-op Cloud environment..."
# Verify DNS first
if ! nslookup traefik.workshop.local 127.0.0.1 >/dev/null 2>&1; then
echo "🔄 Restarting DNS..."
sudo systemctl restart dnsmasq
sleep 3
fi
# Ensure Docker Swarm + proxy network
if ! docker info 2>/dev/null | grep -q "Swarm: active"; then
echo "🔥 Initializing Docker Swarm..."
docker swarm init --advertise-addr 127.0.0.1
fi
if ! docker network ls | grep -q "proxy"; then
echo "🌐 Creating proxy network..."
docker network create -d overlay proxy
fi
# Add LOCAL server (critical difference!)
if ! sudo abra server ls 2>/dev/null | grep -q "default"; then
echo "🗄 Adding LOCAL server..."
sudo abra server add --local
echo " Local server registered"
fi
# Create self-signed certificate for offline use
echo "🔐 Setting up self-signed certificates for offline use..."
# Create temporary cert directory
CERT_DIR="/tmp/workshop-certs"
mkdir -p $CERT_DIR
# Generate self-signed certificate for *.workshop.local
if [[ ! -f "$CERT_DIR/workshop.crt" ]]; then
openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
-keyout "$CERT_DIR/workshop.key" \
-out "$CERT_DIR/workshop.crt" \
-subj "/CN=*.workshop.local" \
-config <(printf "[req]\ndistinguished_name=req\n[v3_req]\nsubjectAltName=DNS:*.workshop.local,DNS:workshop.local,DNS:localhost\n") \
-extensions v3_req 2>/dev/null || true
echo "🔑 Generated self-signed certificate"
fi
# Create and configure Traefik for OFFLINE mode
if ! sudo abra app ls 2>/dev/null | grep -q "traefik"; then
echo "🚀 Creating Traefik app for OFFLINE use..."
sudo abra app new traefik --domain=traefik.workshop.local --server=default
# Configure traefik for offline/local development
TRAEFIK_ENV="/root/.abra/servers/default/traefik.workshop.local.env"
echo " Configuring Traefik for offline mode..."
# Create offline-friendly traefik configuration
sudo tee -a "$TRAEFIK_ENV" >/dev/null <<EOF
# OFFLINE/LOCAL DEVELOPMENT CONFIGURATION
LETS_ENCRYPT_ENV=staging
WILDCARDS_ENABLED=1
SECRET_WILDCARD_CERT_VERSION=v1
SECRET_WILDCARD_KEY_VERSION=v1
COMPOSE_FILE="\$COMPOSE_FILE:compose.wildcard.yml"
# Disable Let's Encrypt for local development
TRAEFIK_ACME_CASERVER=
TRAEFIK_ACME_EMAIL=
EOF
# Insert self-signed certificates as Docker secrets
if [[ -f "$CERT_DIR/workshop.crt" && -f "$CERT_DIR/workshop.key" ]]; then
echo "📋 Installing self-signed certificates..."
sudo abra app secret insert traefik.workshop.local ssl_cert v1 -f < "$CERT_DIR/workshop.crt"
sudo abra app secret insert traefik.workshop.local ssl_key v1 -f < "$CERT_DIR/workshop.key"
fi
echo "🚀 Deploying Traefik..."
sudo abra app deploy traefik.workshop.local
echo " Waiting for Traefik..."
for i in {1..30}; do
if curl -s -k https://traefik.workshop.local/ping >/dev/null 2>&1 || \
curl -s http://traefik.workshop.local/ping >/dev/null 2>&1; then
echo " Traefik ready! Dashboard: https://traefik.workshop.local (accept self-signed cert)"
echo "💡 For HTTP: http://traefik.workshop.local"
return 0
fi
sleep 2
done
echo " Traefik may still be starting. Check: sudo abra app logs traefik.workshop.local"
else
echo " Traefik already exists"
fi
# Cleanup temporary certs
rm -rf "$CERT_DIR" 2>/dev/null || true
}
deploy() {
if [[ -z "$1" ]]; then
echo "Usage: deploy <recipe>"
echo "Available: ${recipeList}"
return 1
fi
local recipe="$1"
local domain="$recipe.workshop.local"
echo "🚀 Deploying $recipe locally..."
echo "Domain: $domain"
# Ensure Traefik is running
if ! curl -s -k --max-time 3 https://traefik.workshop.local/ping >/dev/null 2>&1 && \
! curl -s --max-time 3 http://traefik.workshop.local/ping >/dev/null 2>&1; then
echo " Traefik not responding. Setting up..."
setup || return 1
fi
# Create and deploy app
echo "📦 Creating app: $recipe"
sudo abra app new "$recipe" --domain="$domain" --server=default
echo "🚀 Deploying: $domain"
sudo abra app deploy "$domain"
echo " Waiting for deployment..."
for i in {1..60}; do
if curl -s -k --max-time 3 https://$domain >/dev/null 2>&1 || \
curl -s --max-time 3 http://$domain >/dev/null 2>&1; then
echo " Deployed! Access at: https://$domain (accept self-signed cert)"
echo "💡 Or HTTP: http://$domain"
return 0
fi
sleep 3
done
echo " Deployment may still be starting..."
echo "🔍 Debug: sudo abra app ps $domain"
}
connect() {
if [[ -z "$1" ]]; then
echo "Usage: connect <name>"
echo "Available: ${serverList}"
return 1
fi
echo "🔌 Connecting to $1.codecrispi.es..."
ssh -o StrictHostKeyChecking=no workshop@$1.codecrispi.es
}
browser() {
local target_url="about:blank"
if [[ -n "$1" ]]; then
target_url="http://$1.workshop.local"
echo "🌐 Opening $1 at $target_url"
else
echo "🌐 Opening Firefox browser"
fi
if [[ -n "$DISPLAY" ]]; then
firefox "$target_url" &
else
echo " No GUI session. Run 'desktop' first"
echo "🌐 Target was: $target_url"
fi
}
recipes() {
echo "📚 Complete Co-op Cloud Recipe Catalog:"
echo ""
echo " Tier 1 - Production Ready: gitea mealie nextcloud"
echo "🔧 Tier 2 - Stable: gotosocial wordpress"
echo "🧪 Tier 3 - Community: collabora croc dokuwiki ghost loomio..."
echo "🌐 Extended: matrix-synapse rocketchat gitlab n8n mastodon..."
echo ""
echo "🚀 Usage:"
echo " deploy <recipe> - Deploy locally"
echo " browser <recipe> - Open in browser"
echo " 📖 Full catalog: https://recipes.coopcloud.tech"
echo ""
echo "💡 Tab completion: deploy <TAB> or browser <TAB>"
}
desktop() {
echo "🖥 Starting GUI session..."
if command -v startx >/dev/null 2>&1; then
if [[ -z "$DISPLAY" ]]; then
startx &
export DISPLAY=:0
sleep 3
echo " GUI started"
else
echo " GUI already running"
fi
else
echo "💡 GUI available in QEMU window"
fi
}
help() {
echo "🚀 CODE CRISPIES Workshop Commands:"
echo ""
echo "🏠 Local Development:"
echo " setup - Setup local proxy (REQUIRED FIRST!)"
echo " recipes - Show all available apps"
echo " deploy <recipe> - Deploy app locally"
echo " browser [recipe] - Launch Firefox [to app]"
echo " desktop - Start GUI session"
echo " sudo abra - Run abra CLI directly as root"
echo ""
echo " Cloud Access:"
echo " connect <name> - SSH to cloud server"
echo " Available: ${serverList}"
echo ""
echo "🔍 Debug:"
echo " docker service ls - List running services"
echo " systemctl status dnsmasq - Check DNS"
echo " systemctl status workshop-abra-install - Check abra installation"
echo ""
echo "📚 Learning Flow:"
echo " 1. setup"
echo " 2. deploy wordpress"
echo " 3. browser wordpress"
echo " 4. connect hopper"
}
'';
programs.firefox = {
enable = true;
preferences = {
"browser.fixup.fallback-to-https" = false;
"browser.urlbar.autoFill" = false;
};
};
# GUI Configuration
services.xserver = {
enable = true;
desktopManager.xfce.enable = true;
displayManager.lightdm.enable = true;
};
}