| 1 | package host |
| 2 | |
| 3 | import ( |
| 4 | "errors" |
| 5 | "fmt" |
| 6 | "strings" |
| 7 | |
| 8 | "github.com/carlosframework/platform/internal/registry" |
| 9 | ) |
| 10 | |
| 11 | // FleetIdentity is a fleet box's resolved identity: which fleet it belongs |
| 12 | // to, and which of that fleet's boxes IT is. |
| 13 | // |
| 14 | // Label is the operator-chosen fleet_boxes.label — the channel identity the |
| 15 | // edge actually dials ("<Fleet>/<Label>", edge.fleetChannelFor) — and |
| 16 | // deliberately NEVER status.Label(machineID). That hashed value is the |
| 17 | // PUBLIC status page's opaque pseudonym, chosen specifically so a customer's |
| 18 | // machine id can never be recovered from a feed anyone can view; a fleet |
| 19 | // box's real identity is a name an operator picked when they ran |
| 20 | // `carlos fleets add-box`, and nothing else on the platform (the edge's |
| 21 | // tunnel hub, the registry's fleet_boxes table, a fleet binding object) ever |
| 22 | // refers to a box any other way. Using the hashed pseudonym here would |
| 23 | // publish bindings the edge could never dial back. |
| 24 | type FleetIdentity struct { |
| 25 | Fleet string |
| 26 | Label string |
| 27 | } |
| 28 | |
| 29 | // errNotFleetBox is resolveFleetIdentity's sentinel for "this agent isn't a |
| 30 | // fleet box at all" (CARLOS_FLEET unset) — distinct from an invalid |
| 31 | // identity, which callers must treat very differently: the former is every |
| 32 | // platform box today (silent, expected), the latter is a fleet box |
| 33 | // misconfigured badly enough that it must refuse to arm its fleet |
| 34 | // machinery rather than run under a label nothing else in the system would |
| 35 | // recognize. |
| 36 | var errNotFleetBox = errors.New("host: CARLOS_FLEET is not set") |
| 37 | |
| 38 | // resolveFleetIdentity reads CARLOS_FLEET and CARLOS_FLEET_BOX (falling |
| 39 | // back to CARLOS_TUNNEL_NAME, since a fleet box's tunnel name already has |
| 40 | // the "<fleet>/<label>" shape tunnel_client.go dials with) and validates |
| 41 | // both halves with registry.ValidTunnelName — the exact grammar |
| 42 | // fleet_boxes.label, Route.Tunnel, and a fleet name in Route.Region |
| 43 | // ("fleet/<name>", registry.FleetPlacement) all already share, so a value |
| 44 | // this accepts is guaranteed usable everywhere else in the system that |
| 45 | // stores or parses one. |
| 46 | // |
| 47 | // Returns (_, errNotFleetBox) when CARLOS_FLEET is unset — the ordinary, |
| 48 | // silent, every-platform-box-today case; callers must not log anything for |
| 49 | // it. Any OTHER non-nil error names a genuine misconfiguration (an invalid |
| 50 | // fleet name, or a box label that isn't set and can't be derived/doesn't |
| 51 | // validate) — callers must refuse to arm whatever fleet-box behavior they |
| 52 | // gate on this, loudly, rather than fall back to guessing an identity. |
| 53 | func resolveFleetIdentity(getenv func(string) string) (FleetIdentity, error) { |
| 54 | fleet := strings.TrimSpace(getenv("CARLOS_FLEET")) |
| 55 | if fleet == "" { |
| 56 | return FleetIdentity{}, errNotFleetBox |
| 57 | } |
| 58 | if !registry.ValidTunnelName(fleet) { |
| 59 | return FleetIdentity{}, fmt.Errorf("CARLOS_FLEET=%q is not a valid fleet name", fleet) |
| 60 | } |
| 61 | |
| 62 | label := strings.TrimSpace(getenv("CARLOS_FLEET_BOX")) |
| 63 | if label == "" { |
| 64 | // A fleet box's CARLOS_TUNNEL_NAME is minted as "<fleet>/<label>" |
| 65 | // (plan doc: "FleetChannelAuth: channel name '<fleet>/<label>'"), so |
| 66 | // stripping this fleet's own prefix recovers the label without a |
| 67 | // second env var in the common case. CutPrefix — not TrimPrefix — is |
| 68 | // load-bearing here (fix round 2, Opus re-review): TrimPrefix |
| 69 | // no-ops when CARLOS_TUNNEL_NAME doesn't actually carry this exact |
| 70 | // prefix, silently leaving the WHOLE tunnel name behind as the |
| 71 | // "label" — and a bare legacy tunnel name (CARLOS_TUNNEL_NAME= |
| 72 | // "pi-box", no "/" at all — the live shape in today's runbook) |
| 73 | // still passes ValidTunnelName, so that silent fallback would have |
| 74 | // published a Box the edge could never dial: fleetChannelFor's rule |
| 75 | // 1 checks hub.State(fleet+"/"+boxLabel), but the hub only ever |
| 76 | // knows this box's channel by its RAW CARLOS_TUNNEL_NAME — "pi-box", |
| 77 | // not "pi/pi-box". A fleet box's channel MUST be "<fleet>/<label>" |
| 78 | // for FleetChannelAuth to key it at all, so a tunnel name missing |
| 79 | // that exact prefix is refused here rather than silently accepted. |
| 80 | tunnelName := strings.TrimSpace(getenv("CARLOS_TUNNEL_NAME")) |
| 81 | derived, ok := strings.CutPrefix(tunnelName, fleet+"/") |
| 82 | if !ok { |
| 83 | return FleetIdentity{}, fmt.Errorf( |
| 84 | "fleet box label could not be derived from CARLOS_TUNNEL_NAME=%q (want the %q prefix) — set CARLOS_FLEET_BOX explicitly", |
| 85 | tunnelName, fleet+"/") |
| 86 | } |
| 87 | label = derived |
| 88 | } |
| 89 | if label == "" || !registry.ValidTunnelName(label) { |
| 90 | return FleetIdentity{}, fmt.Errorf( |
| 91 | "fleet box label %q is invalid — set CARLOS_FLEET_BOX, or CARLOS_TUNNEL_NAME=%s/<label>", |
| 92 | label, fleet) |
| 93 | } |
| 94 | return FleetIdentity{Fleet: fleet, Label: label}, nil |
| 95 | } |
| 96 | |