diff --git a/README.md b/README.md index dd668eb..7a0358e 100644 --- a/README.md +++ b/README.md @@ -125,8 +125,8 @@ To deconstruct the above command: - You need to pull down `main` branch and merge it into `develop` locally for your next iteration. ``` git checkout main - git pull + git pull origin main git checkout develop git merge main - git push + git push origin develop ``` diff --git a/blog/2025-09-01-sync-resolver-on-ip-change/index.md b/blog/2025-09-01-sync-resolver-on-ip-change/index.md index a0a7a6a..8b48262 100644 --- a/blog/2025-09-01-sync-resolver-on-ip-change/index.md +++ b/blog/2025-09-01-sync-resolver-on-ip-change/index.md @@ -2,7 +2,7 @@ slug: sync-resolver-macos title: Automating DNS Resolver Updates for Dynamic IP Addresses on macOS authors: [jorge] -tags: [educates, resolver, dns] +tags: [educates, resolver, dns, tips-and-tricks, macos] --- When developing with Educates on a local machine, many developers rely on the local DNS resolver functionality to provide their Educates cluster with a meaningful hostname. A common approach is to use a domain like `educates.test` for consistent local cluster access. diff --git a/blog/2026-02-20-clickable-actions-in-workshops/editor-replace-example.png b/blog/2026-02-20-clickable-actions-in-workshops/editor-replace-example.png new file mode 100644 index 0000000..3dac3d8 Binary files /dev/null and b/blog/2026-02-20-clickable-actions-in-workshops/editor-replace-example.png differ diff --git a/blog/2026-02-20-clickable-actions-in-workshops/index.md b/blog/2026-02-20-clickable-actions-in-workshops/index.md new file mode 100644 index 0000000..03aeed4 --- /dev/null +++ b/blog/2026-02-20-clickable-actions-in-workshops/index.md @@ -0,0 +1,92 @@ +--- +title: "Clickable actions in workshops" +slug: clickable-actions-in-workshops +description: "From copy and paste to fully guided workshop experiences with clickable actions." +authors: [graham] +tags: ["educates", "authoring"] +--- + +The idea of guided instruction in tutorials isn't new. Most online tutorials these days provide a click-to-copy icon next to commands and code snippets. It's a useful convenience. You see the command you need to run, you click the icon, and it lands in your clipboard ready to paste. Better than selecting text by hand and hoping you got the right boundaries. + +But this convenience only goes so far. The instructions still assume you have a suitable environment set up on your own machine. The commands might reference tools you haven't installed, paths that don't exist in your setup, or configuration that differs from what the tutorial expects. The copy button solves the mechanics of getting text into your clipboard, but the real friction is in the gap between the tutorial and your environment. You end up spending more time troubleshooting your local setup than actually learning the thing the tutorial was supposed to teach you. + + + +## Hosted environments and the copy/paste problem + +Online training platforms like [Instruqt](https://instruqt.com/) and [Strigo](https://strigo.io/) improved on this by providing VM-based environments that are pre-configured and ready to go. You don't need to install anything locally. The environment matches what the instructions expect, so commands and paths should work as written. That eliminates the entire class of problems around "works on the tutorial author's machine but not on mine." + +The interaction model, though, is still copy and paste. You read instructions in one panel, find the command you need, copy it, switch to the terminal panel, paste it, and run it. For code changes, you copy a snippet from the instructions and paste it into a file in the editor. It works, but it's a manual process that requires constant context switching between panels. Every copy and paste is a small interruption, and over the course of a full workshop those interruptions add up. Learners end up spending mental energy on the mechanics of following instructions rather than on the material itself. + +## When commands became clickable + +[Katacoda](https://www.katacoda.com/), before it was shut down by O'Reilly in 2022, included an improvement to this model. Commands embedded in the workshop instructions were clickable. Click on a command and it would automatically execute in the terminal session provided alongside the instructions. No copying, no pasting, no switching between panels. The learner reads the explanation, clicks the command, and watches the result appear in the terminal. The flow from reading to doing became much more seamless. + +This was a meaningful step forward for terminal interactions specifically. But it only covered one part of the workflow. For code changes, editing configuration files, or any interaction that involved working with files in an editor, you were still back to the copy and paste model. The guided experience had a gap. Commands were frictionless, but everything else still required manual effort. + +## Educates and the fully guided experience + +[Educates](https://github.com/educates/educates-training-platform/) takes the idea of clickable actions and extends it across the entire workshop interaction. The workshop dashboard provides instructions alongside live terminals and an embedded VS Code editor. Throughout the instructions, learners encounter clickable actions that cover not just running commands, but the full range of things you'd normally do in a hands-on technical workshop. + +Terminal actions work the way Katacoda relied on. Click on a command in the instructions and it runs in the terminal. But Educates goes further by providing a full set of editor actions as well. Clickable actions can open a file in the embedded editor, create a new file with specified content, select and highlight specific text within a file, and then replace that selected text with new content. You can append lines to a file, insert content at a specific location, or delete a range of lines. All of it driven by clicking on actions in the instructions rather than manually editing files. + +Educates also includes YAML-aware editor actions, which is significant because YAML editing is notoriously error-prone when done by hand. A misplaced indent or a missing space after a colon can break an entire configuration file, and debugging YAML syntax issues is not what anyone signs up for in a workshop about Kubernetes or application deployment. The YAML actions let you reference property paths like `spec.replicas` or `spec.template.spec.containers[name=nginx]` and set values, add items to sequences, or replace entries, all while preserving existing comments and formatting in the file. + +Beyond editing, Educates provides examiner actions that run validation scripts to check whether the learner has completed a step correctly. In effect, the workshop can grade the learner's work and provide immediate feedback. If they missed a step or made an error, they find out right away rather than discovering it three steps later when something else breaks. There are also collapsible section actions for hiding optional content or hints until the learner needs them, and file transfer actions for downloading files from the workshop environment to the learner's machine or uploading files into it. + +The end result is that learners can progress through an entire workshop without ever manually typing a command, editing a file by hand, or wondering whether they've completed a step correctly. They focus on understanding the concepts being taught while the clickable actions handle the mechanics. That changes the experience fundamentally. Instead of the workshop being something you push through, it becomes something that carries you forward. + +## The dashboard in action + +To get a sense for what this looks like in practice, here are a couple of screenshots from an Educates workshop. + + +![Workshop instructions with a clickable terminal command and the result displayed in the terminal panel](terminal-execute-example.png) + +The instructions panel on the left contains a clickable action for running a command. When the learner clicks it, the command executes in the terminal panel and the output appears immediately. No copying, no pasting, no typing. + + +![The embedded editor showing text that has been selected and replaced through clickable actions in the instructions](editor-replace-example.png) + +Here the embedded editor shows the result of a select-and-replace flow. The instructions guided the learner through highlighting specific text in a file and then replacing it with updated content, all through clickable actions. The learner sees exactly what changed and why, without needing to manually locate the right line and make the edit themselves. + +## How it works in the instructions + +Workshop instructions in Educates are written in markdown. Clickable actions are embedded as specially annotated fenced code blocks where the language identifier specifies the action type and the body contains YAML configuration that controls what the action does. + +For example, to guide a learner through updating an image reference in a Kubernetes deployment file, you might include two actions in sequence. The first selects the text that needs to change: + +```` +```editor:select-matching-text +file: ~/exercises/deployment.yaml +text: "image: nginx:1.19" +``` +```` + +The second replaces the selected text with the new value: + +```` +```editor:replace-text-selection +file: ~/exercises/deployment.yaml +text: "image: nginx:latest" +``` +```` + +When the learner clicks the first action, the matching text is highlighted in the editor so they can see exactly what will change. When they click the second, the replacement is applied. They understand the change being made because they see both the before and after states, but they don't need to manually find the right line, select the text, and type the replacement. The instructions guide them through it. + +For terminal commands, the syntax is even simpler: + +```` +```terminal:execute +command: |- + echo "Hello from terminal:execute" +``` +```` + +The YAML within each code block controls everything about the action: which file to operate on, what text to match or replace, which terminal session to use, and so on. The format is consistent across all action types. Once you understand the pattern of action type as the language identifier and YAML configuration as the body, authoring with actions is straightforward. + +## The value of removing friction + +The progression from copy/paste tutorials to hosted environments to clickable commands to a fully guided experience like Educates is ultimately a progression toward removing every point where a learner might disengage. Each improvement eliminates another source of friction, another moment where someone might lose focus because they're fighting the tools instead of learning the material. When the mechanics of following instructions become invisible, learners stay engaged longer and absorb more of what the workshop is trying to teach. + +In our [previous post](/blog/when-ai-content-isnt-slop/) we discussed how this interactive format, combined with thoughtful use of AI for content generation, can produce workshop content that maintains consistent quality throughout. The clickable actions we've described here are what make that format possible. They're the mechanism that turns static instructions into a guided, interactive experience where the learner's attention stays on the concepts rather than the process. diff --git a/blog/2026-02-20-clickable-actions-in-workshops/terminal-execute-example.png b/blog/2026-02-20-clickable-actions-in-workshops/terminal-execute-example.png new file mode 100644 index 0000000..0bcf73c Binary files /dev/null and b/blog/2026-02-20-clickable-actions-in-workshops/terminal-execute-example.png differ diff --git a/blog/2026-02-24-fixed-ip-bridge-macos/index.md b/blog/2026-02-24-fixed-ip-bridge-macos/index.md new file mode 100644 index 0000000..d495569 --- /dev/null +++ b/blog/2026-02-24-fixed-ip-bridge-macos/index.md @@ -0,0 +1,317 @@ +--- +slug: fixed-ip-bridge-macos +title: Maintaining a Fixed IP for Educates Local Clusters on macOS +authors: [jorge] +tags: [educates, local, tips-and-tricks, dns, macos] +--- + +When running Educates locally on macOS, your cluster's accessibility depends on your machine's IP address. Every time you move between networks — home, office, conference WiFi — your IP changes. DNS resolution breaks, cluster ingresses stop responding, and workshop URLs go stale. This can be a hassle — especially when you don't immediately realize the IP changed and spend time debugging something else entirely. You end up manually updating the resolver configuration before you can get back to work. + +In the [How to best work locally post](/blog/how-to-best-work-locally/), we showed how to configure a local DNS resolver with a recognizable domain like `educates.test`. And in [Automating DNS Resolver Updates](/blog/sync-resolver-macos), we covered how to detect IP changes and re-sync the resolver automatically. Both of those approaches react to the IP change after it happens. The approach in this post eliminates the change altogether. A better approach is to prevent the problem entirely: give your machine a fixed IP that never changes, regardless of which physical network you're on. + + + +## Why a Fixed IP Matters + +Educates local clusters use DNS resolution to map a domain like `educates.test` to your machine's IP. When that IP changes: + +- The DNS resolver points to a stale address +- Cluster ingresses become unreachable via their configured hostnames +- Workshop URLs break mid-session +- The `dnsmasq` container needs to be reconfigured and restarted + +If you configure Educates to use a fixed IP — one that's independent of your physical network interface — none of this happens. Your cluster always resolves to the same address, whether you're on WiFi, Ethernet, or just woke your laptop from sleep. + +## Virtual Bridge Interfaces on macOS + +macOS supports virtual bridge interfaces — software-defined network interfaces that exist independently of your physical hardware. You can assign a static IP to a bridge, and it will remain stable regardless of what happens on `en0` or `en1`. + +The key insight is that this bridge doesn't need to route traffic to the outside world. It only needs to be reachable from your local machine, which is exactly what Educates needs. + +### Creating the Bridge + +Create a bridge interface using `ifconfig`: + +```bash +sudo ifconfig bridge1 create +``` + +Assign a static IP in a range that won't conflict with your common networks — `10.10.10.1` works well for this since it's unlikely to collide with typical home or office networks: + +```bash +sudo ifconfig bridge1 inet 10.10.10.1/24 +``` + +Verify the configuration: + +```bash +ifconfig bridge1 +``` + +You should see output like: + +``` +bridge1: flags=8863 mtu 1500 + options=63 + ether 86:2f:57:13:e3:01 + inet 10.10.10.1 netmask 0xffffff00 broadcast 10.10.10.255 + Configuration: + id 0:0:0:0:0:0 priority 0 hellotime 0 fwddelay 0 + maxage 0 holdcnt 0 proto stp maxaddr 100 timeout 1200 + root id 0:0:0:0:0:0 priority 0 ifcost 0 port 0 + ipfilter disabled flags 0x0 + media: + status: inactive +``` + +The `status: inactive` is expected — the bridge isn't connected to any physical interface, and it doesn't need to be. The `inet 10.10.10.1` line is what matters. + +### Configuring Educates to Use the Bridge IP + +Update your Educates local configuration to use the bridge IP: + +```bash +educates local config edit +``` + +Set the domain and resolver host IP: + +```yaml +clusterIngress: + domain: educates.test +resolver: + hostIP: 10.10.10.1 +``` + +Then deploy or update your resolver: + +```bash +# If you haven't deployed the resolver yet +educates local resolver deploy + +# If the resolver is already running +educates local resolver update +``` + +From this point on, your `educates.test` domain will always resolve to `10.10.10.1` — a fixed address that doesn't depend on your network connection. + +## The Problem: Bridges Don't Survive Reboots + +The bridge you just created is ephemeral. Reboot your Mac, and it's gone. Wake from sleep after a long period, and the network stack may have reset it. This is where automation comes in. + +## Automating Bridge Creation with a LaunchDaemon + +A LaunchDaemon runs as root and starts before any user logs in — exactly what we need for a network interface. Unlike a LaunchAgent (which runs in user space), a LaunchDaemon has the privileges to create and configure network interfaces without `sudo`. + +### The Bridge Creation Script + +Create the script that will manage the bridge interface: + +```bash +sudo mkdir -p /usr/local/scripts +``` + +Create `/usr/local/scripts/configure_bridge.sh` with the following content: + +```bash +#!/bin/bash + +# --- SELF-LOGGING START --- +LOG_FILE="/var/log/staticbridge.log" + +# Check if log file is larger than 1MB (1048576 bytes) and delete it if so +if [ -f "$LOG_FILE" ] && [ $(stat -f%z "$LOG_FILE") -ge 1048576 ]; then + rm "$LOG_FILE" +fi + +# Ensure the log file is writable (in case it was created by root differently) +# If this fails, the script will continue but logs might go to system log. +touch "$LOG_FILE" 2>/dev/null + +# Redirect all future output (1) and errors (2) to the log file +exec 1>>"$LOG_FILE" 2>&1 +# --- SELF-LOGGING END --- + +# Configuration +INTERFACE="bridge1" +IP_ADDRESS="10.10.10.1/24" + +echo "--- Starting execution at $(date) ---" + +# 1. Check if the bridge interface already exists +if /sbin/ifconfig "$INTERFACE" > /dev/null 2>&1; then + echo "Interface $INTERFACE already exists." +else + echo "Interface $INTERFACE not found. Attempting to create..." + if /sbin/ifconfig "$INTERFACE" create; then + echo "Successfully created $INTERFACE." + else + echo "ERROR: Failed to create $INTERFACE." + exit 1 + fi +fi + +# 2. Configure the IP Address +echo "Configuring IP $IP_ADDRESS on $INTERFACE..." +if /sbin/ifconfig "$INTERFACE" inet "$IP_ADDRESS"; then + echo "IP address assigned successfully." +else + echo "ERROR: Failed to assign IP address." + exit 1 +fi + +# 3. Final Verification +CURRENT_CONFIG=$(/sbin/ifconfig "$INTERFACE") +if [[ "$CURRENT_CONFIG" == *"$IP_ADDRESS"* ]] || [[ "$CURRENT_CONFIG" == *"10.10.10.1"* ]]; then + echo "SUCCESS: Bridge is up and IP is verified." +else + echo "WARNING: Script finished, but IP verification failed. Check interface manually." + exit 1 +fi + +exit 0 +``` + +Make it executable: + +```bash +sudo chmod +x /usr/local/scripts/configure_bridge.sh +``` + +A few things worth noting about this script: + +- It uses absolute paths for `ifconfig` (`/sbin/ifconfig`) because LaunchDaemons run with a minimal `PATH` +- It handles the log rotation itself, keeping the log file under 1MB +- It's idempotent — if the bridge already exists with the correct IP, it reconfigures it without error +- Each execution is timestamped in the log for easy debugging + +### The LaunchDaemon Configuration + +Create the plist file at `/Library/LaunchDaemons/com.educates.staticbridge.plist`: + +```xml + + + + + Label + com.educates.staticbridge + + ProgramArguments + + /usr/local/scripts/configure_bridge.sh + + + RunAtLoad + + + KeepAlive + + + StartInterval + 60 + + WatchPaths + + /Library/Preferences/SystemConfiguration/NetworkInterfaces.plist + /Library/Preferences/SystemConfiguration/com.apple.network.identification.plist + + + StandardOutPath + /var/log/educates-bridge.log + + StandardErrorPath + /var/log/educates-bridge.log + + +``` + +This configuration does two things: + +- **RunAtLoad** ensures the bridge is created immediately on system startup +- **StartInterval** re-runs the script every 60 seconds, catching cases where the bridge was destroyed — after wakeup from sleep, network stack resets, or any other event that removes the interface +- **WatchPaths**: Monitors network configuration files for changes, triggering on network activation +- **KeepAlive**: Set to `false` since we're polling and watching paths + +The 60-second interval is a pragmatic choice. macOS doesn't provide a reliable single event for "the network stack just reset your interfaces." Different macOS versions, different hardware, different sleep/wake scenarios — they all behave slightly differently. Polling every 60 seconds with an idempotent script is simple, reliable, and has negligible system overhead. + +### Loading the LaunchDaemon + +```bash +sudo launchctl load /Library/LaunchDaemons/com.educates.staticbridge.plist +``` + +Verify it's loaded: + +```bash +sudo launchctl list | grep staticbridge +``` + +You should see the daemon in the output with a `0` exit status (or `-` if it hasn't run yet). + +## Verifying the Setup + +After loading the daemon, confirm everything is working: + +```bash +# Check the bridge exists and has the correct IP +ifconfig bridge1 | grep "inet " + +# Test DNS resolution through the Educates resolver +dig @10.10.10.1 test.educates.test + +# Check the daemon logs +cat /var/log/educates-bridge.log +``` + +To test resilience, you can destroy the bridge and wait for it to be recreated: + +```bash +# Destroy the bridge +sudo ifconfig bridge1 destroy + +# Wait 60 seconds for the daemon to recreate it +sleep 65 + +# Verify it's back +ifconfig bridge1 | grep "inet " +``` + +## Removing the Setup + +To remove the LaunchDaemon and bridge: + +```bash +sudo launchctl unload /Library/LaunchDaemons/com.educates.staticbridge.plist +sudo rm /Library/LaunchDaemons/com.educates.staticbridge.plist +sudo rm /usr/local/scripts/configure_bridge.sh +sudo ifconfig bridge1 destroy +``` + +## Design Rationale + +This approach uses a virtual bridge rather than aliasing an IP on an existing interface because: + +- **Isolation**: The bridge doesn't interfere with your primary network configuration — no risk of IP conflicts on your actual interfaces +- **Persistence**: Virtual bridges can be recreated programmatically without affecting system network settings +- **Independence**: It works regardless of which physical interface is active — WiFi, Ethernet, Thunderbolt adapter, or none at all +- **Simplicity**: No need to hook into macOS network preferences, DHCP, or System Settings + +The polling-based LaunchDaemon is intentionally simple. macOS offers `WatchPaths` and network change notifications, but in practice they don't fire reliably for all the scenarios that can destroy a bridge interface. `WatchPaths` handles the majority of transitions reliably, but a periodic check provides a safety net for edge cases — particularly USB-C docking stations and VPN connections that don't always trigger a SystemConfiguration update. A 60-second poll with an idempotent script trades theoretical elegance for real-world reliability — and the resource cost is effectively zero. + +We chose a LaunchDaemon (system-level, runs as root) rather than a LaunchAgent (user-level) because `ifconfig` requires root privileges. A LaunchAgent would need workarounds for privilege escalation that add complexity without benefit. + +If you're combining this with the local resolver and CA setup from [How to best work locally](/blog/how-to-best-work-locally/), the fixed IP simplifies the overall stack. With a stable address, the auto-sync script from the [Automating DNS Resolver Updates post](/blog/sync-resolver-macos) becomes optional — your IP never changes, so the resolver never goes stale. The full recommended stack becomes: + +1. Loopback alias via LaunchDaemon (this post) +2. Local CA with `mkcert` ([How to best work locally](/blog/how-to-best-work-locally/)) +3. DNS resolver with `educates.test` pointing to the fixed IP + +Once you have all three in place, you can destroy and create clusters freely, switch between networks, and your workshops will always be available at the same URLs. + +## Bonus points + +You can even create a [SwiftBar plugin](https://swiftbar.app/) to have some of your Educates local cluster details visible, as well as triggering some +Educates local commands. But if you want to know how, ask us, and we'll write about that. + +![SwiftBar Plugin](swiftbar-plugin.png) \ No newline at end of file diff --git a/blog/2026-02-24-fixed-ip-bridge-macos/swiftbar-plugin.png b/blog/2026-02-24-fixed-ip-bridge-macos/swiftbar-plugin.png new file mode 100644 index 0000000..bdc4e14 Binary files /dev/null and b/blog/2026-02-24-fixed-ip-bridge-macos/swiftbar-plugin.png differ diff --git a/blog/tags.yml b/blog/tags.yml index cc98676..e694f7d 100644 --- a/blog/tags.yml +++ b/blog/tags.yml @@ -91,4 +91,9 @@ dns: ai: label: AI permalink: /ai - description: AI \ No newline at end of file + description: AI + +macos: + label: macOS + permalink: /macos + description: macOS \ No newline at end of file