| 1 | // echoapp is the e2e/vet test tenant: a minimal app honoring the platform |
| 2 | // contract — it accepts -socket (unix) or -addr (TCP), or inherits a |
| 3 | // systemd-activated listener (LISTEN_FDS, the carlos-app@.socket contract), |
| 4 | // answers GET /healthz with "ok", and echoes its BUILD env everywhere else. |
| 5 | // |
| 6 | // It also accepts -db and ignores it. The two backends spawn a tenant |
| 7 | // DIFFERENTLY: carlos-app@.service runs `live/<host> serve` with no flags |
| 8 | // (the listener arrives as LISTEN_FDS), while the activator's ExecBackend |
| 9 | // runs `live/<host> --socket <s> --db <path>` (backend_exec.go). An app that |
| 10 | // declares only -socket/-addr therefore works unit-backed and dies instantly |
| 11 | // exec-backed — Go's flag package rejects the unknown --db, prints usage and |
| 12 | // exits before binding anything, so the activator waits out its 30s health |
| 13 | // timeout on a socket that will never appear and the edge serves "instance |
| 14 | // is waking; retry shortly" forever. |
| 15 | // |
| 16 | // That is exactly what happened on a live deployment: an instance |
| 17 | // provisioned through `carlos instances create` could not boot, because this |
| 18 | // tenant — the one the e2e suite treats as the reference app — only ever met |
| 19 | // the unit-backed half of the contract it claims to honour. echoapp keeps no |
| 20 | // state, so the flag is accepted and discarded; the point is to be spawnable |
| 21 | // by BOTH backends. |
| 22 | package main |
| 23 | |
| 24 | import ( |
| 25 | "flag" |
| 26 | "fmt" |
| 27 | "net" |
| 28 | "net/http" |
| 29 | "os" |
| 30 | "strconv" |
| 31 | ) |
| 32 | |
| 33 | func main() { |
| 34 | socket := flag.String("socket", "", "unix socket to listen on") |
| 35 | addr := flag.String("addr", "", "TCP host:port to listen on") |
| 36 | // Accepted and ignored: the activator always passes it (every route the |
| 37 | // provisioning pass mints carries a DBPath), and a tenant that cannot |
| 38 | // PARSE it never reaches its listener. echoapp keeps no state. |
| 39 | _ = flag.String("db", "", "instance database path (accepted, unused: echoapp is stateless)") |
| 40 | flag.Parse() |
| 41 | |
| 42 | mux := http.NewServeMux() |
| 43 | mux.HandleFunc("/healthz", func(w http.ResponseWriter, _ *http.Request) { |
| 44 | fmt.Fprint(w, "ok") |
| 45 | }) |
| 46 | mux.HandleFunc("/", func(w http.ResponseWriter, _ *http.Request) { |
| 47 | fmt.Fprintf(w, "echoapp BUILD=%s", os.Getenv("BUILD")) |
| 48 | }) |
| 49 | |
| 50 | ln, err := listen(*socket, *addr) |
| 51 | if err != nil { |
| 52 | fmt.Fprintln(os.Stderr, err) |
| 53 | os.Exit(1) |
| 54 | } |
| 55 | if err := http.Serve(ln, mux); err != nil { |
| 56 | fmt.Fprintln(os.Stderr, err) |
| 57 | os.Exit(1) |
| 58 | } |
| 59 | } |
| 60 | |
| 61 | // listen picks the app's listener, activation first: when systemd (or a test |
| 62 | // playing systemd) passed a pre-bound socket — LISTEN_FDS=1, the fd at 3 — |
| 63 | // serve on it and never touch any path. That is the carlos-app@.socket |
| 64 | // contract: the file is root-bound and carlos-owned so only the edge can |
| 65 | // connect; the tenant just inherits the fd. Otherwise bind -socket (unix) |
| 66 | // or -addr (TCP) ourselves. |
| 67 | func listen(socket, addr string) (net.Listener, error) { |
| 68 | if n, _ := strconv.Atoi(os.Getenv("LISTEN_FDS")); n >= 1 { |
| 69 | // systemd sets LISTEN_PID to our pid post-fork; accept it unset for |
| 70 | // harnesses that exec us directly and can't know the pid in advance. |
| 71 | if pid := os.Getenv("LISTEN_PID"); pid == "" || pid == strconv.Itoa(os.Getpid()) { |
| 72 | return net.FileListener(os.NewFile(3, "listen-fd")) |
| 73 | } |
| 74 | } |
| 75 | network, address := "tcp", addr |
| 76 | if socket != "" { |
| 77 | network, address = "unix", socket |
| 78 | os.Remove(socket) |
| 79 | } |
| 80 | return net.Listen(network, address) |
| 81 | } |
| 82 | |