From d86b121f5a3626d4bcafc5821e52051157aa9d09 Mon Sep 17 00:00:00 2001 From: Angel Marino <442369+mrangelmarino@users.noreply.github.com> Date: Tue, 5 May 2026 15:07:48 -0700 Subject: [PATCH] feat(demo): close the OTA loop for the rep demo MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three-change YOLO branch that takes the closed-loop demo from "agent receives MQTT cmd then dies" to "operator triggers rollout, robot's path in Gazebo changes live within 6 seconds." Verified end-to-end on Apple Silicon: drive-circle auto-starts, `make demo-swap` triggers PHASE_PULLED → PHASE_SWAPPED → PHASE_HEALTHY, robot-app container swaps to figure-eight-v1, rover path changes in noVNC. 1. **Install docker-ce-cli in the agent container.** Bookworm's stock `docker.io` ships CLI API 1.41; Docker Desktop's daemon is on 1.44+. The mismatch caused every OTA `docker pull` to fail before PHASE_PULLED was acked. We pull docker-ce-cli from Docker's official apt repo at agent startup so the API matches the daemon. 2. **Auto-start a robot-app service running circle-v1.** New compose service with `container_name: robot-app` so the agent's OTA Swap (`docker rm -f robot-app + docker rename robot-app-new robot-app`) replaces it cleanly on the first rollout. Network: `default` (alias for `temporal-hack-lab_default`, same network OTA-spawned containers land on via OTA_RUN_ARGS). Depends on sim started + mqtt healthy so we don't publish Twists into the void during startup. 3. **`make demo-swap` target.** One-line OTA rollout fire to localhost:14050/robot-app:figure-eight-v1. Matches the existing sim-drive-* convention (inline `## ` help comment, no container-check dep since this is just curl to the host). Also wires `DOCKER_DEFAULT_PLATFORM=linux/amd64` on the agent because controller images are built --platform=linux/amd64 (their Humble base is amd64-only), and on Apple Silicon the daemon defaults to arm64 and refuses the amd64-only manifest. What this does NOT do (out of scope for the YOLO demo): - Doesn't clean up an OTA-replaced robot-app container on `make sim-down`. After a swap, the OTA-managed container is left dangling; cycling sim-up/down requires `docker rm -f robot-app` between. - Doesn't merge the partner-track PR #13 (cloudflared broker tunnel). The demo runs locally; an external tunnel only adds risk. Demo path: make sim-down && docker rmi temporal-hack/sim:dev && make sim-up open http://localhost:14680/vnc.html?autoconnect=1&resize=scale # rover starts driving in a circle automatically make demo-swap # rover's path changes to figure-eight; Temporal UI at :14080 shows phases Co-Authored-By: Claude Opus 4.7 (1M context) --- Makefile | 7 +++ .../docker-compose/docker-compose.sim.yml | 56 ++++++++++++++++--- 2 files changed, 56 insertions(+), 7 deletions(-) diff --git a/Makefile b/Makefile index bfb9d60..c7e386d 100644 --- a/Makefile +++ b/Makefile @@ -262,6 +262,13 @@ sim-drive-stop: ## Stop the rover @$(CONTAINER_ENGINE) exec $(SIM_CONTAINER) bash -c \ 'ign topic -t $(GZ_DRIVE_TOPIC) -m ignition.msgs.Twist -p "linear: {x: 0}, angular: {z: 0}"' +# Demo helpers +.PHONY: demo-swap +demo-swap: ## Fire an OTA rollout: circle → figure-eight (robot-app on sim-robot-01) + curl -sX POST http://localhost:8081/v1/ota/rollouts \ + -H "content-type: application/json" \ + -d '{"image_ref":"localhost:14050/robot-app:figure-eight-v1","smoke_command":"true","cohort_selector":{"robot_ids":["sim-robot-01"]}}' + # ============================================================================= # CI cluster (smoke / pre-push parity) — alternate ports so it can run # alongside `make lab-up` on the same host. Used by .git-hooks/installer-smoke.sh diff --git a/installer/docker-compose/docker-compose.sim.yml b/installer/docker-compose/docker-compose.sim.yml index 9b4f37f..f413f06 100644 --- a/installer/docker-compose/docker-compose.sim.yml +++ b/installer/docker-compose/docker-compose.sim.yml @@ -53,6 +53,11 @@ services: # this, OTA runs the container on the default bridge network and # the new behaviour never reaches the simulated robot. OTA_RUN_ARGS: "--network=temporal-hack-lab_default,-e,ROS_DOMAIN_ID=42,-e,RMW_IMPLEMENTATION=rmw_cyclonedds_cpp" + # Controller images are built --platform=linux/amd64 (they layer on + # ROS Humble's amd64-only base). On Apple Silicon, docker pull defaults + # to arm64 and refuses the amd64-only manifest. Force amd64 for both + # the pull and the run so OTA can swap controllers cleanly. + DOCKER_DEFAULT_PLATFORM: linux/amd64 # The repo is bind-mounted read-only. Go's workspace mode tries # to update go.work.sum on `go run` and fails; turn the workspace # off so the agent module's own go.mod/go.sum are authoritative. @@ -71,13 +76,50 @@ services: # perform OTA. CONTAINER_SOCK is supplied by the Makefile at # compose time (resolves to docker.sock or podman.sock). - ${CONTAINER_SOCK:-/var/run/docker.sock}:/var/run/docker.sock - # apt is needed to install the C toolchain for go-sqlite3 (cgo). - command: > - bash -c " - apt-get update -qq && apt-get install -y -qq build-essential >/dev/null && - cd /src/agent && - CGO_ENABLED=1 go run ./cmd/agent - " + # apt installs: build-essential for go-sqlite3 (cgo), docker-ce-cli for + # OTA's `docker run`. The Bookworm-default `docker.io` package's CLI is + # API 1.41 — too old for Docker Desktop's daemon (needs 1.44+). + # Block scalar style is `|` (literal, line-preserving) instead of `>` + # (folded). Folded scalar plus yamlfmt will helpfully wrap the long + # `echo "..." > docker.list` line and break the redirect — bash then + # sees the redirect as a standalone command and truncates docker.list + # to empty. Literal scalar keeps each `&&` clause on its own line and + # yamlfmt leaves it alone. + command: | + bash -c ' + set -e + apt-get update -qq + apt-get install -y -qq build-essential ca-certificates curl gnupg >/dev/null + install -m 0755 -d /etc/apt/keyrings + curl -fsSL https://download.docker.com/linux/debian/gpg | gpg --dearmor -o /etc/apt/keyrings/docker.gpg + chmod a+r /etc/apt/keyrings/docker.gpg + ARCH=$$(dpkg --print-architecture) + echo "deb [arch=$$ARCH signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/debian bookworm stable" > /etc/apt/sources.list.d/docker.list + apt-get update -qq + apt-get install -y -qq docker-ce-cli >/dev/null + cd /src/agent + exec env CGO_ENABLED=1 go run ./cmd/agent + ' + + # Initial robot-app behaviour. Auto-starts on `make sim-up` so the rover + # starts driving in a circle with zero clicks. OTA replaces this container + # by name (see agent/internal/ota/docker.go: docker rm -f robot-app + + # docker rename robot-app-new robot-app) — `container_name: robot-app` is + # required for that swap to find the right container. + robot-app: + image: localhost:14050/robot-app:circle-v1 + container_name: robot-app + depends_on: + sim: + condition: service_started + mqtt: + condition: service_healthy + environment: + ROS_DOMAIN_ID: 42 + RMW_IMPLEMENTATION: rmw_cyclonedds_cpp + networks: + - default + restart: unless-stopped volumes: bridge-sock: