carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1// The `carlos sidecars` verb family (Task 14, design: sidecar members): a
2// sidecar is a process the platform runs that serves no requests and that
3// nothing can dial, so its whole shape follows the instance family's
4// deliberately, minus the fields a sidecar has no use for. See
5// docs/superpowers/specs/2026-08-17-sidecar-members-design.md §11.
6//
7// - `enable` — the app's opt-in to sidecar provisioning. Two modes,
8// resolved credential-first exactly as instances enable's is
9// (instances_cli.go): store mode writes apps/<sqid>/<app>/sidecars.json
10// via provision.PutSidecarManifest with every field under the
11// operator's hand; API mode asks the console, which lets a member's own
12// args/placement/channel/idle/max-run/hibernate through untouched while
13// refusing -pool and -regions — those are operator, store-mode-only
14// policy fields (apisidecars.go's handleAPISidecarEnable).
15// - `create` / `ls` / `wake` / `rm` — the CLI front over the console's
16// sidecar record verbs. API mode ONLY: a record is a console-authorized
17// write, and there is deliberately NO box-side equivalent — a sidecar's
18// host is derived under the reserved .sidecar.internal suffix, and
19// `carlos add` refuses to mint one by hand (its reserved-suffix rail).
20// - `logs` delegates to the existing `carlos logs -host <h>` path for the
21// sidecar's own derived host: logs key on host, not on Kind (design
22// §9.3), so there is no separate sidecar-log machinery to keep in sync.
23//
24// Regions: `-regions` on `enable` takes PLAIN REGION CODES ONLY, never a
25// fleet placement target. A fleet-placed app's sidecars inherit the app's
26// own placement (set by `carlos apps place`), which overrides whatever the
27// manifest's Regions allow-list or a create request says; that allow-list is
28// only ever consulted for an UNPLACED app (Task 12 review finding). Offering
29// `-regions ie,fleet/eleven` here would be unsatisfiable and doubly
30// misleading, so this flag's help text points at `carlos apps place`
31// instead of pretending to accept fleet syntax.
32package main
33
34import (
35 "context"
36 "flag"
37 "fmt"
38 "log"
39 "os"
40 "strings"
41 "time"
42
43 "github.com/carlosframework/platform/internal/provision"
44 "github.com/carlosframework/platform/internal/release"
45)
46
47const sidecarsEnableUsage = `usage: carlos sidecars enable -app <a> [flags]
48
49Signed-in mode (carlos auth login; no CARLOS_DEPLOYMENT_* env): the console
50writes the manifest under your own membership. -pool and -regions are
51operator, store-mode flags and are refused here.
52
53Store mode (CARLOS_DEPLOYMENT_BUCKET/CARLOS_DEPLOYMENT_DIR set): every field
54is the operator's call, and this write REPLACES the whole manifest — the
55same full-overwrite CAS upsert "carlos instances enable" uses, so a field
56you omit here resets to its own default, not to whatever a previous enable
57left it at.
58
59flags:
60 -app <a> app whose sidecars this enables (required)
61 -args "argv..." argv the activator execs the app's own binary
62 with, whitespace-split via strings.Fields — shell-
63 word semantics (quoting, escaping) are
64 deliberately NOT supported, same contract as
65 -seed on "carlos instances enable"
66 -placement <p> "any" (default) or "no-inbound" — no-inbound
67 mints only on boxes declaring CARLOS_INBOUND=none
68 (design §8)
69 -pool <p> pool stamped into every record at create (store
70 mode only; default: "", the shared pool)
71 -channel <c> release channel stamped into every record at
72 create (store-mode default "stable"; signed-in
73 mode: omitted = preserve what the manifest holds)
74 -regions <r1,r2,..> allow-list for a record's region, PLAIN REGION
75 CODES ONLY (store mode only); empty (default) =
76 any shape-valid region. Consulted only for an
77 UNPLACED app — a fleet-placed app's sidecars
78 inherit the app's own placement instead, set
79 with "carlos apps place", which overrides this
80 list (and anything a create request says)
81 entirely. There is no fleet syntax here.
82 -idle <dur> idle timeout before a hibernating sidecar sleeps
83 (default 5m; a Go duration — 5m, 1h — sent to
84 the wire as whole seconds, never a duration
85 string)
86 -max-run <dur> max run duration before the sweeper stops a
87 sidecar regardless of idle (default 1h; same
88 duration-to-seconds conversion as -idle)
89 -no-hibernate this app's sidecars never hibernate (default:
90 they do)
91 -account <a> account (sqid or name) the app belongs to
92 (default "ops")
93 -account-id <id> account sqid, matched as a sqid ONLY — supersedes
94 -account
95 -console <origin> console to enable through (signed-in mode only;
96 default: the only logged-in one)
97
98Writing this manifest is the app's opt-in to sidecar provisioning: absence
99means the feature does not exist for the app, and the API refuses creates
100until this is set (design §3.1, §11).
101`
102
103func runSidecarsEnable(args []string) {
104 fs := flag.NewFlagSet("sidecars enable", flag.ExitOnError)
105 app := fs.String("app", "", "app whose sidecars this enables")
106 argsFlag := fs.String("args", "", "argv the activator execs the app's own binary with, whitespace-split (strings.Fields)")
107 placement := fs.String("placement", "", `"any" (default) or "no-inbound"`)
108 pool := fs.String("pool", "", "pool stamped into every record at create (store mode only)")
109 channel := fs.String("channel", "", "release channel stamped into every record at create")
110 regions := fs.String("regions", "", "comma-separated region allow-list, plain region codes only (store mode only); empty = any shape-valid region — see carlos apps place for fleet placement")
111 idle := fs.String("idle", "", "idle timeout before a hibernating sidecar sleeps (default 5m; Go duration)")
112 maxRun := fs.String("max-run", "", "max run duration before the sweeper stops a sidecar (default 1h; Go duration)")
113 noHibernate := fs.Bool("no-hibernate", false, "this app's sidecars never hibernate (default: they do)")
114 consoleFlag := consoleOption(fs, "console to enable through (signed-in mode only; default: the only logged-in one)")
115 acctRaw, acctIDRaw, acctOf := accountFlags(fs)
116 fs.Parse(args)
117 if *app != "" && !release.ValidApp(*app) {
118 log.Fatalf("carlos sidecars enable: %q is not a valid app name", *app)
119 }
120 set := map[string]bool{}
121 fs.Visit(func(f *flag.Flag) { set[f.Name] = true })
122
123 sess, err := resolveMode(*consoleFlag)
124 if err != nil {
125 log.Fatal(err)
126 }
127 if sess != nil {
128 // Signed-in mode. -pool/-regions are refused rather than ignored —
129 // a flag that silently did nothing would read as policy applied.
130 for _, f := range []string{"pool", "regions"} {
131 if set[f] {
132 log.Fatalf("carlos sidecars enable: -%s is an operator, store-mode flag. Set CARLOS_DEPLOYMENT_BUCKET/CARLOS_DEPLOYMENT_DIR to enable in store mode instead", f)
133 }
134 }
135 var channelReq *string
136 if set["channel"] {
137 channelReq = channel
138 }
139 var argsReq *[]string
140 if set["args"] {
141 // parseArgv("") is an empty (non-nil after this) argv: an
142 // explicit -args "" clears the manifest's args, matching
143 // -channel's explicit-vs-absent split.
144 argv := parseArgv(*argsFlag)
145 if argv == nil {
146 argv = []string{}
147 }
148 argsReq = &argv
149 }
150 var placementReq *string
151 if set["placement"] {
152 placementReq = placement
153 }
154 var idleReq *int
155 if set["idle"] {
156 secs, err := parseDurationSeconds(*idle)
157 if err != nil {
158 log.Fatalf("carlos sidecars enable: -idle: %v", err)
159 }
160 idleReq = &secs
161 }
162 var maxRunReq *int
163 if set["max-run"] {
164 secs, err := parseDurationSeconds(*maxRun)
165 if err != nil {
166 log.Fatalf("carlos sidecars enable: -max-run: %v", err)
167 }
168 maxRunReq = &secs
169 }
170 var hibernateReq *bool
171 if *noHibernate {
172 no := false
173 hibernateReq = &no
174 }
175 printUsing(stdout, sess)
176 acct := resolvedAccount(sess, acctRaw, acctIDRaw)
177 app2 := resolvedApp(sess, acct, *app, sidecarsEnableUsage)
178 m, err := sess.sidecarsEnable(context.Background(), acct, app2,
179 channelReq, argsReq, placementReq, idleReq, maxRunReq, hibernateReq)
180 if err != nil {
181 log.Fatalf("sidecars enable: %v", err)
182 }
183 fmt.Fprintf(stdout, "%s for %s (channel %s, placement %s)\n",
184 colorFor(stdout).ok("enabled sidecars"), m.App, m.Channel, m.Placement)
185 mutedf(stdout, " first sidecar: carlos sidecars create -app %s <name>", m.App)
186 return
187 }
188
189 // Store mode — the operator path: every field this manifest carries is
190 // set fresh on every call (PutSidecarManifest is a full-overwrite CAS
191 // upsert, not a patch), exactly as instances enable's own store-mode
192 // write is.
193 if *app == "" {
194 log.Fatal(sidecarsEnableUsage)
195 }
196 if *channel == "" && !set["channel"] {
197 *channel = "stable"
198 }
199 if !provision.ValidPool(*pool) {
200 log.Fatalf("carlos sidecars enable: %q is not a valid pool", *pool)
201 }
202 if !release.ValidChannel(*channel) {
203 log.Fatalf("carlos sidecars enable: %q is not a valid channel", *channel)
204 }
205 switch *placement {
206 case "", provision.PlacementAny, provision.PlacementNoInbound:
207 default:
208 log.Fatalf(`carlos sidecars enable: -placement %q: want "any" or "no-inbound"`, *placement)
209 }
210 var regionList []string
211 if *regions != "" {
212 for _, r := range strings.Split(*regions, ",") {
213 r = strings.TrimSpace(r)
214 if r == "" || !provision.ValidRegion(r) {
215 log.Fatalf("carlos sidecars enable: %q is not a valid region — -regions takes plain region codes only; for fleet placement use `carlos apps place`", r)
216 }
217 regionList = append(regionList, r)
218 }
219 }
220 var idleSecs int
221 if set["idle"] {
222 var err error
223 idleSecs, err = parseDurationSeconds(*idle)
224 if err != nil {
225 log.Fatalf("carlos sidecars enable: -idle: %v", err)
226 }
227 }
228 var maxRunSecs int
229 if set["max-run"] {
230 var err error
231 maxRunSecs, err = parseDurationSeconds(*maxRun)
232 if err != nil {
233 log.Fatalf("carlos sidecars enable: -max-run: %v", err)
234 }
235 }
236 m := provision.SidecarManifest{
237 Schema: 1, App: *app, Pool: *pool, Channel: *channel, Placement: *placement,
238 Regions: regionList, Args: parseArgv(*argsFlag),
239 IdleSeconds: idleSecs, MaxRunSeconds: maxRunSecs,
240 }
241 if *noHibernate {
242 no := false
243 m.Hibernate = &no
244 }
245 if err := sidecarsEnable(context.Background(), openStore(), acctOf(), *app, m); err != nil {
246 log.Fatal(err)
247 }
248 fmt.Printf("%s for %s (channel %s)\n", colorFor(os.Stdout).ok("enabled sidecars"), *app, *channel)
249}
250
251// parseArgv splits -args's raw value on whitespace (strings.Fields) into
252// the manifest's Args argv — instances enable's -seed contract
253// (instances_cli.go's parseSeed), carried over unchanged: quoting and
254// escaping are deliberately NOT supported, and anything richer belongs in
255// the app's own argv parsing.
256func parseArgv(raw string) []string {
257 return strings.Fields(raw)
258}
259
260// parseDurationSeconds parses a Go duration flag value (e.g. "5m", "1h")
261// into whole seconds for the wire, which carries idle/max-run timeouts as
262// plain integer seconds, never a duration string (design §4.1,
263// provision.SidecarManifest.IdleSeconds/MaxRunSeconds). A negative duration
264// is refused here rather than reaching the console — or, in store mode, the
265// bucket — as a nonsense wire value.
266func parseDurationSeconds(raw string) (int, error) {
267 d, err := time.ParseDuration(raw)
268 if err != nil {
269 return 0, fmt.Errorf("%q is not a valid duration (e.g. 5m, 1h): %w", raw, err)
270 }
271 if d < 0 {
272 return 0, fmt.Errorf("%q must not be negative", raw)
273 }
274 return int(d / time.Second), nil
275}
276
277// sidecarsEnable is the core store-mode `carlos sidecars enable` write —
278// separated from flag parsing/validation above so a test can call it
279// directly against a release.Store, instancesEnable's own precedent
280// (instances_cli.go).
281func sidecarsEnable(ctx context.Context, s release.Store, acct, app string, m provision.SidecarManifest) error {
282 return provision.PutSidecarManifest(ctx, s, acct, app, m)
283}
284
285const sidecarsRecordsUsage = `usage: carlos sidecars create -app <a> [-region <r>] [-channel <c>] <name>
286 carlos sidecars ls -app <a> [-long]
287 carlos sidecars wake -app <a> <name>
288 carlos sidecars rm -app <a> <name>
289 carlos sidecars logs -app <a> <name>
290
291Signed-in only (carlos auth login): sidecar records are console-authorized
292writes — the console checks the name and region against the app's manifest
293(and a fleet-placed app's own placement, which overrides both) and a box's
294sidecar reconcile pass turns the record into a running process. Common
295flags:
296
297 -account <a> account (sqid or name; default "ops")
298 -account-id <id> account sqid, matched as a sqid ONLY
299 -console <origin> console to act through (default: the only logged-in one)
300
301create-only flags:
302
303 -region <r> region code for the record, subject to the manifest's
304 allow-list — or overridden entirely by the app's own
305 placement, when it has one (carlos apps place)
306 -channel <c> release channel the sidecar follows (default: the
307 manifest's, then "stable")
308
309ls-only flags:
310
311 -long print the synthetic host (<name>.<app>.<sqid>.sidecar.internal)
312 instead of the short name
313
314There is no set-upstreams for a sidecar: it has no upstream set, and
315nothing dials it.
316`
317
318// requireSidecarsAPISession resolves signed-in mode for the sidecar record
319// verbs, refusing direct mode by NAME — sidecars' own advice, never
320// instances' (Task 12 review finding, binding on this task): `carlos add`
321// explicitly refuses to mint a sidecar host (its reserved-suffix rail), so
322// a refusal here that pointed at that verb would send an operator
323// somewhere that cannot help them. There is no box-side equivalent for a
324// sidecar record at all — this family is console-authorized only.
325func requireSidecarsAPISession(verb, consoleFlag string) *apiSession {
326 sess, err := resolveMode(consoleFlag)
327 if err != nil {
328 log.Fatal(err)
329 }
330 if sess == nil {
331 log.Fatalf("carlos sidecars %s works through a console, and this shell is in direct bucket mode\n"+
332 "(CARLOS_DEPLOYMENT_BUCKET/CARLOS_DEPLOYMENT_DIR is set). Sidecar records are console-authorized\n"+
333 "writes with no direct-bucket equivalent; unset the store env and run: carlos auth login -console <url>.\n"+
334 "There is no box-side way to mint a sidecar host by hand — carlos add refuses the reserved suffix", verb)
335 }
336 return sess
337}
338
339func runSidecarsCreate(args []string) {
340 fs := flag.NewFlagSet("sidecars create", flag.ExitOnError)
341 app := fs.String("app", "", "app the sidecar belongs to")
342 region := fs.String("region", "", "region code for the record (subject to the manifest's allow-list, or the app's own placement)")
343 channel := fs.String("channel", "", "release channel the sidecar follows (default: the manifest's, then stable)")
344 consoleFlag := consoleOption(fs, "console to create through (default: the only logged-in one)")
345 acctRaw := fs.String("account", "", "account (sqid or name) the app belongs to (default: your only account, or the configured/chosen one)")
346 acctIDRaw := fs.String("account-id", "", "account sqid, matched as a sqid ONLY — supersedes -account")
347 fs.Parse(args)
348 if fs.NArg() != 1 || strings.TrimSpace(fs.Arg(0)) == "" {
349 log.Fatal(sidecarsRecordsUsage)
350 }
351 name := strings.TrimSpace(fs.Arg(0))
352 sess := requireSidecarsAPISession("create", *consoleFlag)
353 printUsing(stdout, sess)
354 acct := resolvedAccount(sess, acctRaw, acctIDRaw)
355 app2 := resolvedApp(sess, acct, *app, sidecarsRecordsUsage)
356 rec, err := sess.sidecarCreate(context.Background(), acct, app2, name, strings.TrimSpace(*region), strings.TrimSpace(*channel))
357 if err != nil {
358 log.Fatalf("sidecars create: %v", err)
359 }
360 fmt.Fprintf(stdout, "%s %s (channel %s)\n", colorFor(stdout).ok("created sidecar"), rec.Name, rec.Channel)
361 // Deliberately NOT "it comes live": the default sidecar hibernates, so the
362 // reconcile pass mints its row and then nothing runs. Saying otherwise
363 // sends a member off to read logs for a process that was never started.
364 mutedf(stdout, " host: %s — its row lands on the box on the next reconcile pass", rec.Host)
365 mutedf(stdout, " a hibernating sidecar (the default) runs nothing until it is woken: carlos sidecars wake %s -app %s", rec.Name, app2)
366 mutedf(stdout, " an always-on one (hibernate: false in the app's sidecar settings) starts as soon as the row lands")
367}
368
369// sidecarVersionCell renders `ls`'s VERSION column: the build the agent
370// last started for the sidecar, or the literal "unknown" when it never has
371// — the honesty rule this branch was built around (design §9.2, binding on
372// this task): a stamp that overclaims is worse than an absent one, so this
373// must NEVER render the record's own Channel (the desired version) as a
374// stand-in for a version nobody has confirmed is actually running.
375func sidecarVersionCell(running string) string {
376 if running == "" {
377 return "unknown"
378 }
379 return running
380}
381
382// sidecarPlacementCell renders a record's placement, defaulting the same
383// way provision.SidecarManifest.PlacementOrDefault does server-side: empty
384// means "any".
385func sidecarPlacementCell(placement string) string {
386 if placement == "" {
387 return provision.PlacementAny
388 }
389 return placement
390}
391
392func runSidecarsList(args []string) {
393 fs := flag.NewFlagSet("sidecars ls", flag.ExitOnError)
394 app := fs.String("app", "", "app whose sidecar records to list")
395 long := fs.Bool("long", false, "print the synthetic host instead of the short name")
396 consoleFlag := consoleOption(fs, "console to list through (default: the only logged-in one)")
397 acctRaw := fs.String("account", "", "account (sqid or name) the app belongs to (default: your only account, or the configured/chosen one)")
398 acctIDRaw := fs.String("account-id", "", "account sqid, matched as a sqid ONLY — supersedes -account")
399 fs.Parse(args)
400 sess := requireSidecarsAPISession("ls", *consoleFlag)
401 acct := resolvedAccount(sess, acctRaw, acctIDRaw)
402 app2 := resolvedApp(sess, acct, *app, sidecarsRecordsUsage)
403 recs, truncated, err := sess.sidecarList(context.Background(), acct, app2)
404 if err != nil {
405 log.Fatalf("sidecars ls: %v", err)
406 }
407 w := newTable(stdout)
408 idCol := "NAME"
409 if *long {
410 idCol = "HOST"
411 }
412 fmt.Fprintln(w, idCol+"\tREGION\tCHANNEL\tPOOL\tPLACEMENT\tVERSION\tCREATED\tBY")
413 for _, r := range recs {
414 id := r.Name
415 if *long {
416 id = r.Host
417 }
418 fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n",
419 id, r.Region, r.Channel, r.Pool, sidecarPlacementCell(r.Placement),
420 sidecarVersionCell(r.RunningVersion), r.CreatedAt, r.CreatedBy)
421 }
422 w.Flush()
423 // The console caps one answer at sidecarListMax (500, apisidecars.go)
424 // and says so honestly via `truncated` — this line is the same honesty
425 // rule as VERSION's "unknown": a list that quietly stops at a prefix
426 // and says nothing is exactly the overclaim the rest of this branch
427 // refuses to make, and it is not hypothetical for the motivating
428 // customer (Eleven: a bot per enrolled member, so exceeding 500 is
429 // growth, not misuse). No pagination here — a cursor is a product
430 // decision, not this fix's job — just the fact that more exist.
431 if truncated {
432 mutedf(stdout, "(showing the first %d sidecars — more exist)", len(recs))
433 }
434}
435
436func runSidecarsWake(args []string) {
437 fs := flag.NewFlagSet("sidecars wake", flag.ExitOnError)
438 app := fs.String("app", "", "app the sidecar belongs to")
439 consoleFlag := consoleOption(fs, "console to wake through (default: the only logged-in one)")
440 acctRaw := fs.String("account", "", "account (sqid or name) the app belongs to (default: your only account, or the configured/chosen one)")
441 acctIDRaw := fs.String("account-id", "", "account sqid, matched as a sqid ONLY — supersedes -account")
442 fs.Parse(args)
443 if fs.NArg() != 1 || strings.TrimSpace(fs.Arg(0)) == "" {
444 log.Fatal(sidecarsRecordsUsage)
445 }
446 name := strings.TrimSpace(fs.Arg(0))
447 sess := requireSidecarsAPISession("wake", *consoleFlag)
448 printUsing(stdout, sess)
449 acct := resolvedAccount(sess, acctRaw, acctIDRaw)
450 app2 := resolvedApp(sess, acct, *app, sidecarsRecordsUsage)
451 res, err := sess.sidecarWake(context.Background(), acct, app2, name)
452 if err != nil {
453 log.Fatalf("sidecars wake: %v", err)
454 }
455 fmt.Fprintf(stdout, "%s %s\n", colorFor(stdout).ok("requested wake for"), name)
456 mutedf(stdout, " host: %s — the box notices within seconds of the next serial move", res.Host)
457 // The console cannot confirm the wake landed: a box reports its state on
458 // the 5-minute status snapshot, and a running version only appears for a
459 // sidecar that calls /v1/ready, which an app is never required to do. So
460 // this names where the answer actually is rather than implying the write
461 // itself was the confirmation.
462 mutedf(stdout, " watch it come up: carlos sidecars logs %s -app %s", name, app2)
463}
464
465func runSidecarsRm(args []string) {
466 fs := flag.NewFlagSet("sidecars rm", flag.ExitOnError)
467 app := fs.String("app", "", "app the sidecar belongs to")
468 consoleFlag := consoleOption(fs, "console to delete through (default: the only logged-in one)")
469 acctRaw := fs.String("account", "", "account (sqid or name) the app belongs to (default: your only account, or the configured/chosen one)")
470 acctIDRaw := fs.String("account-id", "", "account sqid, matched as a sqid ONLY — supersedes -account")
471 fs.Parse(args)
472 if fs.NArg() != 1 || strings.TrimSpace(fs.Arg(0)) == "" {
473 log.Fatal(sidecarsRecordsUsage)
474 }
475 name := strings.TrimSpace(fs.Arg(0))
476 sess := requireSidecarsAPISession("rm", *consoleFlag)
477 printUsing(stdout, sess)
478 acct := resolvedAccount(sess, acctRaw, acctIDRaw)
479 app2 := resolvedApp(sess, acct, *app, sidecarsRecordsUsage)
480 if err := sess.sidecarDelete(context.Background(), acct, app2, name); err != nil {
481 log.Fatalf("sidecars rm: %v", err)
482 }
483 fmt.Fprintf(stdout, "%s %s — its process stops on the next reconcile pass; parked data stays\n",
484 colorFor(stdout).ok("deleted sidecar"), name)
485}
486
487// runSidecarsLogs delegates to the existing `carlos logs` path for the
488// sidecar's own derived host: logs key on host, not on Kind (design §9.3),
489// so there is no separate sidecar-log machinery to keep in sync. The name
490// is resolved to its host through sidecarGet rather than computed locally,
491// so this client never has to re-derive provision.SidecarHost's shape.
492func runSidecarsLogs(args []string) {
493 fs := flag.NewFlagSet("sidecars logs", flag.ExitOnError)
494 app := fs.String("app", "", "app the sidecar belongs to")
495 consoleFlag := consoleOption(fs, "console to read through (default: the only logged-in one)")
496 acctRaw := fs.String("account", "", "account (sqid or name) the app belongs to (default: your only account, or the configured/chosen one)")
497 acctIDRaw := fs.String("account-id", "", "account sqid, matched as a sqid ONLY — supersedes -account")
498 fs.Parse(args)
499 if fs.NArg() != 1 || strings.TrimSpace(fs.Arg(0)) == "" {
500 log.Fatal(sidecarsRecordsUsage)
501 }
502 name := strings.TrimSpace(fs.Arg(0))
503 sess := requireSidecarsAPISession("logs", *consoleFlag)
504 acct := resolvedAccount(sess, acctRaw, acctIDRaw)
505 app2 := resolvedApp(sess, acct, *app, sidecarsRecordsUsage)
506 rec, err := sess.sidecarGet(context.Background(), acct, app2, name)
507 if err != nil {
508 log.Fatalf("sidecars logs: %v", err)
509 }
510 logsArgs := []string{"-app", app2, "-host", rec.Host}
511 if *consoleFlag != "" {
512 logsArgs = append(logsArgs, "-console", *consoleFlag)
513 }
514 if *acctRaw != "" {
515 logsArgs = append(logsArgs, "-account", *acctRaw)
516 }
517 if *acctIDRaw != "" {
518 logsArgs = append(logsArgs, "-account-id", *acctIDRaw)
519 }
520 runLogs(logsArgs)
521}
522
523// runSidecars dispatches carlos sidecars' subcommand words BEFORE any flag
524// parsing — runInstances' own contract (cli.go): a subcommand word is not
525// a flag. Unlike instances, every word here is required; there is no bare
526// box-local listing form for a sidecar (it has no route to list).
527func runSidecars(args []string) {
528 if len(args) == 0 {
529 log.Fatal(sidecarsEnableUsage)
530 }
531 switch args[0] {
532 case "enable":
533 runSidecarsEnable(args[1:])
534 case "create":
535 runSidecarsCreate(args[1:])
536 case "ls":
537 runSidecarsList(args[1:])
538 case "wake":
539 runSidecarsWake(args[1:])
540 case "rm":
541 runSidecarsRm(args[1:])
542 case "logs":
543 runSidecarsLogs(args[1:])
544 default:
545 log.Fatalf("carlos sidecars: unknown subcommand %q\n%s", args[0], sidecarsEnableUsage)
546 }
547}
548