| 1 | // The agent's wiring for the host half of the reverse channel (issue #53 / C6 |
| 2 | // Step 3). The dial/serve/backoff machinery itself lives in |
| 3 | // internal/tunnelclient — a leaf package, so the edge's acceptance test can |
| 4 | // drive the SAME loop this agent runs (internal/host imports internal/edge, |
| 5 | // so the edge tests can never import this package). What is left here is the |
| 6 | // part that is genuinely agent-shaped: reading the env, and publishing the |
| 7 | // state the status heartbeat reports. |
| 8 | package host |
| 9 | |
| 10 | import ( |
| 11 | "context" |
| 12 | "log" |
| 13 | "net/http" |
| 14 | "os" |
| 15 | "sync/atomic" |
| 16 | |
| 17 | "github.com/carlosframework/platform/internal/tunnelclient" |
| 18 | ) |
| 19 | |
| 20 | // tunnelStateVal is "" (disabled/never started), "connected", or |
| 21 | // "reconnecting" — read by statusLoop's heartbeat probe (probes must fail |
| 22 | // toward honest values; "" means the loop isn't running at all). |
| 23 | var tunnelStateVal atomic.Value |
| 24 | |
| 25 | func tunnelState() string { |
| 26 | if v, ok := tunnelStateVal.Load().(string); ok { |
| 27 | return v |
| 28 | } |
| 29 | return "" |
| 30 | } |
| 31 | |
| 32 | // tunnelLoop dials CARLOS_TUNNEL_URL and serves handler over the inverted |
| 33 | // conn, forever, with jittered capped backoff. Inert unless all three env |
| 34 | // vars are set — the same convention as every other optional agent loop. |
| 35 | func tunnelLoop(handler http.Handler) { |
| 36 | rawURL := os.Getenv("CARLOS_TUNNEL_URL") |
| 37 | token := os.Getenv("CARLOS_TUNNEL_TOKEN") |
| 38 | name := os.Getenv("CARLOS_TUNNEL_NAME") |
| 39 | if rawURL == "" || token == "" || name == "" { |
| 40 | if rawURL != "" || token != "" || name != "" { |
| 41 | log.Printf("agent: tunnel loop disabled: need all of CARLOS_TUNNEL_URL, CARLOS_TUNNEL_TOKEN, CARLOS_TUNNEL_NAME") |
| 42 | } |
| 43 | return |
| 44 | } |
| 45 | log.Printf("agent: tunnel loop armed (%s as %q)", rawURL, name) |
| 46 | // context.Background(): the agent's tunnel runs for the life of the |
| 47 | // process and is torn down with it. The ctx exists for callers that need |
| 48 | // to stop the loop deterministically (the edge's e2e), not for this one. |
| 49 | tunnelclient.Loop(context.Background(), tunnelclient.Config{ |
| 50 | URL: rawURL, |
| 51 | Name: name, |
| 52 | Token: token, |
| 53 | Handler: handler, |
| 54 | OnState: func(s string) { tunnelStateVal.Store(s) }, |
| 55 | // The "agent: " prefix is what operators grep the journal for, so it |
| 56 | // is preserved here rather than baked into the leaf package. |
| 57 | Logf: func(format string, args ...any) { log.Printf("agent: "+format, args...) }, |
| 58 | }) |
| 59 | } |
| 60 | |