diff --git a/.env.example b/.env.example index b413b41..b037afd 100644 --- a/.env.example +++ b/.env.example @@ -1,3 +1,5 @@ -WORKSHOP_WIFI_SSID= -WORKSHOP_WIFI_PASSWORD= -WORKSHOP_DOMAIN= +WORKSHOP_DOMAIN=codecrispi.es +HCLOUD_TOKEN= +HETZNER_DNS_TOKEN= +DNS_ZONE_ID= +USB_DEVICE=/dev/sdb diff --git a/Makefile b/Makefile index 549bbe8..c784490 100644 --- a/Makefile +++ b/Makefile @@ -1,20 +1,100 @@ -.PHONY: help vm-run clean +-include .env +export + +.PHONY: help deploy-cloud build-usb flash-usb local-vm-run clean status destroy-cloud + +DOMAIN := $(or $(WORKSHOP_DOMAIN),codecrispi.es) +USB_DEVICE := $(or $(USB_DEVICE),/dev/sdX) help: - @echo "๐Ÿช Workshop VM with Containers" + @echo "CODE CRISPIES Workshop" @echo "" - @echo "Commands:" - @echo " make vm-run - Start VM with participant containers" - @echo " make clean - Clean build artifacts" + @echo "Cloud Infrastructure (Hetzner):" + @echo " make deploy-cloud - Deploy 15 VMs to Hetzner Cloud" + @echo " make status-cloud - Check server health" + @echo " make destroy-cloud - Destroy cloud infrastructure" @echo "" - @echo "Inside the VM:" - @echo " ssh root@192.168.100.11 # Connect to hopper container" - @echo " ssh root@192.168.100.12 # Connect to curie container" + @echo "USB Boot Drive:" + @echo " make build-usb - Build NixOS workshop ISO" + @echo " make flash-usb - Flash ISO to USB drive" + @echo "" + @echo "Local Development:" + @echo " make local-vm-run - Start local VM with containers" + @echo " make clean - Clean build artifacts" + @echo "" + @echo "Config: Domain=$(DOMAIN), USB=$(USB_DEVICE)" + @echo "Required: HCLOUD_TOKEN, SSH key at ~/.ssh/id_rsa.pub" -vm-run: - @echo "๐Ÿš€ Starting workshop VM with containers..." - @echo "VM will open with desktop. Terminal shows SSH commands." +build-usb: + @echo "Building NixOS workshop ISO for $(DOMAIN)..." + @if [ ! -f ~/.ssh/id_ed25519.pub ]; then \ + echo "SSH key not found at ~/.ssh/id_ed25519.pub"; \ + echo "Generate with: ssh-keygen -t rsa -b 4096"; \ + exit 1; \ + fi + nix build .#live-iso --show-trace + @echo "ISO built: result/iso/nixos.iso" + @echo "Size: $$(du -h result/iso/nixos.iso | cut -f1)" + +flash-usb: build-usb + @if [ "$(USB_DEVICE)" = "/dev/sdX" ]; then \ + echo "Set USB_DEVICE=/dev/sdX (find with 'lsblk')"; \ + exit 1; \ + fi + @echo "About to flash $(USB_DEVICE) - THIS WILL ERASE ALL DATA!" + @echo "Verify device: $$(lsblk $(USB_DEVICE) 2>/dev/null || echo 'DEVICE NOT FOUND')" + @read -p "Continue? [y/N]: " confirm && [ "$$confirm" = "y" ] + sudo dd if=result/iso/nixos.iso of=$(USB_DEVICE) bs=4M status=progress oflag=sync + sync + @echo "USB drive ready for workshop!" + +deploy-cloud: + @if [ -z "$(HCLOUD_TOKEN)" ]; then \ + echo "HCLOUD_TOKEN not set"; \ + echo "Get token from: https://console.hetzner.cloud/"; \ + exit 1; \ + fi + @if [ ! -f ~/.ssh/id_rsa.pub ]; then \ + echo "SSH key not found at ~/.ssh/id_rsa.pub"; \ + exit 1; \ + fi + @echo "Deploying 15 workshop servers to Hetzner Cloud..." + @echo "Domain: $(DOMAIN)" + cd terraform && terraform init + cd terraform && terraform apply -auto-approve \ + -var="hcloud_token=$(HCLOUD_TOKEN)" \ + -var="hetzner_dns_token=$(HETZNER_DNS_TOKEN)" \ + -var="dns_zone_id=$(DNS_ZONE_ID)" \ + -var="domain=$(DOMAIN)" \ + -var="ssh_public_key=$$(cat ~/.ssh/id_rsa.pub)" + @echo "Running health checks..." + @sleep 60 + $(MAKE) status-cloud + @echo "Cloud deployment complete!" + +status-cloud: + @echo "Checking server health..." + @for name in hopper curie lovelace noether hamilton franklin johnson clarke goldberg liskov wing rosen shaw karp rich; do \ + printf "%-10s " "$$name:"; \ + if timeout 10 curl -s -f https://traefik.$$name.$(DOMAIN)/ping >/dev/null 2>&1; then \ + echo "Ready"; \ + 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..."; \ + else \ + echo "Not ready"; \ + fi; \ + done + +destroy-cloud: + @echo "This will destroy ALL workshop servers!" + @read -p "Continue? [y/N]: " confirm && [ "$$confirm" = "y" ] + cd terraform && terraform destroy -auto-approve + +local-vm-run: + @echo "Starting local workshop VM..." + @echo "VM will open with desktop showing 2 participant containers" nix run --impure .#local-vm clean: - rm -rf result .direnv + rm -rf result .direnv terraform/.terraform terraform/terraform.tfstate* + @echo "Cleaned up build artifacts" diff --git a/cloud/cloud-init.yml b/cloud/cloud-init.yml deleted file mode 100644 index d72bde7..0000000 --- a/cloud/cloud-init.yml +++ /dev/null @@ -1,67 +0,0 @@ -#cloud-config -users: - - name: workshop - groups: [adm, docker] - sudo: ['ALL=(ALL) NOPASSWD:ALL'] - shell: /bin/bash - ssh_authorized_keys: - - ${ssh_public_key} - -packages: - - docker.io - - curl - - git - - jq - -runcmd: - # Setup Docker Swarm - - systemctl enable docker - - systemctl start docker - - usermod -aG docker workshop - - docker swarm init - - docker network create -d overlay proxy - - # Install abra - - curl -fsSL https://install.abra.coopcloud.tech | bash - - mv /root/.local/bin/abra /usr/local/bin/ - - chmod +x /usr/local/bin/abra - - # Pre-configure abra server for this participant - - sudo -u workshop mkdir -p /home/workshop/.abra/servers - - sudo -u workshop abra server add ${participant_name}.${domain} - - # Pre-setup Traefik - - sudo -u workshop abra app new traefik --domain=traefik.${participant_name}.${domain} --server=${participant_name}.${domain}#cloud-config -users: - - name: workshop - groups: [adm, docker] - sudo: ['ALL=(ALL) NOPASSWD:ALL'] - shell: /bin/bash - ssh_authorized_keys: - - ${ssh_public_key} - -packages: - - docker.io - - curl - - git - - jq - -runcmd: - # Setup Docker Swarm - - systemctl enable docker - - systemctl start docker - - usermod -aG docker workshop - - docker swarm init - - docker network create -d overlay proxy - - # Install abra - - curl -fsSL https://install.abra.coopcloud.tech | bash - - mv /root/.local/bin/abra /usr/local/bin/ - - chmod +x /usr/local/bin/abra - - # Pre-configure abra server for this participant - - sudo -u workshop mkdir -p /home/workshop/.abra/servers - - sudo -u workshop abra server add ${participant_name}.${domain} - - # Pre-setup Traefik - - sudo -u workshop abra app new traefik --domain=traefik.${participant_name}.${domain} --server=${participant_name}.${domain} diff --git a/cloud/main.tf b/cloud/main.tf deleted file mode 100644 index 7d90dd9..0000000 --- a/cloud/main.tf +++ /dev/null @@ -1,44 +0,0 @@ -locals { - domain = "codecrispi.es" - participants = [ - "hopper", "curie", "lovelace", "noether", "hamilton", - "franklin", "johnson", "clarke", "goldberg", "liskov", - "wing", "rosen", "shaw", "karp", "rich" - ] -} - -resource "hcloud_server" "participant" { - for_each = toset(local.participants) - - name = each.key - image = "ubuntu-22.04" - server_type = "cx21" - location = "nbg1" - - ssh_keys = [hcloud_ssh_key.workshop.id] - - user_data = templatefile("${path.module}/cloud-init.yml", { - participant_name = each.key - domain = local.domain - }) -} - -resource "hcloud_dns_record" "participant_main" { - for_each = toset(local.participants) - - zone_id = var.dns_zone_id - name = each.key - type = "A" - value = hcloud_server.participant[each.key].ipv4_address - ttl = 60 -} - -resource "hcloud_dns_record" "participant_wildcard" { - for_each = toset(local.participants) - - zone_id = var.dns_zone_id - name = "*.${each.key}" - type = "A" - value = hcloud_server.participant[each.key].ipv4_address - ttl = 60 -} diff --git a/docs/USB_BOOT_INSTRUCTIONS.md b/docs/USB_BOOT_INSTRUCTIONS.md index c4e3634..48b630d 100644 --- a/docs/USB_BOOT_INSTRUCTIONS.md +++ b/docs/USB_BOOT_INSTRUCTIONS.md @@ -1,14 +1,13 @@ -# ๐Ÿช CODE CRISPIES Workshop ## USB Boot Instructions -### ๐Ÿ“‹ Quick Reference Card +### Quick Reference Card **Your assigned server:** `__________.codecrispi.es` **Workshop WiFi:** `CODE_CRISPIES_GUEST` / Password: `workshop2024` --- -## ๐Ÿ’ป How to Boot from USB Drive +## How to Boot from USB Drive ### Step 1: Insert USB Drive - Insert the CODE CRISPIES workshop USB drive @@ -16,7 +15,7 @@ ### Step 2: Boot from USB -#### ๐Ÿ–ฅ๏ธ **Desktop PC / Most Laptops** +#### Desktop PC / Most Laptops 1. **Restart** your computer 2. **Press and HOLD** one of these keys immediately as it starts: - `F12` (most common) @@ -26,19 +25,19 @@ 3. Select **USB Drive** or **UEFI: USB Drive** from boot menu 4. Press `Enter` -#### ๐ŸŽ **Mac (Intel)** +#### Mac (Intel) 1. **Restart** your Mac 2. **Press and HOLD** `Option` (โŒฅ) key immediately 3. Select the **USB drive** (may show as "EFI Boot") 4. Press `Enter` -#### ๐ŸŽ **Mac (Apple Silicon M1/M2)** +#### Mac (Apple Silicon M1/M2) 1. **Shut down** completely 2. **Press and HOLD** the power button until you see startup options 3. Select the **USB drive** option 4. Click **Continue** -#### ๐Ÿ”ง **If Boot Menu Doesn't Appear** +#### If Boot Menu Doesn't Appear **Windows PC:** 1. Boot into Windows @@ -60,7 +59,7 @@ --- -## โœ… What You Should See +## What You Should See 1. **NixOS Boot Screen** appears 2. System loads (takes 30-60 seconds) @@ -70,7 +69,7 @@ --- -## ๐Ÿš€ Getting Started Commands +## Getting Started Commands ```bash # Connect to your assigned server @@ -85,7 +84,7 @@ help --- -## ๐Ÿ“ฑ Mobile Hotspot Instructions +## Mobile Hotspot Instructions **If WiFi isn't working:** **iPhone:** @@ -98,553 +97,24 @@ Name: Android, Password: (ask facilitator) --- -## ๐Ÿ†˜ Troubleshooting +## Troubleshooting -โŒ **"No bootable device"** +**"No bootable device"** โ†’ Try different F-key (F11, F9, ESC) โ†’ USB drive may not be properly inserted -โŒ **Mac won't boot USB** +**Mac won't boot USB** โ†’ USB drive might need to be reformatted for Mac โ†’ Ask facilitator for Mac-compatible USB -โŒ **Boots to Windows/Mac instead** +**Boots to Windows/Mac instead** โ†’ You didn't press the boot key fast enough โ†’ Restart and try again immediately -โŒ **Terminal doesn't open** +**Terminal doesn't open** โ†’ Click terminal icon in taskbar โ†’ Or press `Ctrl + Alt + T` -โŒ **Can't connect to internet** +**Can't connect to internet** โ†’ Try different WiFi network โ†’ Use mobile hotspot as backup -``` - -## flake.nix -```nix -{ - description = "CODE CRISPIES Workshop Live Environment"; - - inputs = { - nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; - nixos-generators.url = "github:nix-community/nixos-generators"; - }; - - outputs = { self, nixpkgs, nixos-generators }: - let - system = "x86_64-linux"; - participantNames = [ - "hopper" "curie" "lovelace" "noether" "hamilton" - "franklin" "johnson" "clarke" "goldberg" "liskov" - "wing" "rosen" "shaw" "karp" "rich" - ]; - in { - packages.${system}.live-iso = nixos-generators.nixosGenerate { - inherit system; - format = "iso"; - - modules = [ - ({ pkgs, ... }: { - # WiFi support - networking.wireless.enable = true; - networking.networkmanager.enable = true; - networking.wireless.networks = { - "CODE_CRISPIES_GUEST" = { - psk = "workshop2024"; - }; - }; - - # Auto-connect to workshop WiFi - systemd.services.workshop-wifi = { - wantedBy = [ "multi-user.target" ]; - after = [ "network.target" ]; - script = '' - ${pkgs.networkmanager}/bin/nmcli dev wifi connect "CODE_CRISPIES_GUEST" password "workshop2024" || true - ''; - }; - - # Auto-login workshop user - services.getty.autologinUser = "workshop"; - users.users.workshop = { - isNormalUser = true; - shell = pkgs.zsh; - }; - - # Workshop shell environment - programs.zsh = { - enable = true; - interactiveShellInit = '' - echo "๐Ÿช CODE CRISPIES Workshop Environment" - echo "๐Ÿ“ถ WiFi: CODE_CRISPIES_GUEST (auto-connecting...)" - echo "๐Ÿ“ก Available servers:" - ${builtins.concatStringsSep "\n" (map (name: - "echo \" - ${name}.codecrispi.es\"" - ) participantNames)} - echo "" - echo "๐Ÿ’ก Commands: connect | recipes | help" - - connect() { - [ -z "$1" ] && { echo "Usage: connect "; return 1; } - echo "๐Ÿ”— Connecting to $1.codecrispi.es..." - ssh -o StrictHostKeyChecking=no workshop@$1.codecrispi.es - } - - recipes() { - echo "๐Ÿช Featured Co-op Cloud Recipes:" - echo "" - echo "๐Ÿ“ Content Management:" - echo " wordpress ghost hedgedoc dokuwiki mediawiki" - echo "" - echo "โ˜๏ธ File & Collaboration:" - echo " nextcloud seafile collabora onlyoffice" - echo "" - echo "๐Ÿ’ฌ Communication:" - echo " jitsi-meet matrix-synapse rocketchat mattermost" - echo "" - echo "๐Ÿ›’ E-commerce & Business:" - echo " prestashop invoiceninja kimai pretix" - echo "" - echo "๐Ÿ”ง Development & Tools:" - echo " gitea drone n8n gitlab jupyter-lab" - echo "" - echo "๐Ÿ“Š Analytics & Monitoring:" - echo " plausible matomo uptime-kuma grafana" - echo "" - echo "๐ŸŽต Media & Social:" - echo " peertube funkwhale mastodon pixelfed jellyfin" - echo "" - echo "Deploy: abra app new -S --domain=myapp..codecrispi.es" - echo "Browse all 100+ recipes: https://recipes.coopcloud.tech" - } - - help() { - echo "๐Ÿช CODE CRISPIES Workshop Commands:" - echo "" - echo "connect - SSH to your assigned server" - echo "recipes - Show available app recipes" - echo "abra app new -S --domain=..codecrispi.es" - echo "abra app deploy " - echo "abra app ls - List your apps" - echo "" - echo "Examples:" - echo " connect hopper" - echo " abra app new wordpress -S --domain=blog.hopper.codecrispi.es" - echo " abra app deploy blog.hopper.codecrispi.es" - } - ''; - }; - - environment.systemPackages = with pkgs; [ openssh curl git networkmanager ]; - - # Auto-start terminal - services.xserver = { - enable = true; - displayManager = { - autoLogin.enable = true; - autoLogin.user = "workshop"; - sessionCommands = "${pkgs.xterm}/bin/xterm &"; - }; - }; - }) - ]; - }; - - # Dev shell for local testing - devShells.${system}.default = nixpkgs.legacyPackages.${system}.mkShell { - buildInputs = with nixpkgs.legacyPackages.${system}; [ - terraform - nixos-rebuild - docker - openssh - ]; - }; - }; -} -``` - -## local/flake.nix -```nix -{ - description = "Local Co-op Cloud Testing"; - - inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; - - outputs = { self, nixpkgs }: { - nixosConfigurations.workshop-local = nixpkgs.lib.nixosSystem { - system = "x86_64-linux"; - modules = [ - { - containers = builtins.listToAttrs (map (i: - let participant = builtins.elemAt [ - "hopper" "curie" "lovelace" "noether" "hamilton" - "franklin" "johnson" "clarke" "goldberg" "liskov" - "wing" "rosen" "shaw" "karp" "rich" - ] (i - 1); - in { - name = "participant${toString i}"; - value = { - autoStart = true; - privateNetwork = true; - hostAddress = "192.168.100.1"; - localAddress = "192.168.100.${toString (10 + i)}"; - - config = { pkgs, ... }: { - virtualisation.docker = { - enable = true; - extraOptions = "--experimental"; - }; - - environment.systemPackages = with pkgs; [ - docker git curl wget tar jq - ]; - - # Helper script for workshop commands - environment.etc."workshop-helpers.sh" = { - text = '' - #!/bin/bash - - connect() { - case "$1" in - hopper|curie|lovelace|noether|hamilton|franklin|johnson|clarke|goldberg|liskov|wing|rosen|shaw|karp|rich) - echo "๐Ÿ”— Connecting to $1.codecrispi.es..." - ssh -o StrictHostKeyChecking=no workshop@$1.codecrispi.es - ;; - *) - echo "Available servers:" - echo " hopper curie lovelace noether hamilton franklin johnson" - echo " clarke goldberg liskov wing rosen shaw karp rich" - ;; - esac - } - - recipes() { - echo "๐Ÿช Available Co-op Cloud Recipes:" - echo "" - echo "๐Ÿ“ Content Management:" - echo " wordpress - Blog/CMS platform" - echo " ghost - Publishing platform" - echo " hedgedoc - Collaborative markdown editor" - echo " dokuwiki - Simple textfile based wiki" - echo " mediawiki - The wiki software that runs Wikipedia" - echo "" - echo "โ˜๏ธ File & Collaboration:" - echo " nextcloud - File sync & collaboration" - echo " seafile - File hosting platform" - echo " collabora - Online Office suite" - echo " onlyoffice - Online office suite" - echo "" - echo "๐Ÿ’ฌ Communication:" - echo " jitsi-meet - Video conferencing" - echo " matrix-synapse - Chat server" - echo " rocketchat - Team communication" - echo " mattermost - Team collaboration platform" - echo "" - echo "๐Ÿ›’ E-commerce & Business:" - echo " prestashop - E-commerce platform" - echo " invoiceninja - Invoice & billing" - echo " kimai - Time tracking" - echo " pretix - Event ticketing" - echo "" - echo "๐Ÿ”ง Development & Tools:" - echo " gitea - Git repository hosting" - echo " drone - CI/CD platform" - echo " n8n - Workflow automation" - echo " gitlab - DevOps platform" - echo " jupyter-lab - Interactive computing" - echo "" - echo "๐Ÿ“Š Analytics & Monitoring:" - echo " plausible - Privacy-friendly analytics" - echo " matomo - Web analytics" - echo " uptime-kuma - Status monitoring" - echo " grafana - Observability platform" - echo "" - echo "๐ŸŽต Media & Social:" - echo " peertube - Video platform" - echo " funkwhale - Music platform" - echo " mastodon - Social networking" - echo " pixelfed - Photo sharing" - echo " jellyfin - Media system" - echo "" - echo "Usage: abra app new -S --domain=myapp.${participant}.local" - echo "Browse all 100+ recipes: https://recipes.coopcloud.tech" - } - - help() { - echo "๐Ÿช CODE CRISPIES Workshop Commands:" - echo "" - echo "connect - SSH to cloud server" - echo "recipes - Show available app recipes" - echo "abra app new -S --domain=.${participant}.local" - echo "abra app deploy " - echo "abra app ls - List your apps" - echo "" - echo "Examples:" - echo " connect hopper" - echo " abra app new wordpress -S --domain=blog.${participant}.local" - echo " abra app deploy blog.${participant}.local" - echo "" - echo "Server: ${participant}.local" - echo "Your apps will be available at: https://.${participant}.local" - } - - export -f connect recipes help - ''; - mode = "0755"; - }; - - systemd.services.workshop-setup = { - wantedBy = [ "multi-user.target" ]; - after = [ "docker.service" "network-online.target" ]; - wants = [ "network-online.target" ]; - script = '' - # Wait for network interface - until ip addr show | grep -q "192.168.100.${toString (10 + i)}"; do - sleep 1 - done - - # Install abra - export HOME=/root - ${pkgs.curl}/bin/curl -fsSL https://install.abra.coopcloud.tech | ${pkgs.bash}/bin/bash - - # Docker swarm setup - ${pkgs.docker}/bin/docker swarm init --advertise-addr 192.168.100.${toString (10 + i)} || true - ${pkgs.docker}/bin/docker network create -d overlay proxy || true - - # Abra server setup - mkdir -p /root/.abra/servers - /root/.local/bin/abra server add ${participant}.local - - # Setup helper commands in bash profile - echo "source /etc/workshop-helpers.sh" >> /root/.bashrc - ''; - serviceConfig = { - Type = "oneshot"; - RemainAfterExit = true; - }; - }; - - services.openssh.enable = true; - networking = { - firewall.allowedTCPPorts = [ 22 80 443 ]; - hostName = "${participant}.local"; - }; - }; - }; - } - ) (nixpkgs.lib.range 1 15)); - - # Wildcard DNS for all participant subdomains - services.dnsmasq = { - enable = true; - settings.address = builtins.concatMap (i: - let participant = builtins.elemAt [ - "hopper" "curie" "lovelace" "noether" "hamilton" - "franklin" "johnson" "clarke" "goldberg" "liskov" - "wing" "rosen" "shaw" "karp" "rich" - ] (i - 1); - in [ - "/${participant}.local/192.168.100.${toString (10 + i)}" - "/.${participant}.local/192.168.100.${toString (10 + i)}" - ] - ) (nixpkgs.lib.range 1 15); - }; - } - ]; - }; - }; -} -``` - -## Makefile -```make -# Load .env file if it exists --include .env -export - -.PHONY: help deploy-cloud build-usb flash-usb local-shell local-deploy local-ssh clean status - -DOMAIN := $(or $(WORKSHOP_DOMAIN),codecrispi.es) -USB_DEVICE := /dev/sdX - -help: - @echo "๐Ÿช CODE CRISPIES Workshop" - @echo "" - @echo "Config: WiFi=$(or $(WORKSHOP_WIFI_SSID),CODE_CRISPIES_GUEST), Domain=$(DOMAIN)" - @echo "" - @echo "Cloud Infrastructure:" - @echo " make deploy-cloud - Deploy VMs to Hetzner (with health checks)" - @echo " make status-cloud - Check cloud server status" - @echo " make destroy-cloud - Destroy cloud infrastructure" - @echo "" - @echo "USB Boot Drive:" - @echo " make build-usb - Build NixOS ISO" - @echo " make flash-usb - Flash ISO to USB (set USB_DEVICE=/dev/sdX)" - @echo "" - @echo "Local Development:" - @echo " make local-shell - Enter dev shell" - @echo " make local-deploy - Deploy local NixOS containers" - @echo " make local-ssh - SSH into local participant container" - @echo " make local-clean - Stop local containers" - -build-usb: - @echo "๐Ÿ”จ Building NixOS workshop ISO..." - @echo "๐Ÿ“ Config: WiFi=$(or $(WORKSHOP_WIFI_SSID),CODE_CRISPIES_GUEST), Domain=$(DOMAIN)" - nix build .#live-iso - @echo "โœ… ISO built: result/iso/nixos.iso" - -flash-usb: build-usb - @echo "โš ๏ธ Flashing to ${USB_DEVICE} - THIS WILL ERASE THE DEVICE!" - @read -p "Continue? [y/N]: " confirm && [ "$$confirm" = "y" ] - sudo dd if=result/iso/nixos.iso of=${USB_DEVICE} bs=4M status=progress oflag=sync - @echo "โœ… USB drive ready!" - -deploy-cloud: - @echo "๐Ÿš€ Deploying to Hetzner Cloud..." - cd terraform && terraform init - cd terraform && terraform apply -auto-approve \ - -var="hcloud_token=${HCLOUD_TOKEN}" \ - -var="domain=${DOMAIN}" \ - -var="ssh_public_key=$$(cat ~/.ssh/id_rsa.pub)" - @echo "๐Ÿ” Running health checks..." - ./scripts/deploy.sh - @echo "โœ… Cloud deployment complete and verified!" - -status-cloud: - @echo "๐Ÿ“Š Checking server status..." - @for name in hopper curie lovelace noether hamilton franklin johnson clarke goldberg liskov wing rosen shaw karp rich; do \ - echo -n "$$name.${DOMAIN}: "; \ - if curl -s -f https://traefik.$$name.${DOMAIN}/ping >/dev/null 2>&1; then \ - echo "โœ… Ready"; \ - else \ - echo "โŒ Not ready"; \ - fi; \ - done - -destroy-cloud: - cd terraform && terraform destroy -auto-approve - -local-shell: - nix develop - -local-deploy: - @echo "๐Ÿ  Deploying local workshop environment..." - sudo nixos-rebuild switch --flake ./local#workshop-local - @echo "โœ… Local containers running!" - @echo "Available: participant1.local through participant15.local" - -local-ssh: - @echo "Available local participants:" - @echo " participant1.local (192.168.100.11)" - @echo " participant2.local (192.168.100.12)" - @echo " ... through participant15.local" - @read -p "Connect to participant number [1-15]: " num; \ - ssh root@192.168.100.$$((10 + $$num)) - -local-clean: - sudo nixos-container stop participant{1..15} || true - sudo nixos-container destroy participant{1..15} || true - -clean: - rm -rf result .direnv -``` - -## README.md -```markdown -# ๐Ÿช CODE CRISPIES Workshop Infrastructure - -Three deployment environments for Co-op Cloud workshop: - -## ๐Ÿš€ Quick Start - -```bash -# 1. Build & flash USB drives -make build-usb -make flash-usb USB_DEVICE=/dev/sdX - -# 2. Deploy cloud infrastructure -export HCLOUD_TOKEN=your_token -make deploy-cloud - -# 3. Local development -make local-shell -make local-deploy -make local-ssh -``` - -## ๐Ÿ“ Project Structure - -``` -โ”œโ”€โ”€ flake.nix # USB boot environment -โ”œโ”€โ”€ local/flake.nix # Local NixOS containers -โ”œโ”€โ”€ terraform/ # Hetzner Cloud infrastructure -โ”œโ”€โ”€ scripts/deploy.sh # Cloud setup automation -โ”œโ”€โ”€ docs/USB_BOOT_INSTRUCTIONS.md -โ””โ”€โ”€ Makefile # Build & deploy commands -``` - -## ๐ŸŒ Three Environments - -### 1. Cloud (Production) -- Hetzner VMs: `hopper.codecrispi.es`, `curie.codecrispi.es`, etc. -- Pre-configured with Docker Swarm + abra -- SSL certificates via Let's Encrypt - -### 2. USB Boot (Workshop) -- NixOS live environment -- Auto-connects to workshop WiFi -- Helper functions: `connect hopper`, `recipes`, `help` -- SSH into cloud VMs - -### 3. Local (Development) -- NixOS containers: `participant1.local` through `participant15.local` -- Test abra deployments locally -- Isolated Docker Swarm per container - -## ๐Ÿ”ง Development Workflow - -```bash -# Enter development environment -make local-shell - -# Deploy local testing environment -make local-deploy - -# SSH into local participant container -make local-ssh # Select participant 1-15 - -# Test app deployment inside container -abra app new wordpress -S --domain=test.participant1.local -abra app deploy test.participant1.local -``` - -## ๐Ÿ“ฆ Workshop Flow - -1. **Participant boots USB** โ†’ NixOS live environment -2. **Connects to WiFi** โ†’ `CODE_CRISPIES_GUEST` -3. **SSH to cloud VM** โ†’ `connect hopper` -4. **Deploy apps** โ†’ `abra app new wordpress -S --domain=mysite.hopper.codecrispi.es` -5. **Access via browser** โ†’ `https://mysite.hopper.codecrispi.es` - -## ๐ŸŽฏ Available Apps - -### Featured Recipes -- **WordPress** - Blog/CMS platform -- **Nextcloud** - File sync & collaboration -- **HedgeDoc** - Collaborative markdown editor -- **Jitsi** - Video conferencing -- **PrestaShop** - E-commerce platform -- **Gitea** - Git repository hosting -- **Matrix Synapse** - Chat server -- **Plausible** - Privacy-friendly analytics -- **PeerTube** - Video platform -- **Mastodon** - Social networking - -Browse all 100+ recipes at: https://recipes.coopcloud.tech - -## ๐Ÿงน Cleanup - -```bash -make clean # Clean local artifacts -make destroy-cloud # Destroy Hetzner infrastructure -``` diff --git a/flake.lock b/flake.lock index 0594294..efbf08f 100644 --- a/flake.lock +++ b/flake.lock @@ -1,5 +1,41 @@ { "nodes": { + "nixlib": { + "locked": { + "lastModified": 1736643958, + "narHash": "sha256-tmpqTSWVRJVhpvfSN9KXBvKEXplrwKnSZNAoNPf/S/s=", + "owner": "nix-community", + "repo": "nixpkgs.lib", + "rev": "1418bc28a52126761c02dd3d89b2d8ca0f521181", + "type": "github" + }, + "original": { + "owner": "nix-community", + "repo": "nixpkgs.lib", + "type": "github" + } + }, + "nixos-generators": { + "inputs": { + "nixlib": "nixlib", + "nixpkgs": [ + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1751903740, + "narHash": "sha256-PeSkNMvkpEvts+9DjFiop1iT2JuBpyknmBUs0Un0a4I=", + "owner": "nix-community", + "repo": "nixos-generators", + "rev": "032decf9db65efed428afd2fa39d80f7089085eb", + "type": "github" + }, + "original": { + "owner": "nix-community", + "repo": "nixos-generators", + "type": "github" + } + }, "nixpkgs": { "locked": { "lastModified": 1754937576, @@ -18,6 +54,7 @@ }, "root": { "inputs": { + "nixos-generators": "nixos-generators", "nixpkgs": "nixpkgs" } } diff --git a/flake.nix b/flake.nix index a9c4fed..3a4d21b 100644 --- a/flake.nix +++ b/flake.nix @@ -1,19 +1,138 @@ { - description = "Workshop VM with Participant Containers"; + 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"; + }; }; - outputs = { self, nixpkgs }: + outputs = { self, nixpkgs, nixos-generators }: let system = "x86_64-linux"; pkgs = nixpkgs.legacyPackages.${system}; participantNames = [ "hopper" "curie" ]; + fullParticipantNames = [ + "hopper" "curie" "lovelace" "noether" "hamilton" + "franklin" "johnson" "clarke" "goldberg" "liskov" + "wing" "rosen" "shaw" "karp" "rich" + ]; in { packages.${system} = { local-vm = self.nixosConfigurations.workshop-vm.config.system.build.vm; + + live-iso = nixos-generators.nixosGenerate { + inherit system; + format = "iso"; + + modules = [ + ({ pkgs, ... }: { + system.stateVersion = "25.05"; + + isoImage.makeEfiBootable = true; + isoImage.makeUsbBootable = true; + + networking.wireless.enable = true; + networking.networkmanager.enable = true; + networking.hostName = "workshop-live"; + + services.getty.autologinUser = "workshop"; + 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 + ]; + + programs.zsh = { + enable = true; + interactiveShellInit = '' + echo "CODE CRISPIES Workshop Environment" + echo "Available servers:" + ${builtins.concatStringsSep "\n" (map (name: + "echo \" - ${name}.codecrispi.es\"" + ) fullParticipantNames)} + echo "" + echo "Commands: connect | recipes | help" + + connect() { + [ -z "$1" ] && { echo "Usage: connect "; return 1; } + echo "Connecting to $1.codecrispi.es..." + ssh -o StrictHostKeyChecking=no workshop@$1.codecrispi.es + } + + recipes() { + echo "Available Co-op Cloud Recipes:" + echo "" + echo "Content Management:" + echo " wordpress ghost hedgedoc dokuwiki mediawiki" + echo "" + echo "File & Collaboration:" + echo " nextcloud seafile collabora onlyoffice" + echo "" + echo "Communication:" + echo " jitsi-meet matrix-synapse rocketchat mattermost" + echo "" + echo "E-commerce & Business:" + echo " prestashop invoiceninja kimai pretix" + echo "" + echo "Development & Tools:" + echo " gitea drone n8n gitlab jupyter-lab" + echo "" + echo "Analytics & Monitoring:" + echo " plausible matomo uptime-kuma grafana" + echo "" + echo "Media & Social:" + echo " peertube funkwhale mastodon pixelfed jellyfin" + echo "" + echo "Deploy: abra app new -S --domain=myapp..codecrispi.es" + echo "Browse all: https://recipes.coopcloud.tech" + } + + help() { + echo "CODE CRISPIES Workshop Commands:" + echo "" + echo "connect - SSH to your assigned server" + echo "recipes - Show available app recipes" + echo "sudo nmcli dev wifi connect SSID password PASSWORD" + echo "" + echo "Examples:" + echo " connect hopper" + echo " sudo nmcli dev wifi connect CODE_CRISPIES_GUEST password workshop2024" + } + + export -f connect recipes help + ''; + }; + + services.xserver = { + enable = true; + desktopManager.xfce.enable = true; + displayManager = { + lightdm.enable = true; + autoLogin.enable = true; + autoLogin.user = "workshop"; + }; + }; + + systemd.user.services.workshop-welcome = { + wantedBy = [ "graphical-session.target" ]; + after = [ "graphical-session.target" ]; + script = "${pkgs.xterm}/bin/xterm -title 'CODE CRISPIES Workshop' -e 'zsh' &"; + serviceConfig.Type = "forking"; + }; + }) + ]; + }; }; nixosConfigurations.workshop-vm = nixpkgs.lib.nixosSystem { @@ -36,7 +155,6 @@ security.pam.services.login.allowNullPassword = true; security.sudo.wheelNeedsPassword = false; - # GUI setup services.xserver = { enable = true; desktopManager.xfce.enable = true; @@ -48,32 +166,29 @@ autoLogin.user = "workshop"; }; - # Auto-open terminal with helper commands 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 ' - echo \"๐Ÿช Workshop VM Ready!\"; + echo \"Workshop VM Ready!\"; echo \"\"; - echo \"๐Ÿ”Œ SSH into containers:\"; + echo \"SSH into containers:\"; echo \" sudo connect hopper # Container login\"; echo \" sudo connect curie # Container login\"; echo \" ssh root@192.168.100.11 # Direct SSH to hopper\"; echo \" ssh root@192.168.100.12 # Direct SSH to curie\"; echo \"\"; - echo \"๐Ÿ“ฆ Container management:\"; + echo \"Container management:\"; echo \" sudo containers # List all containers\"; echo \" sudo logs # Show setup logs\"; echo \"\"; - echo \"โœจ Abra is pre-installed in containers!\"; + echo \"Abra is pre-installed in containers!\"; echo \"\"; bash '" & ''; - # System packages including helper scripts environment.systemPackages = with pkgs; [ firefox curl git jq nano tree nixos-container - # Custom helper scripts that work with sudo (pkgs.writeScriptBin "connect" '' #!/bin/bash if [ -z "$1" ]; then @@ -103,7 +218,6 @@ }; }; - # Container configurations with automated abra installation containers = builtins.listToAttrs (builtins.genList (i: let name = builtins.elemAt participantNames i; @@ -147,52 +261,45 @@ docker curl git wget jq bash ]; - # Automated abra installation service systemd.services.workshop-setup = { wantedBy = [ "multi-user.target" ]; after = [ "network-online.target" "docker.service" ]; wants = [ "network-online.target" ]; script = '' - echo "๐Ÿช Setting up ${name} container..." + echo "Setting up ${name} container..." - # Wait for network 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" + echo "Network ready" break fi - echo "โณ Waiting for network... ($i/10)" + echo "Waiting for network... ($i/10)" sleep 2 done - # Initialize Docker Swarm ${pkgs.docker}/bin/docker swarm init --advertise-addr ${ip} || true - # Install abra for root user export HOME=/root if [ ! -f /root/.local/bin/abra ]; then - echo "๐Ÿ“ฆ Installing abra..." + echo "Installing abra..." ${pkgs.curl}/bin/curl -fsSL https://install.abra.coopcloud.tech | ${pkgs.bash}/bin/bash - echo "โœ… Abra installed" + echo "Abra installed" fi - # Make abra available globally if ! grep -q "/.local/bin" /root/.bashrc 2>/dev/null; then echo 'export PATH="$HOME/.local/bin:$PATH"' >> /root/.bashrc fi - # Create symlink for immediate availability if [ -f /root/.local/bin/abra ]; then ln -sf /root/.local/bin/abra /usr/local/bin/abra 2>/dev/null || true fi - # Add server 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 "${name} container ready!" echo "SSH: ssh root@${ip} (password: root)" echo "Abra: Available via 'abra' command" ''; @@ -204,7 +311,6 @@ }; }; - # Ensure abra is in PATH for all sessions environment.sessionVariables = { PATH = [ "/root/.local/bin" ]; }; diff --git a/scripts/deploy.sh b/scripts/deploy.sh index fa8278c..2d0aade 100644 --- a/scripts/deploy.sh +++ b/scripts/deploy.sh @@ -1,52 +1,49 @@ #!/usr/bin/env bash set -e -echo "๐Ÿš€ Deploying Co-op Cloud workshop..." +echo "Deploying Co-op Cloud workshop..." cd terraform terraform apply \ -var="hcloud_token=$HCLOUD_TOKEN" \ + -var="hetzner_dns_token=$HETZNER_DNS_TOKEN" \ + -var="dns_zone_id=$DNS_ZONE_ID" \ -var="domain=codecrispi.es" \ -var="ssh_public_key=$(cat ~/.ssh/id_rsa.pub)" \ -auto-approve -echo "โณ Waiting for servers to be ready..." +echo "Waiting for servers to be ready..." -# Wait for SSH + Docker to be ready on each server terraform output -json participant_ips | jq -r 'keys[]' | while read participant; do echo "Checking $participant.codecrispi.es..." - # Wait for SSH to be available while ! ssh -o ConnectTimeout=5 -o StrictHostKeyChecking=no workshop@$participant.codecrispi.es "exit" 2>/dev/null; do echo " SSH not ready yet, retrying in 10s..." sleep 10 done - # Wait for Docker + abra to be ready while ! ssh -o StrictHostKeyChecking=no workshop@$participant.codecrispi.es "docker info && which abra" &>/dev/null; do echo " Docker/abra not ready yet, retrying in 5s..." sleep 5 done - echo " โœ… $participant ready!" + echo " $participant ready!" done -echo "๐Ÿ”ง Setting up each server..." +echo "Setting up each server..." terraform output -json participant_ips | jq -r 'keys[]' | while read participant; do echo "Configuring $participant..." - ssh -o StrictHostKeyChecking=no workshop@$participant.codecrispi.es << EOF - # Deploy Traefik + ssh -o StrictHostKeyCheckking=no workshop@$participant.codecrispi.es << EOF abra app deploy traefik.$participant.codecrispi.es - # Wait for Traefik to be ready until curl -f https://traefik.$participant.codecrispi.es/ping 2>/dev/null; do echo "Waiting for Traefik..." sleep 5 done - echo "โœ… $participant fully configured!" + echo "$participant fully configured!" EOF done -echo "๐ŸŽ‰ Workshop ready! Participants can access their servers." +echo "Workshop ready! Participants can access their servers." diff --git a/terraform/cloud-init.yml b/terraform/cloud-init.yml new file mode 100644 index 0000000..b635008 --- /dev/null +++ b/terraform/cloud-init.yml @@ -0,0 +1,88 @@ +#cloud-config +users: + - name: workshop + groups: [adm, docker] + sudo: ['ALL=(ALL) NOPASSWD:ALL'] + shell: /bin/bash + ssh_authorized_keys: + - ${ssh_public_key} + +packages: + - docker.io + - curl + - git + - jq + +runcmd: + - systemctl enable docker + - systemctl start docker + - usermod -aG docker workshop + - docker swarm init + - docker network create -d overlay proxy + + - curl -fsSL https://install.abra.coopcloud.tech | sudo -u workshop bash + - ln -sf /home/workshop/.local/bin/abra /usr/local/bin/abra + + - sudo -u workshop mkdir -p /home/workshop/.abra/servers + - sudo -u workshop abra server add ${participant_name}.${domain} + + - sudo -u workshop abra app new traefik --domain=traefik.${participant_name}.${domain} --server=${participant_name}.${domain} + - sudo -u workshop abra app deploy traefik.${participant_name}.${domain} + + - | + cat > /home/workshop/.bashrc << 'EOF' + export PATH="$HOME/.local/bin:$PATH" + + 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 n8n jupyter-lab" + echo "" + echo "Analytics & Monitoring:" + echo " plausible uptime-kuma grafana" + echo "" + echo "Media & Social:" + echo " peertube mastodon jellyfin" + echo "" + echo "Deploy: abra app new -S --domain=myapp.${participant_name}.${domain}" + echo "Browse all: https://recipes.coopcloud.tech" + } + + help() { + echo "CODE CRISPIES Workshop Commands:" + echo "" + echo "recipes - Show available app recipes" + echo "abra app new -S --domain=.${participant_name}.${domain}" + echo "abra app deploy " + echo "abra app ls - List your apps" + echo "" + echo "Examples:" + echo " abra app new wordpress -S --domain=blog.${participant_name}.${domain}" + echo " abra app deploy blog.${participant_name}.${domain}" + echo "" + echo "Server: ${participant_name}.${domain}" + echo "Traefik: https://traefik.${participant_name}.${domain}" + } + + export -f recipes help + + echo "CODE CRISPIES Workshop!" + echo "Server: ${participant_name}.${domain}" + echo "Type 'help' for commands or 'recipes' for available apps" + EOF + +final_message: + - echo "Workshop server ready! Traefik available at https://traefik.${participant_name}.${domain}" diff --git a/terraform/main.tf b/terraform/main.tf new file mode 100644 index 0000000..56ef768 --- /dev/null +++ b/terraform/main.tf @@ -0,0 +1,90 @@ +terraform { + required_providers { + hcloud = { + source = "hetznercloud/hcloud" + version = "~> 1.48" + } + hetznerdns = { + source = "timohirt/hetznerdns" + version = "~> 2.2" + } + } +} + +locals { + domain = var.domain + participants = [ + "hopper", "curie", "lovelace", "noether", "hamilton", + "franklin", "johnson", "clarke", "goldberg", "liskov", + "wing", "rosen", "shaw", "karp", "rich" + ] +} + +provider "hcloud" { + token = var.hcloud_token +} + +provider "hetznerdns" { + apitoken = var.hetzner_dns_token +} + +resource "hcloud_ssh_key" "workshop" { + name = "code-crispies-workshop" + public_key = var.ssh_public_key +} + +resource "hcloud_server" "participant" { + for_each = toset(local.participants) + + name = each.key + image = "ubuntu-22.04" + server_type = "cx21" + location = "nbg1" + + ssh_keys = [hcloud_ssh_key.workshop.id] + + user_data = templatefile("${path.module}/cloud-init.yml", { + participant_name = each.key + domain = local.domain + ssh_public_key = var.ssh_public_key + }) + + labels = { + project = "code-crispies-workshop" + participant = each.key + } +} + +resource "hetznerdns_record" "participant_main" { + for_each = toset(local.participants) + + zone_id = var.dns_zone_id + name = each.key + type = "A" + value = hcloud_server.participant[each.key].ipv4_address + ttl = 60 +} + +resource "hetznerdns_record" "participant_wildcard" { + for_each = toset(local.participants) + + zone_id = var.dns_zone_id + name = "*.${each.key}" + type = "A" + value = hcloud_server.participant[each.key].ipv4_address + ttl = 60 +} + +output "participant_ips" { + value = { + for name, server in hcloud_server.participant : name => server.ipv4_address + } + description = "IP addresses of participant servers" +} + +output "participant_urls" { + value = { + for name, server in hcloud_server.participant : name => "https://traefik.${name}.${local.domain}" + } + description = "Traefik URLs for each participant" +} diff --git a/terraform/variables.tf b/terraform/variables.tf new file mode 100644 index 0000000..6aca70c --- /dev/null +++ b/terraform/variables.tf @@ -0,0 +1,29 @@ +variable "hcloud_token" { + description = "Hetzner Cloud API token" + type = string + sensitive = true +} + +variable "hetzner_dns_token" { + description = "Hetzner DNS API token" + type = string + sensitive = true + default = "" +} + +variable "dns_zone_id" { + description = "Hetzner DNS Zone ID" + type = string + default = "" +} + +variable "domain" { + description = "Workshop domain" + type = string + default = "codecrispi.es" +} + +variable "ssh_public_key" { + description = "SSH public key for server access" + type = string +}