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"

135
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:
```bash
# Become root (no password needed)
sudo -i
* **In the VM's Terminal**, get a root shell and SSH into a participant's container: # Connect to participant 1 (hopper)
```bash connect hopper
# Become root (no password needed)
sudo -i
# Connect to participant 1 (hopper) # Or direct SSH (password: root)
connect hopper ssh root@192.168.100.11
```
# Or direct SSH
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
```

806
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 = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.05";
nixos-generators = {
url = "github:nix-community/nixos-generators";
inputs.nixpkgs.follows = "nixpkgs";
};
};
inputs = { outputs = { self, nixpkgs, nixos-generators }:
nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.05"; let
nixos-generators = { system = "x86_64-linux";
url = "github:nix-community/nixos-generators"; pkgs = nixpkgs.legacyPackages.${system};
inputs.nixpkgs.follows = "nixpkgs";
}; # All possible participant names for the workshop
}; allParticipantNames = [
"hopper" "curie" "lovelace" "noether" "hamilton"
"franklin" "johnson" "clarke" "goldberg" "liskov"
"wing" "rosen" "shaw" "karp" "rich"
];
# Dynamic participant count (default 3, max 15)
participantsEnv = builtins.getEnv "PARTICIPANTS";
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;
# Selected participant names based on count
participantNames = builtins.genList
(i: builtins.elemAt allParticipantNames i)
numParticipants;
in
{
packages.${system} = {
local-vm = self.nixosConfigurations.workshop-vm.config.system.build.vm;
outputs = { self, nixpkgs, nixos-generators }: live-iso = nixos-generators.nixosGenerate {
let inherit system;
system = "x86_64-linux"; format = "iso";
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 { modules = [
inherit system; ({ pkgs, ... }: {
format = "iso"; system.stateVersion = "25.05";
modules = [ isoImage.makeEfiBootable = true;
({ pkgs, ... }: { isoImage.makeUsbBootable = true;
system.stateVersion = "25.05";
isoImage.makeEfiBootable = true; networking.wireless.enable = true;
isoImage.makeUsbBootable = true; networking.networkmanager.enable = true;
networking.hostName = "workshop-live";
networking.wireless.enable = true; services.getty.autologinUser = "workshop";
networking.networkmanager.enable = true; users.users.workshop = {
networking.hostName = "workshop-live"; isNormalUser = true;
shell = pkgs.zsh;
extraGroups = [ "networkmanager" "wheel" ];
password = "";
};
services.getty.autologinUser = "workshop"; security.sudo.wheelNeedsPassword = false;
users.users.workshop = {
isNormalUser = true;
shell = pkgs.zsh;
extraGroups = [ "networkmanager" "wheel" ];
password = "";
};
security.sudo.wheelNeedsPassword = false; environment.systemPackages = with pkgs; [
openssh curl git networkmanager firefox xterm
];
environment.systemPackages = with pkgs; [ programs.zsh = {
openssh enable = true;
curl interactiveShellInit = ''
git echo "CODE CRISPIES Workshop Environment"
networkmanager echo "Available servers:"
firefox ${builtins.concatStringsSep "\n" (map (name:
xterm "echo \" - ${name}.codecrispi.es\""
]; ) allParticipantNames)}
echo ""
echo "Commands: connect <name> | 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
}
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 <recipe> -S --domain=myapp.<name>.codecrispi.es"
echo "Browse all: https://recipes.coopcloud.tech"
}
help() {
echo "CODE CRISPIES Workshop Commands:"
echo ""
echo "connect <name> - 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
'';
};
programs.zsh = { services.xserver = {
enable = true; enable = true;
interactiveShellInit = '' desktopManager.xfce.enable = true;
echo "CODE CRISPIES Workshop Environment" displayManager = {
echo "Available servers:" lightdm.enable = true;
${builtins.concatStringsSep "\n" (map (name: autoLogin.enable = true;
"echo \" - ${name}.codecrispi.es\"" autoLogin.user = "workshop";
) allParticipantNames)} };
echo "" };
echo "Commands: connect <name> | 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
}
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 <recipe> -S --domain=myapp.<name>.codecrispi.es"
echo "Browse all: https://recipes.coopcloud.tech"
}
help() {
echo "CODE CRISPIES Workshop Commands:"
echo ""
echo "connect <name> - 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 = { systemd.user.services.workshop-welcome = {
enable = true; wantedBy = [ "graphical-session.target" ];
desktopManager.xfce.enable = true; after = [ "graphical-session.target" ];
displayManager = { script = "${pkgs.xterm}/bin/xterm -title 'CODE CRISPIES Workshop' -e 'zsh' &";
lightdm.enable = true; serviceConfig.Type = "forking";
autoLogin.enable = true; };
autoLogin.user = "workshop"; })
}; ];
}; };
};
systemd.user.services.workshop-welcome = { devShells.${system}.default = pkgs.mkShell {
wantedBy = [ "graphical-session.target" ]; packages = with pkgs; [
after = [ "graphical-session.target" ]; markdownlint-cli
script = "${pkgs.xterm}/bin/xterm -title 'CODE CRISPIES Workshop' -e 'zsh' &"; jq
serviceConfig.Type = "forking"; nixpkgs-fmt
}; ];
}) };
];
};
};
devShells.${system}.default = pkgs.mkShell { nixosConfigurations.workshop-vm = nixpkgs.lib.nixosSystem {
packages = with pkgs; [ inherit system;
markdownlint-cli modules = [
jq ({ config, pkgs, ... }: {
nixpkgs-fmt system.stateVersion = "25.05";
];
};
nixosConfigurations.workshop-vm = nixpkgs.lib.nixosSystem { boot.loader.grub.enable = false;
inherit system; boot.loader.generic-extlinux-compatible.enable = true;
modules = [ boot.kernel.sysctl."net.ipv4.ip_forward" = 1;
({ config, pkgs, ... }: {
system.stateVersion = "25.05";
boot.loader.grub.enable = false; users.users.workshop = {
boot.loader.generic-extlinux-compatible.enable = true; isNormalUser = true;
boot.kernel.sysctl."net.ipv4.ip_forward" = 1; extraGroups = [ "wheel" ];
password = "workshop";
shell = pkgs.bash;
};
users.users.workshop = { security.sudo.wheelNeedsPassword = false;
isNormalUser = true;
extraGroups = [ "wheel" ];
password = "";
shell = pkgs.bash;
};
security.pam.services.login.allowNullPassword = true; services.xserver = {
security.sudo.wheelNeedsPassword = false; enable = true;
desktopManager.xfce.enable = true;
displayManager.lightdm.enable = true;
};
services.xserver = { services.displayManager = {
enable = true; autoLogin.enable = true;
desktopManager.xfce.enable = true; autoLogin.user = "workshop";
displayManager.lightdm.enable = true; };
};
services.displayManager = { services.xserver.displayManager.sessionCommands = ''
autoLogin.enable = true; ${pkgs.xfce.xfce4-terminal}/bin/xfce4-terminal --title="Workshop Terminal" \
autoLogin.user = "workshop"; --command="bash -c '
}; 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
'" &
'';
services.xserver.displayManager.sessionCommands = '' environment.systemPackages = with pkgs; [
${pkgs.xfce.xfce4-terminal}/bin/xfce4-terminal --title="Workshop Terminal" \ firefox curl git jq nano tree nixos-container
--command="bash -c '
echo "Workshop VM Ready!"; (pkgs.writeScriptBin "connect" ''
echo ""; #!/bin/bash
echo "SSH into containers:"; if [ -z "$1" ]; then
${builtins.concatStringsSep " echo "Usage: connect <container-name>"
" (map (name: echo "Available: ${builtins.concatStringsSep " " participantNames}"
let ip = "192.168.100.${toString (11 + (builtins.elemAt (builtins.genList (x: x) (builtins.length participantNames)) exit 1
(builtins.elemAt fi
(builtins.filter (i: builtins.elemAt participantNames i == name) exec nixos-container root-login "$1"
(builtins.genList (x: x) (builtins.length participantNames))) 0)))}"; '')
in "echo \" sudo connect ${name} # Container login to ${name}\""
) participantNames)} (pkgs.writeScriptBin "containers" ''
echo " (Total: ${toString numParticipants} containers)"; #!/bin/bash
echo ""; echo "Active containers:"
echo "Container management:"; nixos-container list
echo " sudo containers # List all containers"; echo ""
echo " sudo logs # Show setup logs"; echo "Container IPs:"
echo " sudo recipes # Show available recipes"; ${builtins.concatStringsSep "\n" (builtins.genList (i:
echo ""; let
echo "Abra is pre-installed in containers!"; name = builtins.elemAt participantNames i;
echo ""; ip = "192.168.100.${toString (11 + i)}";
bash in "echo \" ${name}: ${ip}\""
'" & ) (builtins.length participantNames))}
''; '')
environment.systemPackages = with pkgs; [
firefox (pkgs.writeScriptBin "logs" ''
curl #!/bin/bash
git echo "Showing logs for all containers (Ctrl+C to exit)"
jq exec journalctl -u container@* -f
nano '')
tree
nixos-container (pkgs.writeScriptBin "recipes" ''
#!/bin/bash
(pkgs.writeScriptBin "connect" '' echo "Available Co-op Cloud Recipes:"
#!/bin/bash echo ""
if [ -z "$1" ]; then echo "Content Management:"
echo "Usage: connect <container-name>" echo " wordpress ghost hedgedoc dokuwiki mediawiki"
echo "Available: ${builtins.concatStringsSep " " participantNames}" echo ""
exit 1 echo "File & Collaboration:"
fi echo " nextcloud seafile collabora onlyoffice"
exec nixos-container root-login "$1" echo ""
'') echo "Communication:"
echo " jitsi-meet matrix-synapse rocketchat mattermost"
(pkgs.writeScriptBin "containers" '' echo ""
#!/bin/bash echo "E-commerce & Business:"
echo "Active containers:" echo " prestashop invoiceninja kimai pretix"
nixos-container list echo ""
'') echo "Development & Tools:"
echo " gitea drone n8n gitlab jupyter-lab"
(pkgs.writeScriptBin "logs" '' echo ""
#!/bin/bash echo "Analytics & Monitoring:"
echo "Showing logs for all containers (Ctrl+C to exit)" echo " plausible matomo uptime-kuma grafana"
exec journalctl -u container@* -f echo ""
'') echo "Media & Social:"
echo " peertube funkwhale mastodon pixelfed jellyfin"
(pkgs.writeScriptBin "recipes" '' echo ""
#!/bin/bash echo "Usage in container:"
echo "Available Co-op Cloud Recipes" echo " abra app new <recipe> -S --domain=myapp.<container-name>.local"
echo "Content Management:" echo " abra app deploy myapp.<container-name>.local"
echo " wordpress ghost hedgedoc dokuwiki mediawiki" echo ""
echo "File & Collaboration:" echo "Browse all: https://recipes.coopcloud.tech"
echo " nextcloud seafile collabora onlyoffice" '')
echo "Communication:"
echo " jitsi-meet matrix-synapse rocketchat mattermost" (pkgs.writeScriptBin "help" ''
echo "E-commerce & Business:" #!/bin/bash
echo " prestashop invoiceninja kimai pretix" echo "CODE CRISPIES Workshop VM Commands:"
echo "Development & Tools:" echo ""
echo " gitea drone n8n gitlab jupyter-lab" echo "Container Management:"
echo "Analytics & Monitoring:" echo " connect <name> - SSH into specific container"
echo " plausible matomo uptime-kuma grafana" echo " containers - List all containers with IPs"
echo "Media & Social:" echo " logs - Show container setup logs"
echo " peertube funkwhale mastodon pixelfed jellyfin" echo ""
echo "Usage in container:" echo "Workshop Tools:"
echo " abra app new <recipe> -S --domain=myapp.<container-name>.local" echo " recipes - Show available Co-op Cloud recipes"
echo " abra app deploy myapp.<container-name>.local" echo " help - Show this help"
echo "Browse all: https://recipes.coopcloud.tech" echo ""
'') echo "Examples:"
echo " sudo connect hopper"
(pkgs.writeScriptBin "help" '' echo " ssh root@192.168.100.11"
#!/bin/bash echo ""
echo "CODE CRISPIES Workshop VM Commands:" echo "Available containers (${toString numParticipants}): ${builtins.concatStringsSep " " participantNames}"
echo "" '')
echo "Container Management:" ];
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: ${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 # Local DNS resolution for .local domains
(i: networking = {
let hostName = "workshop-vm";
name = builtins.elemAt participantNames i; firewall.enable = false;
ip = "192.168.100.${toString (11 + i)}"; nat = {
in enable = true;
{ internalInterfaces = [ "ve-+" ];
inherit name; externalInterface = "eth0";
value = { };
autoStart = true; extraHosts = builtins.concatStringsSep "\n" (builtins.genList (i:
privateNetwork = true; let
hostAddress = "192.168.100.1"; name = builtins.elemAt participantNames i;
localAddress = ip; ip = "192.168.100.${toString (11 + i)}";
in "${ip} ${name}.local"
) (builtins.length participantNames));
};
config = { # Dynamic container generation
system.stateVersion = "25.05"; containers = builtins.listToAttrs (builtins.genList
(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;
users.users.root.password = ""; config = {
users.users.workshop = { system.stateVersion = "25.05";
isNormalUser = true;
password = "";
extraGroups = [ "wheel" "docker" ];
};
services.openssh = { users.users.root.password = "root";
enable = true; users.users.workshop = {
settings = { isNormalUser = true;
PasswordAuthentication = true; password = "workshop";
PermitRootLogin = "yes"; extraGroups = [ "wheel" "docker" ];
PermitEmptyPasswords = true; };
};
};
networking = { services.openssh = {
hostName = name; enable = true;
nameservers = [ "8.8.8.8" ]; settings = {
firewall.enable = false; PasswordAuthentication = true;
}; PermitRootLogin = "yes";
};
};
security.sudo.wheelNeedsPassword = false; networking = {
security.pam.services.login.allowNullPassword = true; hostName = name;
virtualisation.docker.enable = true; nameservers = [ "8.8.8.8" "1.1.1.1" ];
firewall.enable = false;
};
environment.systemPackages = with pkgs; [ security.sudo.wheelNeedsPassword = false;
docker virtualisation.docker.enable = true;
curl
git
wget
jq
bash
];
systemd.services.workshop-setup = { environment.systemPackages = with pkgs; [
wantedBy = [ "multi-user.target" ]; docker curl git wget jq bash nano tree
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} (no password)"
echo "Abra: Available via 'abra' command"
'';
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
StandardOutput = "journal";
StandardError = "journal";
};
};
environment.sessionVariables = { systemd.services.workshop-setup = {
PATH = [ "/root/.local/bin" ]; wantedBy = [ "multi-user.target" ];
}; after = [ "network-online.target" "docker.service" ];
}; wants = [ "network-online.target" ];
}; script = ''
} echo "Setting up ${name} container..."
)
(builtins.length participantNames)); # Wait for network connectivity
}) for i in {1..15}; 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/15)"
sleep 3
done
# Initialize Docker Swarm
${pkgs.docker}/bin/docker swarm init --advertise-addr ${ip} || echo "Swarm already initialized or failed"
# Install abra
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
# 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));
})
];
};
};
} }