From c49eb614d52abf2644401a8b3d268fe626f599e3 Mon Sep 17 00:00:00 2001 From: Michael Czechowski Date: Thu, 14 Aug 2025 18:45:18 +0200 Subject: [PATCH] stable vm run and container spin up, not reachable via ssh --- AGENTS.md | 51 +++++++-- Makefile | 22 ++-- README.md | 162 +++++++++++++++++++------- flake.nix | 335 ++++++++++++++++++++++++++++++++---------------------- 4 files changed, 379 insertions(+), 191 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index ddba039..7e7be4b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -5,20 +5,51 @@ This file provides guidelines for AI coding agents operating within this reposit ## Build, Lint, and Test Commands - **Build**: `make build-usb` (Builds the NixOS workshop ISO) -- **Lint**: No specific linting command found. Follow general code style guidelines. -- **Test**: No specific testing command found. Use `make status-cloud` for health checks. -- **Single Test**: No specific command for running a single test. +- **Local VM**: `make local-vm-run` (Starts local development environment with 15 containers) +- **Lint**: `make lint` (Runs markdownlint, JSON validation, and nixpkgs-fmt) +- **Test**: `make status-cloud` (Health checks for cloud infrastructure) +- **Deploy**: `make deploy-cloud` (Deploys 15 VMs to Hetzner Cloud) ## Code Style Guidelines - **Imports**: Organize imports alphabetically. Avoid unused imports. -- **Formatting**: Adhere to Nixpkgs formatting conventions. Use `nixpkgs-fmt` if available. +- **Formatting**: Adhere to Nixpkgs formatting conventions. Use `nixpkgs-fmt` for consistency. - **Types**: Use Nix's type system rigorously. Define types explicitly where possible. - **Naming Conventions**: - - Variables and functions: `camelCase` or `snake_case` (be consistent). - - Package names: `lowercase-with-hyphens`. + - Variables and functions: `camelCase` for Nix expressions + - Container/server names: `lowercase` (hopper, curie, lovelace, etc.) + - Script names: `kebab-case` for executables +- **SSH Keys**: Always use Ed25519 keys (`~/.ssh/id_ed25519.pub`) +- **Domain**: Use `codecrispi.es` consistently across all environments +- **Password Policy**: Minimize password usage; prefer key-based authentication - **Error Handling**: Handle errors explicitly. Use Nix's error reporting mechanisms. -- **General**: - - Keep code concise and readable. - - Prefer declarative over imperative approaches. - - Document complex logic. + +## Container Architecture + +- **Local VM**: Creates 15 containers (192.168.100.11-25) matching production count +- **Container Names**: hopper, curie, lovelace, noether, hamilton, franklin, johnson, clarke, goldberg, liskov, wing, rosen, shaw, karp, rich +- **Networking**: Private networking with NAT for local development +- **DNS**: Local `.local` domain resolution for testing + +## Available Scripts + +- `connect ` - SSH into specific container +- `containers` - List all containers with IPs +- `logs` - Show container setup logs +- `recipes` - Display available Co-op Cloud recipes +- `help` - Show command help + +## Development Workflow + +1. Use `make local-vm-run` for local development +2. Test with all 15 containers to match production +3. Use `make build-usb` for workshop USB drives +4. Deploy to cloud with `make deploy-cloud` + +## General Guidelines + +- Keep code concise and readable +- Prefer declarative over imperative approaches +- Document complex logic with comments +- Test locally before cloud deployment +- Maintain feature parity between USB/VM environments where possible diff --git a/Makefile b/Makefile index 7f15a0d..afe6d54 100644 --- a/Makefile +++ b/Makefile @@ -4,6 +4,7 @@ export .PHONY: help deploy-cloud build-usb flash-usb local-vm-run clean status destroy-cloud opencode lint DOMAIN := $(or $(WORKSHOP_DOMAIN),codecrispi.es) +PARTICIPANTS := $(or $(WORKSHOP_PARTICIPANTS),3) USB_DEVICE := $(or $(USB_DEVICE),/dev/sdX) help: @@ -19,17 +20,21 @@ help: @echo " make flash-usb - Flash ISO to USB drive" @echo "" @echo "Local Development:" - @echo " make local-vm-run - Start local VM with containers" + @echo " make local-vm-run - Start local VM with 15 containers" @echo " make clean - Clean build artifacts" @echo "" + @echo "Development:" + @echo " make opencode - Start opencode in dev shell" + @echo " make lint - Run linting checks" + @echo "" @echo "Config: Domain=$(DOMAIN), USB=$(USB_DEVICE)" - @echo "Required: HCLOUD_TOKEN, SSH key at ~/.ssh/id_rsa.pub" + @echo "Required: HCLOUD_TOKEN, SSH key at ~/.ssh/id_ed25519.pub" build-usb: @echo "Building NixOS workshop ISO for $(DOMAIN)..." @if [ ! -f ~/.ssh/id_ed25519.pub ]; then \ echo "SSH key not found at ~/.ssh/id_ed25519.pub"; \ - echo "Generate with: ssh-keygen -t rsa -b 4096"; \ + echo "Generate with: ssh-keygen -t ed25519"; \ exit 1; \ fi nix build .#live-iso --show-trace @@ -54,8 +59,9 @@ deploy-cloud: echo "Get token from: https://console.hetzner.cloud/"; \ exit 1; \ fi - @if [ ! -f ~/.ssh/id_rsa.pub ]; then \ - echo "SSH key not found at ~/.ssh/id_rsa.pub"; \ + @if [ ! -f ~/.ssh/id_ed25519.pub ]; then \ + echo "SSH key not found at ~/.ssh/id_ed25519.pub"; \ + echo "Generate with: ssh-keygen -t ed25519"; \ exit 1; \ fi @echo "Deploying 15 workshop servers to Hetzner Cloud..." @@ -66,7 +72,7 @@ deploy-cloud: -var="hetzner_dns_token=$(HETZNER_DNS_TOKEN)" \ -var="dns_zone_id=$(DNS_ZONE_ID)" \ -var="domain=$(DOMAIN)" \ - -var="ssh_public_key=$$(cat ~/.ssh/id_rsa.pub)" + -var="ssh_public_key=$$(cat ~/.ssh/id_ed25519.pub)" @echo "Running health checks..." @sleep 60 $(MAKE) status-cloud @@ -91,8 +97,8 @@ destroy-cloud: cd terraform && terraform destroy -auto-approve local-vm-run: - @echo "Starting local workshop VM..." - @echo "VM will open with desktop showing 2 participant containers" + @echo "Starting local workshop VM with $(PARTICIPANTS) containers..." + @echo "VM will open with desktop showing all participant containers" nix run --impure .#local-vm clean: diff --git a/README.md b/README.md index 4b3f66b..10e4114 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ This repository contains the infrastructure for the Co-op Cloud workshop, provid ## ๐Ÿš€ Quick Start ```bash -# 1. Start the local development virtual machine +# 1. Start the local development virtual machine (15 containers) make local-vm-run # 2. Build & flash USB drives for participants @@ -16,9 +16,9 @@ make flash-usb USB_DEVICE=/dev/sdX # 3. Deploy the production cloud infrastructure export HCLOUD_TOKEN="your_token_here" make deploy-cloud -```` +``` ------ +--- ## ๐Ÿ“ Project Structure @@ -30,60 +30,119 @@ make deploy-cloud โ””โ”€โ”€ Makefile # Build & deploy commands ``` ------ +--- ## ๐ŸŒ Three Environments -### 1\. Cloud (Production) +### 1. Cloud (Production) - - [cite\_start]**What:** Hetzner VMs named `hopper.codecrispi.es`, `curie.codecrispi.es`, etc. [cite: 52] - - **Purpose:** The live environment for workshop participants. +- **What:** 15 Hetzner VMs named `hopper.codecrispi.es`, `curie.codecrispi.es`, etc. +- **Purpose:** The live environment for workshop participants. +- **Participants:** hopper, curie, lovelace, noether, hamilton, franklin, johnson, clarke, goldberg, liskov, wing, rosen, shaw, karp, rich -### 2\. USB Boot (Workshop) +### 2. USB Boot (Workshop) - - [cite\_start]**What:** A bootable NixOS live environment. [cite: 4] - - **Purpose:** Used by participants to connect to their cloud servers. [cite\_start]It includes helper functions like `connect hopper`. [cite: 12] +- **What:** A bootable NixOS live environment with SSH client tools. +- **Purpose:** Used by participants to connect to their cloud servers. +- **Features:** Pre-configured with helper functions like `connect hopper`, `recipes` command, and workshop-specific tooling. -### 3\. Local (Development) +### 3. Local (Development) - - **What:** A self-contained Virtual Machine (VM) that runs on your local computer. - - **Purpose:** The VM hosts simulated participant containers (e.g., `hopper.local`) and includes a lightweight desktop with a web browser, providing a perfect, isolated environment to test the entire workshop flow without needing cloud servers. +- **What:** A self-contained Virtual Machine (VM) that runs on your local computer with all 15 containers. +- **Purpose:** Complete local testing environment that mirrors production setup without needing cloud servers. +- **Resources:** Creates 15 containers (heavy resource usage - ensure adequate RAM/CPU) ------ +--- ## ๐Ÿ”ง Local Development Workflow -1. **Start the VM** - Run the following command. A new window will open and automatically boot into a lightweight desktop. +1. **Start the VM** + Run the following command. A new window will open and automatically boot into a lightweight desktop. - ```bash - make local-vm-run - ``` + ```bash + make local-vm-run + ``` -2. **Work Inside the VM** - All testing is now done inside the VM's graphical desktop. +2. **Work Inside the VM** + All testing is now done inside the VM's graphical desktop. - * Open the **Terminal** to run commands. - * Open **Firefox** to view the deployed web applications. + * Open the **Terminal** to run commands. + * Open **Firefox** to view the deployed web applications. -3. **Example: Deploying WordPress** +3. **Example: Deploying WordPress** - * **In the VM's Terminal**, get a root shell and SSH into the first participant's container: - ```bash - # Become root (no password needed) - sudo -i + * **In the VM's Terminal**, get a root shell and SSH into a participant's container: + ```bash + # Become root (no password needed) + sudo -i - # Connect to participant 1 (hopper.local) - ssh root@192.168.100.11 - ``` - * **Inside the container**, deploy a WordPress site with `abra`: - ```bash - abra app new wordpress -S --domain=blog.hopper.local - abra app deploy blog.hopper.local - ``` - * **In the VM's Firefox**, navigate to the address `http://blog.hopper.local`. You will see the WordPress installation screen. + # Connect to participant 1 (hopper) + connect hopper ------ + # Or direct SSH + ssh root@192.168.100.11 + ``` + * **Inside the container**, deploy a WordPress site with `abra`: + ```bash + abra app new wordpress -S --domain=blog.hopper.local + abra app deploy blog.hopper.local + ``` + * **In the VM's Firefox**, navigate to `http://blog.hopper.local`. You will see the WordPress installation screen. + +4. **Available Helper Commands** + ```bash + sudo containers # List all 15 containers with IPs + sudo logs # Show setup logs for all containers + sudo recipes # Display available Co-op Cloud recipes + sudo help # Show all available commands + ``` + +--- + +## ๐ŸŒ Cloud Deployment + +The cloud environment creates 15 production servers: + +```bash +# Set required environment variables +export HCLOUD_TOKEN="your_hetzner_token" +export HETZNER_DNS_TOKEN="your_dns_token" +export DNS_ZONE_ID="your_zone_id" + +# Deploy all 15 servers +make deploy-cloud + +# Check server status +make status-cloud +``` + +Each server is accessible at: +- `hopper.codecrispi.es` +- `curie.codecrispi.es` +- `lovelace.codecrispi.es` +- ... (15 total) + +--- + +## ๐Ÿ’พ USB Workshop Environment + +Build bootable USB drives for participants: + +```bash +# Build the ISO +make build-usb + +# Flash to USB drive (replace /dev/sdX with your device) +make flash-usb USB_DEVICE=/dev/sdb +``` + +The USB environment includes: +- Pre-configured SSH client +- `connect ` command to SSH into assigned servers +- `recipes` command showing available Co-op Cloud applications +- Workshop-specific networking and WiFi helpers + +--- ## ๐Ÿงน Cleanup @@ -91,8 +150,31 @@ make deploy-cloud # Clean local build artifacts make clean -# Destroy Hetzner cloud infrastructure +# Destroy Hetzner cloud infrastructure make destroy-cloud -# To stop the local VM, simply close its window. +# To stop the local VM, simply close its window ``` + +--- + +## ๐Ÿ”‘ Prerequisites + +- **SSH Key:** Ed25519 key at `~/.ssh/id_ed25519.pub` + ```bash + ssh-keygen -t ed25519 + ``` +- **Nix:** NixOS or Nix package manager with flakes enabled +- **Cloud Tokens:** Hetzner Cloud API token for deployment +- **Resources:** For local VM: 8GB+ RAM recommended (runs 15 containers) + +--- + +## ๐ŸŽฏ Workshop Flow + +1. **Preparation:** Deploy cloud infrastructure with `make deploy-cloud` +2. **Distribution:** Flash USB drives for participants with `make build-usb && make flash-usb` +3. **Workshop:** Participants boot from USB and connect to their assigned cloud servers +4. **Development:** Use local VM (`make local-vm-run`) for testing and development + +The architecture ensures participants get identical environments whether connecting from USB boot drives to cloud servers, or testing locally in the development VM. diff --git a/flake.nix b/flake.nix index a47994d..adb889e 100644 --- a/flake.nix +++ b/flake.nix @@ -13,8 +13,7 @@ let system = "x86_64-linux"; pkgs = nixpkgs.legacyPackages.${system}; - participantNames = [ "hopper" "curie" ]; - fullParticipantNames = [ + allParticipantNames = [ "hopper" "curie" "lovelace" @@ -31,6 +30,15 @@ "karp" "rich" ]; + numLookup = { + "0" = 0; "1" = 1; "2" = 2; "3" = 3; "4" = 4; "5" = 5; "6" = 6; "7" = 7; "8" = 8; "9" = 9; + "10" = 10; "11" = 11; "12" = 12; "13" = 13; "14" = 14; "15" = 15; + }; + participantsEnv = builtins.getEnv "PARTICIPANTS"; + numParticipants = if builtins.hasAttr participantsEnv numLookup + then builtins.getAttr participantsEnv numLookup + else 3; + participantNames = builtins.genList (i: builtins.elemAt allParticipantNames i) numParticipants; in { packages.${system} = { @@ -73,62 +81,62 @@ programs.zsh = { enable = true; interactiveShellInit = '' - echo "CODE CRISPIES Workshop Environment" - echo "Available servers:" - ${builtins.concatStringsSep "\n" (map (name: - "echo \" - ${name}.codecrispi.es\"" - ) fullParticipantNames)} - echo "" - echo "Commands: connect | recipes | help" - - connect() { - [ -z "$1" ] && { echo "Usage: connect "; return 1; } - echo "Connecting to $1.codecrispi.es..." - ssh -o StrictHostKeyChecking=no workshop@$1.codecrispi.es - } - - recipes() { - echo "Available Co-op Cloud Recipes:" - echo "" - echo "Content Management:" - echo " wordpress ghost hedgedoc dokuwiki mediawiki" - echo "" - echo "File & Collaboration:" - echo " nextcloud seafile collabora onlyoffice" - echo "" - echo "Communication:" - echo " jitsi-meet matrix-synapse rocketchat mattermost" - echo "" - echo "E-commerce & Business:" - echo " prestashop invoiceninja kimai pretix" - echo "" - echo "Development & Tools:" - echo " gitea drone n8n gitlab jupyter-lab" - echo "" - echo "Analytics & Monitoring:" - echo " plausible matomo uptime-kuma grafana" - echo "" - echo "Media & Social:" - echo " peertube funkwhale mastodon pixelfed jellyfin" - echo "" - echo "Deploy: abra app new -S --domain=myapp..codecrispi.es" - echo "Browse all: https://recipes.coopcloud.tech" - } - - help() { - echo "CODE CRISPIES Workshop Commands:" - echo "" - echo "connect - SSH to your assigned server" - echo "recipes - Show available app recipes" - echo "sudo nmcli dev wifi connect SSID password PASSWORD" - echo "" - echo "Examples:" - echo " connect hopper" - echo " sudo nmcli dev wifi connect CODE_CRISPIES_GUEST password workshop2024" - } - - export -f connect recipes help - ''; + echo "CODE CRISPIES Workshop Environment" + echo "Available servers:" + ${builtins.concatStringsSep "\n" (map (name: + "echo \" - ${name}.codecrispi.es\"" + ) allParticipantNames)} + echo "" + echo "Commands: connect | recipes | help" + + connect() { + [ -z "$1" ] && { echo "Usage: connect "; return 1; } + echo "Connecting to $1.codecrispi.es..." + ssh -o StrictHostKeyChecking=no workshop@$1.codecrispi.es + } + + recipes() { + echo "Available Co-op Cloud Recipes:" + echo "" + echo "Content Management:" + echo " wordpress ghost hedgedoc dokuwiki mediawiki" + echo "" + echo "File & Collaboration:" + echo " nextcloud seafile collabora onlyoffice" + echo "" + echo "Communication:" + echo " jitsi-meet matrix-synapse rocketchat mattermost" + echo "" + echo "E-commerce & Business:" + echo " prestashop invoiceninja kimai pretix" + echo "" + echo "Development & Tools:" + echo " gitea drone n8n gitlab jupyter-lab" + echo "" + echo "Analytics & Monitoring:" + echo " plausible matomo uptime-kuma grafana" + echo "" + echo "Media & Social:" + echo " peertube funkwhale mastodon pixelfed jellyfin" + echo "" + echo "Deploy: abra app new -S --domain=myapp..codecrispi.es" + echo "Browse all: https://recipes.coopcloud.tech" + } + + help() { + echo "CODE CRISPIES Workshop Commands:" + echo "" + echo "connect - SSH to your assigned server" + echo "recipes - Show available app recipes" + echo "sudo nmcli dev wifi connect SSID password PASSWORD" + echo "" + echo "Examples:" + echo " connect hopper" + echo " sudo nmcli dev wifi connect CODE_CRISPIES_GUEST password workshop2024" + } + + export -f connect recipes help + ''; }; services.xserver = { @@ -191,27 +199,32 @@ autoLogin.user = "workshop"; }; - services.xserver.displayManager.sessionCommands = '' - ${pkgs.xfce.xfce4-terminal}/bin/xfce4-terminal --title="Workshop Terminal" \ - --command="bash -c ' - echo \"Workshop VM Ready!\"; - echo \"\"; - echo \"SSH into containers:\"; - echo \" sudo connect hopper # Container login\"; - echo \" sudo connect curie # Container login\"; - echo \" ssh root@192.168.100.11 # Direct SSH to hopper\"; - echo \" ssh root@192.168.100.12 # Direct SSH to curie\"; - echo \"\"; - echo \"Container management:\"; - echo \" sudo containers # List all containers\"; - echo \" sudo logs # Show setup logs\"; - echo \"\"; - echo \"Abra is pre-installed in containers!\"; - echo \"\"; - bash - '" & - ''; - + services.xserver.displayManager.sessionCommands = '' + ${pkgs.xfce.xfce4-terminal}/bin/xfce4-terminal --title="Workshop Terminal" \ + --command="bash -c ' + echo "Workshop VM Ready!"; + echo ""; + echo "SSH into containers:"; + ${builtins.concatStringsSep " +" (map (name: + let ip = "192.168.100.${toString (11 + (builtins.elemAt (builtins.genList (x: x) (builtins.length participantNames)) + (builtins.elemAt + (builtins.filter (i: builtins.elemAt participantNames i == name) + (builtins.genList (x: x) (builtins.length participantNames))) 0)))}"; + in "echo \" sudo connect ${name} # Container login to ${name}\"" + ) participantNames)} + echo " (Total: ${toString numParticipants} containers)"; + echo ""; + echo "Container management:"; + echo " sudo containers # List all containers"; + echo " sudo logs # Show setup logs"; + echo " sudo recipes # Show available recipes"; + echo ""; + echo "Abra is pre-installed in containers!"; + echo ""; + bash + '" & + ''; environment.systemPackages = with pkgs; [ firefox curl @@ -220,25 +233,73 @@ nano tree nixos-container + (pkgs.writeScriptBin "connect" '' - #!/bin/bash - if [ -z "$1" ]; then - echo "Usage: connect " - echo "Available: hopper curie" - exit 1 - fi - exec nixos-container root-login "$1" - '') + #!/bin/bash + if [ -z "$1" ]; then + echo "Usage: connect " + echo "Available: ${builtins.concatStringsSep " " participantNames}" + exit 1 + fi + exec nixos-container root-login "$1" + '') + (pkgs.writeScriptBin "containers" '' - #!/bin/bash - exec nixos-container list - '') + #!/bin/bash + echo "Active containers:" + nixos-container list + '') + (pkgs.writeScriptBin "logs" '' - #!/bin/bash - exec journalctl -u container@hopper -u container@curie -f - '') + #!/bin/bash + echo "Showing logs for all containers (Ctrl+C to exit)" + exec journalctl -u container@* -f + '') + + (pkgs.writeScriptBin "recipes" '' + #!/bin/bash + echo "Available Co-op Cloud Recipes" + echo "Content Management:" + echo " wordpress ghost hedgedoc dokuwiki mediawiki" + echo "File & Collaboration:" + echo " nextcloud seafile collabora onlyoffice" + echo "Communication:" + echo " jitsi-meet matrix-synapse rocketchat mattermost" + echo "E-commerce & Business:" + echo " prestashop invoiceninja kimai pretix" + echo "Development & Tools:" + echo " gitea drone n8n gitlab jupyter-lab" + echo "Analytics & Monitoring:" + echo " plausible matomo uptime-kuma grafana" + echo "Media & Social:" + echo " peertube funkwhale mastodon pixelfed jellyfin" + echo "Usage in container:" + echo " abra app new -S --domain=myapp..local" + echo " abra app deploy myapp..local" + echo "Browse all: https://recipes.coopcloud.tech" + '') + + (pkgs.writeScriptBin "help" '' + #!/bin/bash + echo "CODE CRISPIES Workshop VM Commands:" + echo "" + echo "Container Management:" + echo " connect - SSH into specific container" + echo " containers - List all containers with IPs" + echo " logs - Show container setup logs" + echo "" + echo "Workshop Tools:" + echo " recipes - Show available Co-op Cloud recipes" + echo " help - Show this help" + echo "" + echo "Examples:" + echo " sudo connect hopper" + echo " ssh root@192.168.100.11" + echo "" + echo "Available containers: ${builtins.concatStringsSep " " participantNames}" + '') ]; - + # Add local DNS resolution for .local domains networking = { hostName = "workshop-vm"; firewall.enable = false; @@ -247,6 +308,12 @@ internalInterfaces = [ "ve-+" ]; externalInterface = "eth0"; }; + extraHosts = builtins.concatStringsSep "\n" (builtins.genList (i: + let + name = builtins.elemAt participantNames i; + ip = "192.168.100.${toString (11 + i)}"; + in "${ip} ${name}.local" + ) (builtins.length participantNames)); }; containers = builtins.listToAttrs (builtins.genList @@ -266,10 +333,10 @@ config = { system.stateVersion = "25.05"; - users.users.root.password = "root"; + users.users.root.password = ""; users.users.workshop = { isNormalUser = true; - password = "workshop"; + password = ""; extraGroups = [ "wheel" "docker" ]; }; @@ -278,6 +345,7 @@ settings = { PasswordAuthentication = true; PermitRootLogin = "yes"; + PermitEmptyPasswords = true; }; }; @@ -288,6 +356,7 @@ }; security.sudo.wheelNeedsPassword = false; + security.pam.services.login.allowNullPassword = true; virtualisation.docker.enable = true; environment.systemPackages = with pkgs; [ @@ -304,43 +373,43 @@ after = [ "network-online.target" "docker.service" ]; wants = [ "network-online.target" ]; script = '' - echo "Setting up ${name} container..." - - for i in {1..10}; do - if ${pkgs.curl}/bin/curl -s --max-time 5 google.com >/dev/null 2>&1; then - echo "Network ready" - break - fi - echo "Waiting for network... ($i/10)" - sleep 2 - done - - ${pkgs.docker}/bin/docker swarm init --advertise-addr ${ip} || true - - export HOME=/root - if [ ! -f /root/.local/bin/abra ]; then - echo "Installing abra..." - ${pkgs.curl}/bin/curl -fsSL https://install.abra.coopcloud.tech | ${pkgs.bash}/bin/bash - echo "Abra installed" - fi - - if ! grep -q "/.local/bin" /root/.bashrc 2>/dev/null; then - echo 'export PATH="$HOME/.local/bin:$PATH"' >> /root/.bashrc - fi - - if [ -f /root/.local/bin/abra ]; then - ln -sf /root/.local/bin/abra /usr/local/bin/abra 2>/dev/null || true - fi - - if [ -f /root/.local/bin/abra ]; then - export PATH="/root/.local/bin:$PATH" - /root/.local/bin/abra server add ${name}.local 2>/dev/null || true - fi - - echo "${name} container ready!" - echo "SSH: ssh root@${ip} (password: root)" - echo "Abra: Available via 'abra' command" - ''; + echo "Setting up ${name} container..." + + for i in {1..10}; do + if ${pkgs.curl}/bin/curl -s --max-time 5 google.com >/dev/null 2>&1; then + echo "Network ready" + break + fi + echo "Waiting for network... ($i/10)" + sleep 2 + done + + ${pkgs.docker}/bin/docker swarm init --advertise-addr ${ip} || true + + export HOME=/root + if [ ! -f /root/.local/bin/abra ]; then + echo "Installing abra..." + ${pkgs.curl}/bin/curl -fsSL https://install.abra.coopcloud.tech | ${pkgs.bash}/bin/bash + echo "Abra installed" + fi + + if ! grep -q "/.local/bin" /root/.bashrc 2>/dev/null; then + echo 'export PATH="$HOME/.local/bin:$PATH"' >> /root/.bashrc + fi + + if [ -f /root/.local/bin/abra ]; then + ln -sf /root/.local/bin/abra /usr/local/bin/abra 2>/dev/null || true + fi + + if [ -f /root/.local/bin/abra ]; then + export PATH="/root/.local/bin:$PATH" + /root/.local/bin/abra server add ${name}.local 2>/dev/null || true + fi + + echo "${name} container ready!" + echo "SSH: ssh root@${ip} (no password)" + echo "Abra: Available via 'abra' command" + ''; serviceConfig = { Type = "oneshot"; RemainAfterExit = true;