carlos / platform Public

Clone
git clone https://amadan.net/carlos/platform

Plain git — no account needed to clone.

Download

Download this file

1// `carlos routes` — what routes this app has, and what each one is bound
2// to (issue #64).
3//
4// API MODE ONLY, and that is the point of the verb existing at all. The
5// facts it prints live in a box's registry, and reading them used to mean
6// sqlite3 on the box: `carlos route` is a setter, `carlos apps` answers
7// from the console's app list, and `carlos instances list` answers for
8// instance RECORDS — so a hand-added route had no reader anywhere. Since a
9// route's ENVIRONMENT binding decides which sealed secrets it is
10// delivered, "which routes is this credential going to" was a question
11// only answerable with box access: the one step of titogo's whole secrets
12// procedure that needed it.
13//
14// The box-side listing is `carlos instances` (cli.go), which carries the
15// same columns for an operator already standing on the box. A second
16// box-local verb answering the same question would be two commands to keep
17// in step for no gain.
18package main
19
20import (
21 "context"
22 "flag"
23 "fmt"
24 "log"
25 "strings"
26)
27
28const routesUsage = `usage: carlos routes --app <a> [--account <sqid|name>] [--match <substr>] [--console <origin>]
29
30Lists this app's routes on the console's deployment: where each one sends
31traffic, which release channel it follows, and which named config
32environment it is bound to — the binding that decides which secrets the
33route's process is delivered (see carlos env, carlos secrets).
34
35A hostname whose traffic is split by leading path segment lists those
36prefixes indented beneath it, naming the route each one is served by.
37Read-only: prefix maps are published wholesale by the platform that owns
38them, so there is no carlos verb that writes one.
39
40Signed-in only (carlos auth login). On a box, the local listing is
41"carlos instances".`
42
43func runRoutes(args []string) {
44 fs := flag.NewFlagSet("routes", flag.ExitOnError)
45 app := appFlag(fs, "app whose routes to list")
46 match := fs.String("match", "", "only hosts containing this substring")
47 consoleFlag := consoleOption(fs, "console to list through (default: the only logged-in one)")
48 acctRaw := fs.String("account", "", "account (sqid or name) the app belongs to (default: your only account, or the configured/chosen one)")
49 acctIDRaw := fs.String("account-id", "", "account sqid, matched as a sqid ONLY — supersedes --account")
50 fs.Usage = flagUsage(fs, routesUsage)
51 fs.Parse(args)
52 sess := requireAPISession("routes", *consoleFlag)
53 acct := resolvedAccount(sess, acctRaw, acctIDRaw)
54 app2 := resolvedApp(sess, acct, *app, routesUsage)
55 rows, paths, err := sess.routeList(context.Background(), acct, app2)
56 if err != nil {
57 log.Fatalf("routes: %v", err)
58 }
59 shown := 0
60 w := newTable(stdout)
61 fmt.Fprintln(w, "HOST\tKIND\tUPSTREAM\tCHANNEL\tENVIRONMENT\tREPLICATION\tSTATUS\tHEALTH\tDB")
62 for _, r := range rows {
63 if *match != "" && !strings.Contains(r.Host, *match) {
64 continue
65 }
66 shown++
67 fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n",
68 r.Host, dashIfEmpty(r.Kind), dashIfEmpty(r.Upstream), dashIfEmpty(r.Channel),
69 // An empty ENVIRONMENT is a real answer — the route takes the
70 // app's default bundle — so it prints as a dash rather than a
71 // blank cell a reader would take for missing data.
72 dashIfEmpty(r.Environment),
73 // REPLICATION reads the same way: empty is the pre-ladder
74 // default (change-driven ~1s sync), which is what every route
75 // carried until `carlos route --replication` gave the column a
76 // door (discussion #10). It prints "default" rather than a
77 // dash, because unlike ENVIRONMENT the empty value has a name
78 // and a behaviour worth saying out loud — this is the column
79 // where "no answer" cost one tenant 97% of a day's replica
80 // traffic.
81 replicationCell(r.Replication),
82 dashIfEmpty(r.Status), dashIfEmpty(r.Health), dbCell(r.DBBytes))
83 // Path routes indented under the hostname whose traffic they split.
84 // A flipped prefix set has to be visible from the command operators
85 // already use, or a migration is invisible until something breaks.
86 //
87 // KIND says "path" and UPSTREAM names the TARGET ROUTE rather than
88 // an address, because that is all a path row is: a pointer, whose
89 // channel, environment, replication and health are the target's own
90 // row's, listed wherever that route appears. Dashes rather than
91 // blanks in the remaining columns, for the reason ENVIRONMENT prints
92 // one above — a blank cell reads as data this client failed to fetch.
93 //
94 // Read-only, here and everywhere in the CLI: the tenant's own
95 // platform publishes these wholesale and is their source of truth,
96 // and a second writer would drift from it the first time it was used.
97 for _, p := range paths {
98 if p.Host != r.Host {
99 continue
100 }
101 fmt.Fprintf(w, " /%s\tpath\t%s\t—\t—\t—\t—\t—\t—\n", p.Prefix, dashIfEmpty(p.Target))
102 }
103 }
104 w.Flush()
105 if shown == 0 {
106 mutedf(stdout, "no routes for %s on this deployment's box", app2)
107 }
108 // Said every time, not only when the list is empty: this console reads
109 // its OWN box's registry, so on a multi-box fleet a short answer is not
110 // a complete one and must not be read as one (internal/fleetapi, #145).
111 mutedf(stdout, "(routes on this deployment's own box; other boxes in a fleet are not listed)")
112}
113
114// replicationCell renders the replication rung. The empty column is named
115// rather than dashed: it is the default every route was on before the ladder
116// had a door, it behaves exactly like "stream", and a reader deciding
117// whether a large low-write database is costing more than it should needs to
118// see that stated, not inferred from a blank.
119func replicationCell(class string) string {
120 if class == "" {
121 return "default"
122 }
123 return class
124}
125
126// dashIfEmpty renders an empty cell as an em dash, so a column that is
127// legitimately blank reads as an answer rather than as missing data.
128func dashIfEmpty(s string) string {
129 if s == "" {
130 return "—"
131 }
132 return s
133}
134
135// dbCell renders a route's database size, a dash (this table's own em dash)
136// when no box has measured it.
137func dbCell(n *int64) string {
138 if n == nil {
139 return "—"
140 }
141 return humanBytes(n)
142}
143