stable vm run and container spin up, not reachable via ssh

This commit is contained in:
2025-08-14 18:45:18 +02:00
parent adaa7b29d2
commit c49eb614d5
4 changed files with 379 additions and 191 deletions

View File

@@ -5,20 +5,51 @@ This file provides guidelines for AI coding agents operating within this reposit
## Build, Lint, and Test Commands ## Build, Lint, and Test Commands
- **Build**: `make build-usb` (Builds the NixOS workshop ISO) - **Build**: `make build-usb` (Builds the NixOS workshop ISO)
- **Lint**: No specific linting command found. Follow general code style guidelines. - **Local VM**: `make local-vm-run` (Starts local development environment with 15 containers)
- **Test**: No specific testing command found. Use `make status-cloud` for health checks. - **Lint**: `make lint` (Runs markdownlint, JSON validation, and nixpkgs-fmt)
- **Single Test**: No specific command for running a single test. - **Test**: `make status-cloud` (Health checks for cloud infrastructure)
- **Deploy**: `make deploy-cloud` (Deploys 15 VMs to Hetzner Cloud)
## Code Style Guidelines ## Code Style Guidelines
- **Imports**: Organize imports alphabetically. Avoid unused imports. - **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. - **Types**: Use Nix's type system rigorously. Define types explicitly where possible.
- **Naming Conventions**: - **Naming Conventions**:
- Variables and functions: `camelCase` or `snake_case` (be consistent). - Variables and functions: `camelCase` for Nix expressions
- Package names: `lowercase-with-hyphens`. - 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. - **Error Handling**: Handle errors explicitly. Use Nix's error reporting mechanisms.
- **General**:
- Keep code concise and readable. ## Container Architecture
- Prefer declarative over imperative approaches.
- Document complex logic. - **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 <name>` - 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

View File

@@ -4,6 +4,7 @@ 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)
USB_DEVICE := $(or $(USB_DEVICE),/dev/sdX) USB_DEVICE := $(or $(USB_DEVICE),/dev/sdX)
help: help:
@@ -19,17 +20,21 @@ help:
@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 containers" @echo " make local-vm-run - Start local VM with 15 containers"
@echo " make clean - Clean build artifacts" @echo " make clean - Clean build artifacts"
@echo "" @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 "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: build-usb:
@echo "Building NixOS workshop ISO for $(DOMAIN)..." @echo "Building NixOS workshop ISO for $(DOMAIN)..."
@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 rsa -b 4096"; \ 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
@@ -54,8 +59,9 @@ deploy-cloud:
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_rsa.pub ]; then \ @if [ ! -f ~/.ssh/id_ed25519.pub ]; then \
echo "SSH key not found at ~/.ssh/id_rsa.pub"; \ echo "SSH key not found at ~/.ssh/id_ed25519.pub"; \
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..."
@@ -66,7 +72,7 @@ deploy-cloud:
-var="hetzner_dns_token=$(HETZNER_DNS_TOKEN)" \ -var="hetzner_dns_token=$(HETZNER_DNS_TOKEN)" \
-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_rsa.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
@@ -91,8 +97,8 @@ destroy-cloud:
cd terraform && terraform destroy -auto-approve cd terraform && terraform destroy -auto-approve
local-vm-run: local-vm-run:
@echo "Starting local workshop VM..." @echo "Starting local workshop VM with $(PARTICIPANTS) containers..."
@echo "VM will open with desktop showing 2 participant containers" @echo "VM will open with desktop showing all participant containers"
nix run --impure .#local-vm nix run --impure .#local-vm
clean: clean:

118
README.md
View File

@@ -6,7 +6,7 @@ This repository contains the infrastructure for the Co-op Cloud workshop, provid
## 🚀 Quick Start ## 🚀 Quick Start
```bash ```bash
# 1. Start the local development virtual machine # 1. Start the local development virtual machine (15 containers)
make local-vm-run make local-vm-run
# 2. Build & flash USB drives for participants # 2. Build & flash USB drives for participants
@@ -16,9 +16,9 @@ make flash-usb USB_DEVICE=/dev/sdX
# 3. Deploy the production cloud infrastructure # 3. Deploy the production cloud infrastructure
export HCLOUD_TOKEN="your_token_here" export HCLOUD_TOKEN="your_token_here"
make deploy-cloud make deploy-cloud
```` ```
----- ---
## 📁 Project Structure ## 📁 Project Structure
@@ -30,26 +30,29 @@ make deploy-cloud
└── Makefile # Build & deploy commands └── Makefile # Build & deploy commands
``` ```
----- ---
## 🌍 Three Environments ## 🌍 Three Environments
### 1\. Cloud (Production) ### 1. Cloud (Production)
- [cite\_start]**What:** Hetzner VMs named `hopper.codecrispi.es`, `curie.codecrispi.es`, etc. [cite: 52] - **What:** 15 Hetzner VMs named `hopper.codecrispi.es`, `curie.codecrispi.es`, etc.
- **Purpose:** The live environment for workshop participants. - **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] - **What:** A bootable NixOS live environment with SSH client tools.
- **Purpose:** Used by participants to connect to their cloud servers. [cite\_start]It includes helper functions like `connect hopper`. [cite: 12] - **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. - **What:** A self-contained Virtual Machine (VM) that runs on your local computer with all 15 containers.
- **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. - **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 ## 🔧 Local Development Workflow
@@ -68,12 +71,15 @@ make deploy-cloud
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: * **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.local) # Connect to participant 1 (hopper)
connect hopper
# Or direct SSH
ssh root@192.168.100.11 ssh root@192.168.100.11
``` ```
* **Inside the container**, deploy a WordPress site with `abra`: * **Inside the container**, deploy a WordPress site with `abra`:
@@ -81,9 +87,62 @@ make deploy-cloud
abra app new wordpress -S --domain=blog.hopper.local abra app new wordpress -S --domain=blog.hopper.local
abra app deploy 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. * **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 <name>` command to SSH into assigned servers
- `recipes` command showing available Co-op Cloud applications
- Workshop-specific networking and WiFi helpers
---
## 🧹 Cleanup ## 🧹 Cleanup
@@ -94,5 +153,28 @@ 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. # 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.

119
flake.nix
View File

@@ -13,8 +13,7 @@
let let
system = "x86_64-linux"; system = "x86_64-linux";
pkgs = nixpkgs.legacyPackages.${system}; pkgs = nixpkgs.legacyPackages.${system};
participantNames = [ "hopper" "curie" ]; allParticipantNames = [
fullParticipantNames = [
"hopper" "hopper"
"curie" "curie"
"lovelace" "lovelace"
@@ -31,6 +30,15 @@
"karp" "karp"
"rich" "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 in
{ {
packages.${system} = { packages.${system} = {
@@ -77,7 +85,7 @@
echo "Available servers:" echo "Available servers:"
${builtins.concatStringsSep "\n" (map (name: ${builtins.concatStringsSep "\n" (map (name:
"echo \" - ${name}.codecrispi.es\"" "echo \" - ${name}.codecrispi.es\""
) fullParticipantNames)} ) allParticipantNames)}
echo "" echo ""
echo "Commands: connect <name> | recipes | help" echo "Commands: connect <name> | recipes | help"
@@ -194,24 +202,29 @@
services.xserver.displayManager.sessionCommands = '' services.xserver.displayManager.sessionCommands = ''
${pkgs.xfce.xfce4-terminal}/bin/xfce4-terminal --title="Workshop Terminal" \ ${pkgs.xfce.xfce4-terminal}/bin/xfce4-terminal --title="Workshop Terminal" \
--command="bash -c ' --command="bash -c '
echo \"Workshop VM Ready!\"; echo "Workshop VM Ready!";
echo \"\"; echo "";
echo \"SSH into containers:\"; echo "SSH into containers:";
echo \" sudo connect hopper # Container login\"; ${builtins.concatStringsSep "
echo \" sudo connect curie # Container login\"; " (map (name:
echo \" ssh root@192.168.100.11 # Direct SSH to hopper\"; let ip = "192.168.100.${toString (11 + (builtins.elemAt (builtins.genList (x: x) (builtins.length participantNames))
echo \" ssh root@192.168.100.12 # Direct SSH to curie\"; (builtins.elemAt
echo \"\"; (builtins.filter (i: builtins.elemAt participantNames i == name)
echo \"Container management:\"; (builtins.genList (x: x) (builtins.length participantNames))) 0)))}";
echo \" sudo containers # List all containers\"; in "echo \" sudo connect ${name} # Container login to ${name}\""
echo \" sudo logs # Show setup logs\"; ) participantNames)}
echo \"\"; echo " (Total: ${toString numParticipants} containers)";
echo \"Abra is pre-installed in containers!\"; echo "";
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 bash
'" & '" &
''; '';
environment.systemPackages = with pkgs; [ environment.systemPackages = with pkgs; [
firefox firefox
curl curl
@@ -220,25 +233,73 @@
nano nano
tree tree
nixos-container nixos-container
(pkgs.writeScriptBin "connect" '' (pkgs.writeScriptBin "connect" ''
#!/bin/bash #!/bin/bash
if [ -z "$1" ]; then if [ -z "$1" ]; then
echo "Usage: connect <container-name>" echo "Usage: connect <container-name>"
echo "Available: hopper curie" echo "Available: ${builtins.concatStringsSep " " participantNames}"
exit 1 exit 1
fi fi
exec nixos-container root-login "$1" exec nixos-container root-login "$1"
'') '')
(pkgs.writeScriptBin "containers" '' (pkgs.writeScriptBin "containers" ''
#!/bin/bash #!/bin/bash
exec nixos-container list echo "Active containers:"
nixos-container list
'') '')
(pkgs.writeScriptBin "logs" '' (pkgs.writeScriptBin "logs" ''
#!/bin/bash #!/bin/bash
exec journalctl -u container@hopper -u container@curie -f 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 <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" ''
#!/bin/bash
echo "CODE CRISPIES Workshop VM Commands:"
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 = { networking = {
hostName = "workshop-vm"; hostName = "workshop-vm";
firewall.enable = false; firewall.enable = false;
@@ -247,6 +308,12 @@
internalInterfaces = [ "ve-+" ]; internalInterfaces = [ "ve-+" ];
externalInterface = "eth0"; 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 containers = builtins.listToAttrs (builtins.genList
@@ -266,10 +333,10 @@
config = { config = {
system.stateVersion = "25.05"; system.stateVersion = "25.05";
users.users.root.password = "root"; users.users.root.password = "";
users.users.workshop = { users.users.workshop = {
isNormalUser = true; isNormalUser = true;
password = "workshop"; password = "";
extraGroups = [ "wheel" "docker" ]; extraGroups = [ "wheel" "docker" ];
}; };
@@ -278,6 +345,7 @@
settings = { settings = {
PasswordAuthentication = true; PasswordAuthentication = true;
PermitRootLogin = "yes"; PermitRootLogin = "yes";
PermitEmptyPasswords = true;
}; };
}; };
@@ -288,6 +356,7 @@
}; };
security.sudo.wheelNeedsPassword = false; security.sudo.wheelNeedsPassword = false;
security.pam.services.login.allowNullPassword = true;
virtualisation.docker.enable = true; virtualisation.docker.enable = true;
environment.systemPackages = with pkgs; [ environment.systemPackages = with pkgs; [
@@ -338,7 +407,7 @@
fi fi
echo "${name} container ready!" echo "${name} container ready!"
echo "SSH: ssh root@${ip} (password: root)" echo "SSH: ssh root@${ip} (no password)"
echo "Abra: Available via 'abra' command" echo "Abra: Available via 'abra' command"
''; '';
serviceConfig = { serviceConfig = {