{ 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 (based on your ABRA_RECIPES.md and more) allRecipes = [ # Tier 1 - Production Ready (Score 5) "gitea" "mealie" "nextcloud" # Tier 2 - Stable (Score 4) "gotosocial" "wordpress" # Tier 3 - Community (Score 3) "collabora" "croc" "custom-php" "dokuwiki" "engelsystem" "fab-manager" "ghost" "karrot" "lauti" "loomio" "mattermost" "mattermost-lts" "mrbs" "onlyoffice" "open-inventory" "outline" "owncast" "rallly" # Additional recipes from Co-op Cloud catalog "hedgedoc" "mediawiki" "seafile" "jitsi-meet" "matrix-synapse" "rocketchat" "prestashop" "invoiceninja" "kimai" "pretix" "drone" "n8n" "gitlab" "jupyter-lab" "plausible" "matomo" "uptime-kuma" "grafana" "peertube" "funkwhale" "mastodon" "pixelfed" "jellyfin" ]; in isoConfig // { system.stateVersion = "25.05"; networking = { wireless.enable = false; # Disable to avoid conflicts networkmanager = { enable = true; dns = "none"; # Critical: Don't let NetworkManager manage DNS }; hostName = if isLiveIso then "workshop-live" else "workshop-vm"; }; # Configure dnsmasq properly for wildcard DNS services.dnsmasq = { enable = true; settings = { # Wildcard: *.workshop.local -> 127.0.0.1 address = "/.workshop.local/127.0.0.1"; # Use upstream DNS for everything else server = [ "8.8.8.8" "1.1.1.1" ]; # Listen on all interfaces (important for VM/container access) listen-address = [ "127.0.0.1" "0.0.0.0" ]; # Bind to interfaces bind-interfaces = true; # Don't read /etc/hosts for our custom domains no-hosts = false; # Cache settings cache-size = 1000; # Local domain handling local = "/workshop.local/"; domain-needed = true; bogus-priv = true; }; }; # Force system to use our dnsmasq networking.nameservers = lib.mkForce [ "127.0.0.1" ]; # Disable systemd-resolved to avoid conflicts services.resolved.enable = false; # Enable Docker for local development virtualisation.docker.enable = true; services.getty.autologinUser = "workshop"; users.users.workshop = { isNormalUser = true; shell = pkgs.bash; extraGroups = [ "networkmanager" "wheel" "docker" ]; password = ""; }; security.sudo.wheelNeedsPassword = false; environment.systemPackages = with pkgs; [ openssh curl git networkmanager firefox xterm docker docker-compose bash wget jq tree nano dnsutils dig # For DNS debugging ]; # Auto-install abra and setup Docker Swarm systemd.services.workshop-abra-setup = { wantedBy = [ "multi-user.target" ]; after = [ "network-online.target" "docker.service" "dnsmasq.service" ]; wants = [ "network-online.target" ]; script = '' export HOME=/home/workshop # Wait for network and services with better testing echo "Waiting for services to start..." for i in {1..30}; do # Test external connectivity if ${pkgs.curl}/bin/curl -s --max-time 3 google.com >/dev/null 2>&1; then echo "โœ… External network ready" break fi sleep 2 done # Test DNS resolution specifically for i in {1..20}; do if ${pkgs.dnsutils}/bin/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 ${pkgs.docker}/bin/docker info >/dev/null 2>&1; then echo "โœ… Docker ready" break fi sleep 2 done # Install abra for workshop user if [ ! -f /home/workshop/.local/bin/abra ]; then sudo -u workshop mkdir -p /home/workshop/.local/bin cd /home/workshop sudo -u workshop ${pkgs.curl}/bin/curl -fsSL https://install.abra.coopcloud.tech | sudo -u workshop ${pkgs.bash}/bin/bash fi # Initialize Docker Swarm if ! ${pkgs.docker}/bin/docker info | grep -q "Swarm: active"; then ${pkgs.docker}/bin/docker swarm init --advertise-addr 127.0.0.1 2>/dev/null || true fi # Ensure workshop user is in docker group usermod -aG docker workshop # Test final DNS resolution if ${pkgs.dnsutils}/bin/nslookup test.workshop.local 127.0.0.1; then echo "๐ŸŽ‰ All services ready!" else echo "โš ๏ธ DNS may need manual restart: sudo systemctl restart dnsmasq" fi ''; serviceConfig = { Type = "oneshot"; RemainAfterExit = true; User = "root"; }; }; # Enhanced bash configuration with complete recipe support programs.bash = { interactiveShellInit = '' # Workshop welcome and command definitions echo "๐Ÿš€ CODE CRISPIES Workshop Environment" echo "Mode: Local Development + Cloud Access" echo "" # Test DNS immediately on login if command -v nslookup &> /dev/null; 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" echo "๐Ÿ”ง Debug: nslookup test.workshop.local 127.0.0.1" fi fi # Ensure abra is in PATH export PATH="$HOME/.local/bin:$PATH" # Complete recipe list for bash completion ALL_RECIPES="${builtins.concatStringsSep " " allRecipes}" # Enable tab completion for deploy and browser commands _workshop_completion() { local cur prev opts COMPREPLY=() cur="''${COMP_WORDS[COMP_CWORD]}" prev="''${COMP_WORDS[COMP_CWORD-1]}" case "''${prev}" in deploy|browser) opts="$ALL_RECIPES" COMPREPLY=( $(compgen -W "''${opts}" -- ''${cur}) ) return 0 ;; connect) opts="${builtins.concatStringsSep " " cloudServerNames}" COMPREPLY=( $(compgen -W "''${opts}" -- ''${cur}) ) return 0 ;; esac } complete -F _workshop_completion deploy browser connect setup-traefik() { echo "๐Ÿ”ง Setting up local Traefik proxy..." # Test DNS first if ! nslookup traefik.workshop.local 127.0.0.1 >/dev/null 2>&1; then echo "โŒ DNS not resolving *.workshop.local" echo "๐Ÿ”„ Restarting dnsmasq..." sudo systemctl restart dnsmasq sleep 3 if ! nslookup traefik.workshop.local 127.0.0.1 >/dev/null 2>&1; then echo "โŒ DNS still not working!" echo "๐Ÿ” Debug info:" echo " systemctl status dnsmasq" echo " nslookup traefik.workshop.local 127.0.0.1" return 1 fi fi echo "โœ… DNS resolution working" # Rest of your existing setup-traefik function... if ! command -v abra &> /dev/null; then echo "โŒ Abra not found. Installing..." sudo systemctl restart workshop-abra-setup sleep 5 export PATH="$HOME/.local/bin:$PATH" fi # Ensure Docker Swarm is ready if ! docker info 2>/dev/null | grep -q "Swarm: active"; then echo "๐Ÿ”ฅ Initializing Docker Swarm..." docker swarm init --advertise-addr 127.0.0.1 || true fi # Create abra context if not exists if ! abra server ls 2>/dev/null | grep -q "workshop-local"; then echo "๐Ÿ— Creating local abra context..." abra server add workshop-local docker://localhost --local fi echo "๐Ÿš€ Deploying Traefik..." abra app new traefik -S --domain=traefik.workshop.local --server=workshop-local abra app deploy traefik.workshop.local # Wait for Traefik to be ready echo "โณ Waiting for Traefik to start..." for i in {1..30}; do if curl -s http://traefik.workshop.local >/dev/null 2>&1; then echo "โœ… Traefik deployed! Dashboard: http://traefik.workshop.local" echo "๐Ÿš€ Now you can deploy apps with 'deploy '" return 0 fi sleep 2 done echo "โš ๏ธ Traefik deployed but may still be starting..." echo "๐Ÿ” Debug: docker service ls | curl -I http://traefik.workshop.local" } deploy() { if [ -z "$1" ]; then echo "Usage: deploy " echo "Example: deploy wordpress" echo "Available recipes: $ALL_RECIPES" echo "" echo "๐Ÿ” Use tab completion or run 'recipes' for categorized list" return 1 fi local recipe="$1" local domain="$recipe.workshop.local" echo "๐Ÿš€ Deploying $recipe locally..." echo "Domain: $domain" if ! command -v abra &> /dev/null; then echo "โŒ Abra not found. Run 'sudo systemctl restart workshop-abra-setup'" return 1 fi # Check if Traefik is running if ! curl -s http://traefik.workshop.local >/dev/null 2>&1; then echo "โš ๏ธ Traefik not detected. Running setup first..." setup-traefik fi echo "๐Ÿ“ฆ Creating app: $recipe" abra app new "$recipe" -S --domain="$domain" --server=workshop-local echo "๐Ÿš€ Deploying app: $domain" abra app deploy "$domain" echo "โณ Waiting for deployment..." for i in {1..60}; do if curl -s http://$domain >/dev/null 2>&1; then echo "โœ… Deployed! Access at: http://$domain" echo "๐ŸŒ Quick launch: browser $recipe" return 0 fi sleep 3 done echo "โš ๏ธ Deployment completed but app may still be starting..." echo "๐Ÿ” Debug: docker service ls | dig @127.0.0.1 $domain +short" echo "๐ŸŒ Try: browser $recipe (in a few moments)" } connect() { [ -z "$1" ] && { echo "Usage: connect "; echo "Available: ${builtins.concatStringsSep " " cloudServerNames}"; return 1; } echo "๐Ÿ”Œ Connecting to $1.codecrispi.es..." ssh -o StrictHostKeyChecking=no workshop@$1.codecrispi.es } browser() { local target_url="about:blank" if [ -n "$1" ]; then # Specific app requested 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 (Score 5):" echo " gitea mealie nextcloud" echo "" echo "๐Ÿ”ง Tier 2 - Stable (Score 4):" echo " gotosocial wordpress" echo "" echo "๐Ÿงช Tier 3 - Community (Score 3):" echo " collabora croc custom-php dokuwiki engelsystem" echo " fab-manager ghost karrot lauti loomio mattermost" echo " mattermost-lts mrbs onlyoffice open-inventory outline" echo " owncast rallly" echo "" echo "๐ŸŒ Extended Catalog:" echo " Content: hedgedoc mediawiki seafile" echo " Chat: jitsi-meet matrix-synapse rocketchat" echo " Business: prestashop invoiceninja kimai pretix" echo " Dev Tools: drone n8n gitlab jupyter-lab" echo " Analytics: plausible matomo uptime-kuma grafana" echo " Media: peertube funkwhale mastodon pixelfed jellyfin" echo "" echo "๐Ÿš€ Usage:" echo " deploy - Deploy locally" echo " browser - Open app in browser" echo " ๐Ÿ“– Full catalog: https://recipes.coopcloud.tech" echo "" echo "๐Ÿ’ก Use tab completion: type 'deploy ' or 'browser '" } desktop() { echo "๐Ÿ–ฅ๏ธ Starting GUI session..." if command -v startx &> /dev/null; then if [ -z "$DISPLAY" ]; then startx & export DISPLAY=:0 sleep 3 echo "โœ… GUI started. Check QEMU window or run 'browser'" else echo "โ„น๏ธ GUI already running" fi else echo "๐Ÿ’ก GUI available in QEMU window (Alt+Tab to switch)" echo "๐Ÿ–ฑ๏ธ Click on QEMU graphics window to use desktop" fi } help() { echo "๐Ÿš€ CODE CRISPIES Workshop Commands:" echo "" echo "๐Ÿ  Local Development:" echo " setup-traefik - Setup local Traefik proxy (REQUIRED FIRST!)" echo " recipes - Show all available app recipes" echo " deploy - Deploy app locally (e.g., deploy wordpress)" echo " browser [recipe] - Launch Firefox [to specific app]" echo " desktop - Start GUI desktop session" echo "" echo "โ˜๏ธ Cloud Access:" echo " connect - SSH to cloud server (e.g., connect hopper)" echo "" echo "Available servers: ${builtins.concatStringsSep " " cloudServerNames}" echo "" echo "๐Ÿ“š Learning Flow:" echo " 1. First time: setup-traefik" echo " 2. Try local: recipes โ†’ deploy wordpress โ†’ browser wordpress" echo " 3. Try cloud: connect hopper โ†’ same abra commands" echo "" echo "๐Ÿ” Debug Commands:" echo " docker service ls - Check running services" echo " dig @127.0.0.1 app.workshop.local - Test DNS resolution" echo " systemctl status dnsmasq - Check DNS service" echo "" echo "๐Ÿ’ก Tab completion available for deploy, browser, connect commands" } ''; }; services.xserver = { enable = true; desktopManager.xfce.enable = true; displayManager.lightdm.enable = true; }; # Don't auto-start GUI, let user choose systemd.user.services.workshop-welcome = { wantedBy = [ "default.target" ]; script = '' echo "Welcome! Run 'desktop' to start GUI session" ''; serviceConfig.Type = "oneshot"; }; }