feat: Add dynamic participant scaling and improved UX

- Makefile: Add local-vm-test/local-vm-full targets, improve error messages
- README.md: Document dynamic scaling, add troubleshooting section
- flake.nix: Implement dynamic container generation based on PARTICIPANTS env var

This enables running the workshop VM with 1-15 containers instead of fixed 15,
making local development more accessible on resource-constrained machines.
This commit is contained in:
2025-08-14 18:52:04 +02:00
parent c49eb614d5
commit 82780552f0
3 changed files with 563 additions and 470 deletions

View File

@@ -4,67 +4,74 @@ export
.PHONY: help deploy-cloud build-usb flash-usb local-vm-run clean status destroy-cloud opencode lint .PHONY: help deploy-cloud build-usb flash-usb local-vm-run clean status destroy-cloud opencode lint
DOMAIN := $(or $(WORKSHOP_DOMAIN),codecrispi.es) DOMAIN := $(or $(WORKSHOP_DOMAIN),codecrispi.es)
PARTICIPANTS := $(or $(WORKSHOP_PARTICIPANTS),3) PARTICIPANTS := $(or $(PARTICIPANTS),3)
USB_DEVICE := $(or $(USB_DEVICE),/dev/sdX) USB_DEVICE := $(or $(USB_DEVICE),/dev/sdX)
help: help:
@echo "CODE CRISPIES Workshop" @echo "CODE CRISPIES Workshop Infrastructure"
@echo "" @echo ""
@echo "Cloud Infrastructure (Hetzner):" @echo "🌍 Cloud Infrastructure (Hetzner):"
@echo " make deploy-cloud - Deploy 15 VMs to Hetzner Cloud" @echo " make deploy-cloud - Deploy 15 VMs to Hetzner Cloud"
@echo " make status-cloud - Check server health" @echo " make status-cloud - Check server health"
@echo " make destroy-cloud - Destroy cloud infrastructure" @echo " make destroy-cloud - Destroy cloud infrastructure"
@echo "" @echo ""
@echo "USB Boot Drive:" @echo "💾 USB Boot Drive:"
@echo " make build-usb - Build NixOS workshop ISO" @echo " make build-usb - Build NixOS workshop ISO"
@echo " make flash-usb - Flash ISO to USB drive" @echo " make flash-usb - Flash ISO to USB drive"
@echo "" @echo ""
@echo "Local Development:" @echo "🖥️ Local Development:"
@echo " make local-vm-run - Start local VM with 15 containers" @echo " make local-vm-run - Start local VM with containers"
@echo " make local-vm-test - Test with 2 containers only"
@echo " make local-vm-full - Test with all 15 containers"
@echo " make clean - Clean build artifacts" @echo " make clean - Clean build artifacts"
@echo "" @echo ""
@echo "Development:" @echo "⚙️ Development:"
@echo " make opencode - Start opencode in dev shell" @echo " make opencode - Start opencode in dev shell"
@echo " make lint - Run linting checks" @echo " make lint - Run linting checks"
@echo " make check-vm - Verify VM builds correctly"
@echo ""
@echo "Current Config:"
@echo " Domain: $(DOMAIN)"
@echo " Participants: $(PARTICIPANTS)"
@echo " USB Device: $(USB_DEVICE)"
@echo "" @echo ""
@echo "Config: Domain=$(DOMAIN), USB=$(USB_DEVICE)"
@echo "Required: HCLOUD_TOKEN, SSH key at ~/.ssh/id_ed25519.pub" @echo "Required: HCLOUD_TOKEN, SSH key at ~/.ssh/id_ed25519.pub"
build-usb: build-usb:
@echo "Building NixOS workshop ISO for $(DOMAIN)..." @echo "🔨 Building NixOS workshop ISO..."
@if [ ! -f ~/.ssh/id_ed25519.pub ]; then \ @if [ ! -f ~/.ssh/id_ed25519.pub ]; then \
echo "SSH key not found at ~/.ssh/id_ed25519.pub"; \ echo "SSH key not found at ~/.ssh/id_ed25519.pub"; \
echo "Generate with: ssh-keygen -t ed25519"; \ echo "Generate with: ssh-keygen -t ed25519"; \
exit 1; \ exit 1; \
fi fi
nix build .#live-iso --show-trace nix build .#live-iso --show-trace
@echo "ISO built: result/iso/nixos.iso" @echo "ISO built: result/iso/nixos.iso"
@echo "Size: $$(du -h result/iso/nixos.iso | cut -f1)" @echo "📦 Size: $$(du -h result/iso/nixos.iso | cut -f1)"
flash-usb: build-usb flash-usb: build-usb
@if [ "$(USB_DEVICE)" = "/dev/sdX" ]; then \ @if [ "$(USB_DEVICE)" = "/dev/sdX" ]; then \
echo "Set USB_DEVICE=/dev/sdX (find with 'lsblk')"; \ echo "Set USB_DEVICE=/dev/sdX (find with 'lsblk')"; \
exit 1; \ exit 1; \
fi fi
@echo "About to flash $(USB_DEVICE) - THIS WILL ERASE ALL DATA!" @echo "⚠️ About to flash $(USB_DEVICE) - THIS WILL ERASE ALL DATA!"
@echo "Verify device: $$(lsblk $(USB_DEVICE) 2>/dev/null || echo 'DEVICE NOT FOUND')" @echo "Device info: $$(lsblk $(USB_DEVICE) 2>/dev/null || echo 'DEVICE NOT FOUND')"
@read -p "Continue? [y/N]: " confirm && [ "$$confirm" = "y" ] @read -p "Continue? [y/N]: " confirm && [ "$$confirm" = "y" ]
sudo dd if=result/iso/nixos.iso of=$(USB_DEVICE) bs=4M status=progress oflag=sync sudo dd if=result/iso/nixos.iso of=$(USB_DEVICE) bs=4M status=progress oflag=sync
sync sync
@echo "USB drive ready for workshop!" @echo "USB drive ready for workshop!"
deploy-cloud: deploy-cloud:
@if [ -z "$(HCLOUD_TOKEN)" ]; then \ @if [ -z "$(HCLOUD_TOKEN)" ]; then \
echo "HCLOUD_TOKEN not set"; \ echo "HCLOUD_TOKEN not set"; \
echo "Get token from: https://console.hetzner.cloud/"; \ echo "Get token from: https://console.hetzner.cloud/"; \
exit 1; \ exit 1; \
fi fi
@if [ ! -f ~/.ssh/id_ed25519.pub ]; then \ @if [ ! -f ~/.ssh/id_ed25519.pub ]; then \
echo "SSH key not found at ~/.ssh/id_ed25519.pub"; \ echo "SSH key not found at ~/.ssh/id_ed25519.pub"; \
echo "Generate with: ssh-keygen -t ed25519"; \ echo "Generate with: ssh-keygen -t ed25519"; \
exit 1; \ exit 1; \
fi fi
@echo "Deploying 15 workshop servers to Hetzner Cloud..." @echo "🚀 Deploying 15 workshop servers to Hetzner Cloud..."
@echo "Domain: $(DOMAIN)" @echo "Domain: $(DOMAIN)"
cd terraform && terraform init cd terraform && terraform init
cd terraform && terraform apply -auto-approve \ cd terraform && terraform apply -auto-approve \
@@ -73,47 +80,62 @@ deploy-cloud:
-var="dns_zone_id=$(DNS_ZONE_ID)" \ -var="dns_zone_id=$(DNS_ZONE_ID)" \
-var="domain=$(DOMAIN)" \ -var="domain=$(DOMAIN)" \
-var="ssh_public_key=$$(cat ~/.ssh/id_ed25519.pub)" -var="ssh_public_key=$$(cat ~/.ssh/id_ed25519.pub)"
@echo "Running health checks..." @echo "Running health checks..."
@sleep 60 @sleep 60
$(MAKE) status-cloud $(MAKE) status-cloud
@echo "Cloud deployment complete!" @echo "Cloud deployment complete!"
status-cloud: status-cloud:
@echo "Checking server health..." @echo "🔍 Checking server health..."
@for name in hopper curie lovelace noether hamilton franklin johnson clarke goldberg liskov wing rosen shaw karp rich; do \ @for name in hopper curie lovelace noether hamilton franklin johnson clarke goldberg liskov wing rosen shaw karp rich; do \
printf "%-10s " "$$name:"; \ printf "%-10s " "$$name:"; \
if timeout 10 curl -s -f https://traefik.$$name.$(DOMAIN)/ping >/dev/null 2>&1; then \ if timeout 10 curl -s -f https://traefik.$$name.$(DOMAIN)/ping >/dev/null 2>&1; then \
echo "Ready"; \ echo "Ready"; \
elif timeout 5 ssh -o ConnectTimeout=5 -o StrictHostKeyChecking=no workshop@$$name.$(DOMAIN) "echo ok" >/dev/null 2>&1; then \ elif timeout 5 ssh -o ConnectTimeout=5 -o StrictHostKeyChecking=no workshop@$$name.$(DOMAIN) "echo ok" >/dev/null 2>&1; then \
echo "SSH OK, Traefik starting..."; \ echo "SSH OK, Traefik starting..."; \
else \ else \
echo "Not ready"; \ echo "Not ready"; \
fi; \ fi; \
done done
destroy-cloud: destroy-cloud:
@echo "This will destroy ALL workshop servers!" @echo "⚠️ This will destroy ALL workshop servers!"
@read -p "Continue? [y/N]: " confirm && [ "$$confirm" = "y" ] @read -p "Continue? [y/N]: " confirm && [ "$$confirm" = "y" ]
cd terraform && terraform destroy -auto-approve cd terraform && terraform destroy -auto-approve
@echo "✅ Cloud infrastructure destroyed"
local-vm-run: local-vm-run:
@echo "Starting local workshop VM with $(PARTICIPANTS) containers..." @echo "🖥️ Starting local workshop VM with $(PARTICIPANTS) containers..."
@echo "VM will open with desktop showing all participant containers" @echo "VM will open with desktop showing all participant containers"
nix run --impure .#local-vm PARTICIPANTS=$(PARTICIPANTS) nix run --impure .#local-vm
local-vm-test:
@echo "🧪 Testing with 2 containers only..."
PARTICIPANTS=2 nix run --impure .#local-vm
local-vm-full:
@echo "🚀 Testing with all 15 containers (heavy resource usage!)..."
PARTICIPANTS=15 nix run --impure .#local-vm
check-vm:
@echo "✅ Verifying VM builds correctly..."
PARTICIPANTS=2 nix build --impure .#local-vm
@echo "✅ VM build successful"
clean: clean:
rm -rf result .direnv terraform/.terraform terraform/terraform.tfstate* rm -rf result .direnv terraform/.terraform terraform/terraform.tfstate*
@echo "Cleaned up build artifacts" @echo "🧹 Cleaned up build artifacts"
opencode: opencode:
@echo "Starting opencode in Nix dev shell..." @echo "💻 Starting opencode in Nix dev shell..."
nix develop --command opencode nix develop --command opencode
lint: lint:
@echo "Linting Markdown files..." @echo "🔍 Linting project files..."
@echo "Markdown files..."
@markdownlint-cli . || true @markdownlint-cli . || true
@echo "Linting JSON files..." @echo "JSON files..."
@find . -type f -name "*.json" -print0 | xargs -0 -I {} bash -c 'jq . "{}" >/dev/null || (echo "JSON lint error in {}" && exit 1)' @find . -type f -name "*.json" -print0 | xargs -0 -I {} bash -c 'jq . "{}" >/dev/null || (echo "JSON lint error in {}" && exit 1)'
@echo "Linting Nix files..." @echo "Nix files..."
@nixpkgs-fmt . || true @nixpkgs-fmt --check . || true
@echo "Linting complete." @echo "Linting complete"

133
README.md
View File

@@ -1,19 +1,23 @@
# 🍪 CODE CRISPIES Workshop Infrastructure # 🍪 CODE CRISPIES Workshop Infrastructure
This repository contains the infrastructure for the Co-op Cloud workshop, providing three distinct deployment environments. This repository contains the infrastructure for the Co-op Cloud workshop, providing three distinct deployment environments with dynamic scaling support.
--- ---
## 🚀 Quick Start ## 🚀 Quick Start
```bash ```bash
# 1. Start the local development virtual machine (15 containers) # 1. Start the local development virtual machine (default: 3 containers)
make local-vm-run make local-vm-run
# 2. Build & flash USB drives for participants # 2. Test with different container counts
PARTICIPANTS=2 make local-vm-test # Lightweight testing
PARTICIPANTS=15 make local-vm-full # Full workshop simulation
# 3. Build & flash USB drives for participants
make build-usb make build-usb
make flash-usb USB_DEVICE=/dev/sdX make flash-usb USB_DEVICE=/dev/sdX
# 3. Deploy the production cloud infrastructure # 4. Deploy the production cloud infrastructure
export HCLOUD_TOKEN="your_token_here" export HCLOUD_TOKEN="your_token_here"
make deploy-cloud make deploy-cloud
``` ```
@@ -23,7 +27,7 @@ make deploy-cloud
## 📁 Project Structure ## 📁 Project Structure
``` ```
├── flake.nix # All Nix configurations (USB, VM) ├── flake.nix # All Nix configurations (USB, VM, containers)
├── terraform/ # Hetzner Cloud infrastructure ├── terraform/ # Hetzner Cloud infrastructure
├── scripts/deploy.sh # Cloud setup automation ├── scripts/deploy.sh # Cloud setup automation
├── docs/USB_BOOT_INSTRUCTIONS.md ├── docs/USB_BOOT_INSTRUCTIONS.md
@@ -48,50 +52,56 @@ make deploy-cloud
### 3. Local (Development) ### 3. Local (Development)
- **What:** A self-contained Virtual Machine (VM) that runs on your local computer with all 15 containers. - **What:** A self-contained Virtual Machine (VM) that runs on your local computer with configurable container count.
- **Purpose:** Complete local testing environment that mirrors production setup without needing cloud servers. - **Purpose:** Complete local testing environment that mirrors production setup without needing cloud servers.
- **Resources:** Creates 15 containers (heavy resource usage - ensure adequate RAM/CPU) - **Scalability:** Supports 1-15 containers via `PARTICIPANTS` environment variable.
--- ---
## 🔧 Local Development Workflow ## 🔧 Local Development Workflow
1. **Start the VM** 1. **Choose Your Scale**
Run the following command. A new window will open and automatically boot into a lightweight desktop.
```bash ```bash
# Lightweight development (2 containers)
PARTICIPANTS=2 make local-vm-run
# Production simulation (15 containers) - requires 8GB+ RAM
PARTICIPANTS=15 make local-vm-run
# Use default (3 containers) - good balance
make local-vm-run make local-vm-run
``` ```
2. **Work Inside the VM** 2. **Work Inside the VM**
All testing is now done inside the VM's graphical desktop. All testing is now done inside the VM's graphical desktop:
* Open the **Terminal** to run commands. * Open the **Terminal** to run commands.
* Open **Firefox** to view the deployed web applications. * 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 a participant's container: **In the VM's Terminal**, get a root shell and SSH into a participant's container:
```bash ```bash
# Become root (no password needed) # Become root (no password needed)
sudo -i sudo -i
# Connect to participant 1 (hopper) # Connect to participant 1 (hopper)
connect hopper connect hopper
# Or direct SSH # Or direct SSH (password: root)
ssh root@192.168.100.11 ssh root@192.168.100.11
``` ```
* **Inside the container**, deploy a WordPress site with `abra`:
```bash **Inside the container**, deploy a WordPress site with `abra`:
abra app new wordpress -S --domain=blog.hopper.local ```bash
abra app deploy blog.hopper.local 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. ```
**In the VM's Firefox**, navigate to `http://blog.hopper.local`. You will see the WordPress installation screen.
4. **Available Helper Commands** 4. **Available Helper Commands**
```bash ```bash
sudo containers # List all 15 containers with IPs sudo containers # List all containers with IPs
sudo logs # Show setup logs for all containers sudo logs # Show setup logs for all containers
sudo recipes # Display available Co-op Cloud recipes sudo recipes # Display available Co-op Cloud recipes
sudo help # Show all available commands sudo help # Show all available commands
@@ -144,7 +154,25 @@ The USB environment includes:
--- ---
## 🧹 Cleanup ## ⚙️ Environment Variables
Control workshop behavior with environment variables:
```bash
# Number of containers (1-15, default: 3)
export PARTICIPANTS=5
make local-vm-run
# Workshop domain for cloud deployment
export WORKSHOP_DOMAIN=myworkshop.com
# USB device for flashing
export USB_DEVICE=/dev/sdb
```
---
## 🧹 Cleanup & Management
```bash ```bash
# Clean local build artifacts # Clean local build artifacts
@@ -153,7 +181,12 @@ make clean
# Destroy Hetzner cloud infrastructure # Destroy Hetzner cloud infrastructure
make destroy-cloud make destroy-cloud
# To stop the local VM, simply close its window # Verify VM builds correctly
make check-vm
# Run development tools
make opencode # Start development environment
make lint # Code quality checks
``` ```
--- ---
@@ -166,7 +199,10 @@ make destroy-cloud
``` ```
- **Nix:** NixOS or Nix package manager with flakes enabled - **Nix:** NixOS or Nix package manager with flakes enabled
- **Cloud Tokens:** Hetzner Cloud API token for deployment - **Cloud Tokens:** Hetzner Cloud API token for deployment
- **Resources:** For local VM: 8GB+ RAM recommended (runs 15 containers) - **Resources:**
- 2-3 containers: 4GB+ RAM
- 5-10 containers: 8GB+ RAM
- 15 containers: 16GB+ RAM
--- ---
@@ -175,6 +211,39 @@ make destroy-cloud
1. **Preparation:** Deploy cloud infrastructure with `make deploy-cloud` 1. **Preparation:** Deploy cloud infrastructure with `make deploy-cloud`
2. **Distribution:** Flash USB drives for participants with `make build-usb && make flash-usb` 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 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 4. **Development:** Use local VM with `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. The architecture ensures participants get identical environments whether connecting from USB boot drives to cloud servers, or testing locally in the development VM.
---
## 🐛 Troubleshooting
### VM Won't Start
```bash
# Check if build works
make check-vm
# Try with fewer containers
PARTICIPANTS=2 make local-vm-run
```
### Containers Not Accessible
```bash
# Check container status inside VM
sudo containers
# View setup logs
sudo logs
# Manual SSH test
ssh root@192.168.100.11 # Password: root
```
### Abra Not Working in Container
```bash
# Inside container, check installation
ls -la /root/.local/bin/abra
export PATH="/root/.local/bin:$PATH"
abra --version
```

774
flake.nix
View File

@@ -1,433 +1,435 @@
{ {
description = "Workshop VM with Participant Containers + USB ISO"; description = "Workshop VM with Participant Containers + USB ISO";
inputs = { inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.05"; nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.05";
nixos-generators = { nixos-generators = {
url = "github:nix-community/nixos-generators"; url = "github:nix-community/nixos-generators";
inputs.nixpkgs.follows = "nixpkgs"; inputs.nixpkgs.follows = "nixpkgs";
}; };
}; };
outputs = { self, nixpkgs, nixos-generators }: outputs = { self, nixpkgs, nixos-generators }:
let let
system = "x86_64-linux"; system = "x86_64-linux";
pkgs = nixpkgs.legacyPackages.${system}; pkgs = nixpkgs.legacyPackages.${system};
allParticipantNames = [
"hopper"
"curie"
"lovelace"
"noether"
"hamilton"
"franklin"
"johnson"
"clarke"
"goldberg"
"liskov"
"wing"
"rosen"
"shaw"
"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} = {
local-vm = self.nixosConfigurations.workshop-vm.config.system.build.vm;
live-iso = nixos-generators.nixosGenerate { # All possible participant names for the workshop
inherit system; allParticipantNames = [
format = "iso"; "hopper" "curie" "lovelace" "noether" "hamilton"
"franklin" "johnson" "clarke" "goldberg" "liskov"
"wing" "rosen" "shaw" "karp" "rich"
];
modules = [ # Dynamic participant count (default 3, max 15)
({ pkgs, ... }: { participantsEnv = builtins.getEnv "PARTICIPANTS";
system.stateVersion = "25.05"; numParticipants =
if participantsEnv != "" && builtins.match "^[0-9]+$" participantsEnv != null
then
let num = builtins.fromJSON participantsEnv;
in if num >= 1 && num <= 15 then num else 3
else 3;
isoImage.makeEfiBootable = true; # Selected participant names based on count
isoImage.makeUsbBootable = true; participantNames = builtins.genList
(i: builtins.elemAt allParticipantNames i)
numParticipants;
in
{
packages.${system} = {
local-vm = self.nixosConfigurations.workshop-vm.config.system.build.vm;
networking.wireless.enable = true; live-iso = nixos-generators.nixosGenerate {
networking.networkmanager.enable = true; inherit system;
networking.hostName = "workshop-live"; format = "iso";
services.getty.autologinUser = "workshop"; modules = [
users.users.workshop = { ({ pkgs, ... }: {
isNormalUser = true; system.stateVersion = "25.05";
shell = pkgs.zsh;
extraGroups = [ "networkmanager" "wheel" ];
password = "";
};
security.sudo.wheelNeedsPassword = false; isoImage.makeEfiBootable = true;
isoImage.makeUsbBootable = true;
environment.systemPackages = with pkgs; [ networking.wireless.enable = true;
openssh networking.networkmanager.enable = true;
curl networking.hostName = "workshop-live";
git
networkmanager
firefox
xterm
];
programs.zsh = { services.getty.autologinUser = "workshop";
enable = true; users.users.workshop = {
interactiveShellInit = '' isNormalUser = true;
echo "CODE CRISPIES Workshop Environment" shell = pkgs.zsh;
echo "Available servers:" extraGroups = [ "networkmanager" "wheel" ];
${builtins.concatStringsSep "\n" (map (name: password = "";
"echo \" - ${name}.codecrispi.es\"" };
) allParticipantNames)}
echo ""
echo "Commands: connect <name> | recipes | help"
connect() { security.sudo.wheelNeedsPassword = false;
[ -z "$1" ] && { echo "Usage: connect <name>"; return 1; }
echo "Connecting to $1.codecrispi.es..."
ssh -o StrictHostKeyChecking=no workshop@$1.codecrispi.es
}
recipes() { environment.systemPackages = with pkgs; [
echo "Available Co-op Cloud Recipes:" openssh curl git networkmanager firefox xterm
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 <recipe> -S --domain=myapp.<name>.codecrispi.es"
echo "Browse all: https://recipes.coopcloud.tech"
}
help() { programs.zsh = {
echo "CODE CRISPIES Workshop Commands:" enable = true;
echo "" interactiveShellInit = ''
echo "connect <name> - SSH to your assigned server" echo "CODE CRISPIES Workshop Environment"
echo "recipes - Show available app recipes" echo "Available servers:"
echo "sudo nmcli dev wifi connect SSID password PASSWORD" ${builtins.concatStringsSep "\n" (map (name:
echo "" "echo \" - ${name}.codecrispi.es\""
echo "Examples:" ) allParticipantNames)}
echo " connect hopper" echo ""
echo " sudo nmcli dev wifi connect CODE_CRISPIES_GUEST password workshop2024" echo "Commands: connect <name> | recipes | help"
}
export -f connect recipes help connect() {
''; [ -z "$1" ] && { echo "Usage: connect <name>"; return 1; }
}; echo "Connecting to $1.codecrispi.es..."
ssh -o StrictHostKeyChecking=no workshop@$1.codecrispi.es
}
services.xserver = { recipes() {
enable = true; echo "Available Co-op Cloud Recipes:"
desktopManager.xfce.enable = true; echo ""
displayManager = { echo "Content Management:"
lightdm.enable = true; echo " wordpress ghost hedgedoc dokuwiki mediawiki"
autoLogin.enable = true; echo ""
autoLogin.user = "workshop"; 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 <recipe> -S --domain=myapp.<name>.codecrispi.es"
echo "Browse all: https://recipes.coopcloud.tech"
}
systemd.user.services.workshop-welcome = { help() {
wantedBy = [ "graphical-session.target" ]; echo "CODE CRISPIES Workshop Commands:"
after = [ "graphical-session.target" ]; echo ""
script = "${pkgs.xterm}/bin/xterm -title 'CODE CRISPIES Workshop' -e 'zsh' &"; echo "connect <name> - SSH to your assigned server"
serviceConfig.Type = "forking"; 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"
}
devShells.${system}.default = pkgs.mkShell { export -f connect recipes help
packages = with pkgs; [ '';
markdownlint-cli };
jq
nixpkgs-fmt
];
};
nixosConfigurations.workshop-vm = nixpkgs.lib.nixosSystem { services.xserver = {
inherit system; enable = true;
modules = [ desktopManager.xfce.enable = true;
({ config, pkgs, ... }: { displayManager = {
system.stateVersion = "25.05"; lightdm.enable = true;
autoLogin.enable = true;
autoLogin.user = "workshop";
};
};
boot.loader.grub.enable = false; systemd.user.services.workshop-welcome = {
boot.loader.generic-extlinux-compatible.enable = true; wantedBy = [ "graphical-session.target" ];
boot.kernel.sysctl."net.ipv4.ip_forward" = 1; after = [ "graphical-session.target" ];
script = "${pkgs.xterm}/bin/xterm -title 'CODE CRISPIES Workshop' -e 'zsh' &";
serviceConfig.Type = "forking";
};
})
];
};
};
users.users.workshop = { devShells.${system}.default = pkgs.mkShell {
isNormalUser = true; packages = with pkgs; [
extraGroups = [ "wheel" ]; markdownlint-cli
password = ""; jq
shell = pkgs.bash; nixpkgs-fmt
}; ];
};
security.pam.services.login.allowNullPassword = true; nixosConfigurations.workshop-vm = nixpkgs.lib.nixosSystem {
security.sudo.wheelNeedsPassword = false; inherit system;
modules = [
({ config, pkgs, ... }: {
system.stateVersion = "25.05";
services.xserver = { boot.loader.grub.enable = false;
enable = true; boot.loader.generic-extlinux-compatible.enable = true;
desktopManager.xfce.enable = true; boot.kernel.sysctl."net.ipv4.ip_forward" = 1;
displayManager.lightdm.enable = true;
};
services.displayManager = { users.users.workshop = {
autoLogin.enable = true; isNormalUser = true;
autoLogin.user = "workshop"; extraGroups = [ "wheel" ];
}; password = "workshop";
shell = pkgs.bash;
};
services.xserver.displayManager.sessionCommands = '' security.sudo.wheelNeedsPassword = false;
${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
git
jq
nano
tree
nixos-container
(pkgs.writeScriptBin "connect" '' services.xserver = {
#!/bin/bash enable = true;
if [ -z "$1" ]; then desktopManager.xfce.enable = true;
echo "Usage: connect <container-name>" displayManager.lightdm.enable = true;
echo "Available: ${builtins.concatStringsSep " " participantNames}" };
exit 1
fi
exec nixos-container root-login "$1"
'')
(pkgs.writeScriptBin "containers" '' services.displayManager = {
#!/bin/bash autoLogin.enable = true;
echo "Active containers:" autoLogin.user = "workshop";
nixos-container list };
'')
(pkgs.writeScriptBin "logs" '' services.xserver.displayManager.sessionCommands = ''
#!/bin/bash ${pkgs.xfce.xfce4-terminal}/bin/xfce4-terminal --title="Workshop Terminal" \
echo "Showing logs for all containers (Ctrl+C to exit)" --command="bash -c '
exec journalctl -u container@* -f echo \"Workshop VM Ready!\";
'') echo \"\";
echo \"SSH into containers:\";
${builtins.concatStringsSep "\n" (builtins.genList (i:
let
name = builtins.elemAt participantNames i;
ip = "192.168.100.${toString (11 + i)}";
in "echo \" sudo connect ${name} # Container login to ${name} (${ip})\""
) (builtins.length 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
'" &
'';
(pkgs.writeScriptBin "recipes" '' environment.systemPackages = with pkgs; [
#!/bin/bash firefox curl git jq nano tree nixos-container
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 <recipe> -S --domain=myapp.<container-name>.local"
echo " abra app deploy myapp.<container-name>.local"
echo "Browse all: https://recipes.coopcloud.tech"
'')
(pkgs.writeScriptBin "help" '' (pkgs.writeScriptBin "connect" ''
#!/bin/bash #!/bin/bash
echo "CODE CRISPIES Workshop VM Commands:" if [ -z "$1" ]; then
echo "" echo "Usage: connect <container-name>"
echo "Container Management:" echo "Available: ${builtins.concatStringsSep " " participantNames}"
echo " connect <name> - SSH into specific container" exit 1
echo " containers - List all containers with IPs" fi
echo " logs - Show container setup logs" exec nixos-container root-login "$1"
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;
nat = {
enable = true;
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 (pkgs.writeScriptBin "containers" ''
(i: #!/bin/bash
let echo "Active containers:"
name = builtins.elemAt participantNames i; nixos-container list
ip = "192.168.100.${toString (11 + i)}"; echo ""
in echo "Container IPs:"
{ ${builtins.concatStringsSep "\n" (builtins.genList (i:
inherit name; let
value = { name = builtins.elemAt participantNames i;
autoStart = true; ip = "192.168.100.${toString (11 + i)}";
privateNetwork = true; in "echo \" ${name}: ${ip}\""
hostAddress = "192.168.100.1"; ) (builtins.length participantNames))}
localAddress = ip; '')
config = { (pkgs.writeScriptBin "logs" ''
system.stateVersion = "25.05"; #!/bin/bash
echo "Showing logs for all containers (Ctrl+C to exit)"
exec journalctl -u container@* -f
'')
users.users.root.password = ""; (pkgs.writeScriptBin "recipes" ''
users.users.workshop = { #!/bin/bash
isNormalUser = true; echo "Available Co-op Cloud Recipes:"
password = ""; echo ""
extraGroups = [ "wheel" "docker" ]; 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 "Usage in container:"
echo " abra app new <recipe> -S --domain=myapp.<container-name>.local"
echo " abra app deploy myapp.<container-name>.local"
echo ""
echo "Browse all: https://recipes.coopcloud.tech"
'')
services.openssh = { (pkgs.writeScriptBin "help" ''
enable = true; #!/bin/bash
settings = { echo "CODE CRISPIES Workshop VM Commands:"
PasswordAuthentication = true; echo ""
PermitRootLogin = "yes"; echo "Container Management:"
PermitEmptyPasswords = true; echo " connect <name> - 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 (${toString numParticipants}): ${builtins.concatStringsSep " " participantNames}"
'')
];
networking = { # Local DNS resolution for .local domains
hostName = name; networking = {
nameservers = [ "8.8.8.8" ]; hostName = "workshop-vm";
firewall.enable = false; firewall.enable = false;
}; nat = {
enable = true;
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));
};
security.sudo.wheelNeedsPassword = false; # Dynamic container generation
security.pam.services.login.allowNullPassword = true; containers = builtins.listToAttrs (builtins.genList
virtualisation.docker.enable = true; (i:
let
name = builtins.elemAt participantNames i;
ip = "192.168.100.${toString (11 + i)}";
in
{
inherit name;
value = {
autoStart = true;
privateNetwork = true;
hostAddress = "192.168.100.1";
localAddress = ip;
environment.systemPackages = with pkgs; [ config = {
docker system.stateVersion = "25.05";
curl
git
wget
jq
bash
];
systemd.services.workshop-setup = { users.users.root.password = "root";
wantedBy = [ "multi-user.target" ]; users.users.workshop = {
after = [ "network-online.target" "docker.service" ]; isNormalUser = true;
wants = [ "network-online.target" ]; password = "workshop";
script = '' extraGroups = [ "wheel" "docker" ];
echo "Setting up ${name} container..." };
for i in {1..10}; do services.openssh = {
if ${pkgs.curl}/bin/curl -s --max-time 5 google.com >/dev/null 2>&1; then enable = true;
echo "Network ready" settings = {
break PasswordAuthentication = true;
fi PermitRootLogin = "yes";
echo "Waiting for network... ($i/10)" };
sleep 2 };
done
${pkgs.docker}/bin/docker swarm init --advertise-addr ${ip} || true networking = {
hostName = name;
nameservers = [ "8.8.8.8" "1.1.1.1" ];
firewall.enable = false;
};
export HOME=/root security.sudo.wheelNeedsPassword = false;
if [ ! -f /root/.local/bin/abra ]; then virtualisation.docker.enable = true;
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 environment.systemPackages = with pkgs; [
echo 'export PATH="$HOME/.local/bin:$PATH"' >> /root/.bashrc docker curl git wget jq bash nano tree
fi ];
if [ -f /root/.local/bin/abra ]; then systemd.services.workshop-setup = {
ln -sf /root/.local/bin/abra /usr/local/bin/abra 2>/dev/null || true wantedBy = [ "multi-user.target" ];
fi after = [ "network-online.target" "docker.service" ];
wants = [ "network-online.target" ];
script = ''
echo "Setting up ${name} container..."
if [ -f /root/.local/bin/abra ]; then # Wait for network connectivity
export PATH="/root/.local/bin:$PATH" for i in {1..15}; do
/root/.local/bin/abra server add ${name}.local 2>/dev/null || true if ${pkgs.curl}/bin/curl -s --max-time 5 google.com >/dev/null 2>&1; then
fi echo "Network ready"
break
fi
echo "Waiting for network... ($i/15)"
sleep 3
done
echo "${name} container ready!" # Initialize Docker Swarm
echo "SSH: ssh root@${ip} (no password)" ${pkgs.docker}/bin/docker swarm init --advertise-addr ${ip} || echo "Swarm already initialized or failed"
echo "Abra: Available via 'abra' command"
'';
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
StandardOutput = "journal";
StandardError = "journal";
};
};
environment.sessionVariables = { # Install abra
PATH = [ "/root/.local/bin" ]; 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 to /root/.local/bin/abra"
) fi
(builtins.length participantNames));
}) # Setup PATH in .bashrc
]; if ! grep -q "/.local/bin" /root/.bashrc 2>/dev/null; then
}; echo 'export PATH="$HOME/.local/bin:$PATH"' >> /root/.bashrc
}; fi
# Create system symlink for abra
if [ -f /root/.local/bin/abra ]; then
ln -sf /root/.local/bin/abra /usr/local/bin/abra 2>/dev/null || true
fi
# Add abra server config
if [ -f /root/.local/bin/abra ]; then
export PATH="/root/.local/bin:$PATH"
/root/.local/bin/abra server add ${name}.local 2>/dev/null || echo "Server already added or command failed"
fi
echo "${name} container ready!"
echo "SSH: ssh root@${ip} (password: root)"
echo "Workshop user: ssh workshop@${ip} (password: workshop)"
echo "Abra: Available via 'abra' command"
'';
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
StandardOutput = "journal";
StandardError = "journal";
TimeoutStartSec = "300";
};
};
environment.sessionVariables = {
PATH = [ "/root/.local/bin" ];
};
};
};
}
)
(builtins.length participantNames));
})
];
};
};
} }