| new file mode 100644 |
| index 000000000..4ccab29c3 |
| --- /dev/null |
| +++ b/sidecars_cli.go |
| @@ -0,0 +1,529 @@ |
| +// The `carlos sidecars` verb family (Task 14, design: sidecar members): a |
| +// sidecar is a process the platform runs that serves no requests and that |
| +// nothing can dial, so its whole shape follows the instance family's |
| +// deliberately, minus the fields a sidecar has no use for. See |
| +// docs/superpowers/specs/2026-08-17-sidecar-members-design.md §11. |
| +// |
| +// - `enable` — the app's opt-in to sidecar provisioning. Two modes, |
| +// resolved credential-first exactly as instances enable's is |
| +// (instances_cli.go): store mode writes apps/<sqid>/<app>/sidecars.json |
| +// via provision.PutSidecarManifest with every field under the |
| +// operator's hand; API mode asks the console, which lets a member's own |
| +// args/placement/channel/idle/max-run/hibernate through untouched while |
| +// refusing -pool and -regions — those are operator, store-mode-only |
| +// policy fields (apisidecars.go's handleAPISidecarEnable). |
| +// - `create` / `ls` / `wake` / `rm` — the CLI front over the console's |
| +// sidecar record verbs. API mode ONLY: a record is a console-authorized |
| +// write, and there is deliberately NO box-side equivalent — a sidecar's |
| +// host is derived under the reserved .sidecar.internal suffix, and |
| +// `carlos add` refuses to mint one by hand (its reserved-suffix rail). |
| +// - `logs` delegates to the existing `carlos logs -host <h>` path for the |
| +// sidecar's own derived host: logs key on host, not on Kind (design |
| +// §9.3), so there is no separate sidecar-log machinery to keep in sync. |
| +// |
| +// Regions: `-regions` on `enable` takes PLAIN REGION CODES ONLY, never a |
| +// fleet placement target. A fleet-placed app's sidecars inherit the app's |
| +// own placement (set by `carlos apps place`), which overrides whatever the |
| +// manifest's Regions allow-list or a create request says; that allow-list is |
| +// only ever consulted for an UNPLACED app (Task 12 review finding). Offering |
| +// `-regions ie,fleet/eleven` here would be unsatisfiable and doubly |
| +// misleading, so this flag's help text points at `carlos apps place` |
| +// instead of pretending to accept fleet syntax. |
| +package main |
| + |
| +import ( |
| + "context" |
| + "flag" |
| + "fmt" |
| + "log" |
| + "os" |
| + "strings" |
| + "time" |
| + |
| + "github.com/carlosframework/platform/internal/provision" |
| + "github.com/carlosframework/platform/internal/release" |
| +) |
| + |
| +const sidecarsEnableUsage = `usage: carlos sidecars enable -app <a> [flags] |
| + |
| +Signed-in mode (carlos auth login; no CARLOS_DEPLOYMENT_* env): the console |
| +writes the manifest under your own membership. -pool and -regions are |
| +operator, store-mode flags and are refused here. |
| + |
| +Store mode (CARLOS_DEPLOYMENT_BUCKET/CARLOS_DEPLOYMENT_DIR set): every field |
| +is the operator's call, and this write REPLACES the whole manifest — the |
| +same full-overwrite CAS upsert "carlos instances enable" uses, so a field |
| +you omit here resets to its own default, not to whatever a previous enable |
| +left it at. |
| + |
| +flags: |
| + -app <a> app whose sidecars this enables (required) |
| + -args "argv..." argv the activator execs the app's own binary |
| + with, whitespace-split via strings.Fields — shell- |
| + word semantics (quoting, escaping) are |
| + deliberately NOT supported, same contract as |
| + -seed on "carlos instances enable" |
| + -placement <p> "any" (default) or "no-inbound" — no-inbound |
| + mints only on boxes declaring CARLOS_INBOUND=none |
| + (design §8) |
| + -pool <p> pool stamped into every record at create (store |
| + mode only; default: "", the shared pool) |
| + -channel <c> release channel stamped into every record at |
| + create (store-mode default "stable"; signed-in |
| + mode: omitted = preserve what the manifest holds) |
| + -regions <r1,r2,..> allow-list for a record's region, PLAIN REGION |
| + CODES ONLY (store mode only); empty (default) = |
| + any shape-valid region. Consulted only for an |
| + UNPLACED app — a fleet-placed app's sidecars |
| + inherit the app's own placement instead, set |
| + with "carlos apps place", which overrides this |
| + list (and anything a create request says) |
| + entirely. There is no fleet syntax here. |
| + -idle <dur> idle timeout before a hibernating sidecar sleeps |
| + (default 5m; a Go duration — 5m, 1h — sent to |
| + the wire as whole seconds, never a duration |
| + string) |
| + -max-run <dur> max run duration before the sweeper stops a |
| + sidecar regardless of idle (default 1h; same |
| + duration-to-seconds conversion as -idle) |
| + -no-hibernate this app's sidecars never hibernate (default: |
| + they do) |
| + -account <a> account (sqid or name) the app belongs to |
| + (default "ops") |
| + -account-id <id> account sqid, matched as a sqid ONLY — supersedes |
| + -account |
| + -console <origin> console to enable through (signed-in mode only; |
| + default: the only logged-in one) |
| + |
| +Writing this manifest is the app's opt-in to sidecar provisioning: absence |
| +means the feature does not exist for the app, and the API refuses creates |
| +until this is set (design §3.1, §11). |
| +` |
| + |
| +func runSidecarsEnable(args []string) { |
| + fs := flag.NewFlagSet("sidecars enable", flag.ExitOnError) |
| + app := fs.String("app", "", "app whose sidecars this enables") |
| + argsFlag := fs.String("args", "", "argv the activator execs the app's own binary with, whitespace-split (strings.Fields)") |
| + placement := fs.String("placement", "", `"any" (default) or "no-inbound"`) |
| + pool := fs.String("pool", "", "pool stamped into every record at create (store mode only)") |
| + channel := fs.String("channel", "", "release channel stamped into every record at create") |
| + 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") |
| + idle := fs.String("idle", "", "idle timeout before a hibernating sidecar sleeps (default 5m; Go duration)") |
| + maxRun := fs.String("max-run", "", "max run duration before the sweeper stops a sidecar (default 1h; Go duration)") |
| + noHibernate := fs.Bool("no-hibernate", false, "this app's sidecars never hibernate (default: they do)") |
| + consoleFlag := fs.String("console", "", "console to enable through (signed-in mode only; default: the only logged-in one)") |
| + acctRaw, acctIDRaw, acctOf := accountFlags(fs) |
| + fs.Parse(args) |
| + if *app != "" && !release.ValidApp(*app) { |
| + log.Fatalf("carlos sidecars enable: %q is not a valid app name", *app) |
| + } |
| + set := map[string]bool{} |
| + fs.Visit(func(f *flag.Flag) { set[f.Name] = true }) |
| + |
| + sess, err := resolveMode(*consoleFlag) |
| + if err != nil { |
| + log.Fatal(err) |
| + } |
| + if sess != nil { |
| + // Signed-in mode. -pool/-regions are refused rather than ignored — |
| + // a flag that silently did nothing would read as policy applied. |
| + for _, f := range []string{"pool", "regions"} { |
| + if set[f] { |
| + 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) |
| + } |
| + } |
| + var channelReq *string |
| + if set["channel"] { |
| + channelReq = channel |
| + } |
| + var argsReq *[]string |
| + if set["args"] { |
| + // parseArgv("") is an empty (non-nil after this) argv: an |
| + // explicit -args "" clears the manifest's args, matching |
| + // -channel's explicit-vs-absent split. |
| + argv := parseArgv(*argsFlag) |
| + if argv == nil { |
| + argv = []string{} |
| + } |
| + argsReq = &argv |
| + } |
| + var placementReq *string |
| + if set["placement"] { |
| + placementReq = placement |
| + } |
| + var idleReq *int |
| + if set["idle"] { |
| + secs, err := parseDurationSeconds(*idle) |
| + if err != nil { |
| + log.Fatalf("carlos sidecars enable: -idle: %v", err) |
| + } |
| + idleReq = &secs |
| + } |
| + var maxRunReq *int |
| + if set["max-run"] { |
| + secs, err := parseDurationSeconds(*maxRun) |
| + if err != nil { |
| + log.Fatalf("carlos sidecars enable: -max-run: %v", err) |
| + } |
| + maxRunReq = &secs |
| + } |
| + var hibernateReq *bool |
| + if *noHibernate { |
| + no := false |
| + hibernateReq = &no |
| + } |
| + printUsing(stdout, sess) |
| + acct := resolvedAccount(sess, acctRaw, acctIDRaw) |
| + app2 := resolvedApp(sess, acct, *app, sidecarsEnableUsage) |
| + m, err := sess.sidecarsEnable(context.Background(), acct, app2, |
| + channelReq, argsReq, placementReq, idleReq, maxRunReq, hibernateReq) |
| + if err != nil { |
| + log.Fatalf("sidecars enable: %v", err) |
| + } |
| + fmt.Fprintf(stdout, "%s for %s (channel %s, placement %s)\n", |
| + colorFor(stdout).ok("enabled sidecars"), m.App, m.Channel, m.Placement) |
| + mutedf(stdout, " first sidecar: carlos sidecars create -app %s <name>", m.App) |
| + return |
| + } |
| + |
| + // Store mode — the operator path: every field this manifest carries is |
| + // set fresh on every call (PutSidecarManifest is a full-overwrite CAS |
| + // upsert, not a patch), exactly as instances enable's own store-mode |
| + // write is. |
| + if *app == "" { |
| + log.Fatal(sidecarsEnableUsage) |
| + } |
| + if *channel == "" && !set["channel"] { |
| + *channel = "stable" |
| + } |
| + if !provision.ValidPool(*pool) { |
| + log.Fatalf("carlos sidecars enable: %q is not a valid pool", *pool) |
| + } |
| + if !release.ValidChannel(*channel) { |
| + log.Fatalf("carlos sidecars enable: %q is not a valid channel", *channel) |
| + } |
| + switch *placement { |
| + case "", provision.PlacementAny, provision.PlacementNoInbound: |
| + default: |
| + log.Fatalf(`carlos sidecars enable: -placement %q: want "any" or "no-inbound"`, *placement) |
| + } |
| + var regionList []string |
| + if *regions != "" { |
| + for _, r := range strings.Split(*regions, ",") { |
| + r = strings.TrimSpace(r) |
| + if r == "" || !provision.ValidRegion(r) { |
| + 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) |
| + } |
| + regionList = append(regionList, r) |
| + } |
| + } |
| + var idleSecs int |
| + if set["idle"] { |
| + var err error |
| + idleSecs, err = parseDurationSeconds(*idle) |
| + if err != nil { |
| + log.Fatalf("carlos sidecars enable: -idle: %v", err) |
| + } |
| + } |
| + var maxRunSecs int |
| + if set["max-run"] { |
| + var err error |
| + maxRunSecs, err = parseDurationSeconds(*maxRun) |
| + if err != nil { |
| + log.Fatalf("carlos sidecars enable: -max-run: %v", err) |
| + } |
| + } |
| + m := provision.SidecarManifest{ |
| + Schema: 1, App: *app, Pool: *pool, Channel: *channel, Placement: *placement, |
| + Regions: regionList, Args: parseArgv(*argsFlag), |
| + IdleSeconds: idleSecs, MaxRunSeconds: maxRunSecs, |
| + } |
| + if *noHibernate { |
| + no := false |
| + m.Hibernate = &no |
| + } |
| + if err := sidecarsEnable(context.Background(), openStore(), acctOf(), *app, m); err != nil { |
| + log.Fatal(err) |
| + } |
| + fmt.Printf("%s for %s (channel %s)\n", colorFor(os.Stdout).ok("enabled sidecars"), *app, *channel) |
| +} |
| + |
| +// parseArgv splits -args's raw value on whitespace (strings.Fields) into |
| +// the manifest's Args argv — instances enable's -seed contract |
| +// (instances_cli.go's parseSeed), carried over unchanged: quoting and |
| +// escaping are deliberately NOT supported, and anything richer belongs in |
| +// the app's own argv parsing. |
| +func parseArgv(raw string) []string { |
| + return strings.Fields(raw) |
| +} |
| + |
| +// parseDurationSeconds parses a Go duration flag value (e.g. "5m", "1h") |
| +// into whole seconds for the wire, which carries idle/max-run timeouts as |
| +// plain integer seconds, never a duration string (design §4.1, |
| +// provision.SidecarManifest.IdleSeconds/MaxRunSeconds). A negative duration |
| +// is refused here rather than reaching the console — or, in store mode, the |
| +// bucket — as a nonsense wire value. |
| +func parseDurationSeconds(raw string) (int, error) { |
| + d, err := time.ParseDuration(raw) |
| + if err != nil { |
| + return 0, fmt.Errorf("%q is not a valid duration (e.g. 5m, 1h): %w", raw, err) |
| + } |
| + if d < 0 { |
| + return 0, fmt.Errorf("%q must not be negative", raw) |
| + } |
| + return int(d / time.Second), nil |
| +} |
| + |
| +// sidecarsEnable is the core store-mode `carlos sidecars enable` write — |
| +// separated from flag parsing/validation above so a test can call it |
| +// directly against a release.Store, instancesEnable's own precedent |
| +// (instances_cli.go). |
| +func sidecarsEnable(ctx context.Context, s release.Store, acct, app string, m provision.SidecarManifest) error { |
| + return provision.PutSidecarManifest(ctx, s, acct, app, m) |
| +} |
| + |
| +const sidecarsRecordsUsage = `usage: carlos sidecars create -app <a> [-region <r>] [-channel <c>] <name> |
| + carlos sidecars ls -app <a> [-long] |
| + carlos sidecars wake -app <a> <name> |
| + carlos sidecars rm -app <a> <name> |
| + carlos sidecars logs -app <a> <name> |
| + |
| +Signed-in only (carlos auth login): sidecar records are console-authorized |
| +writes — the console checks the name and region against the app's manifest |
| +(and a fleet-placed app's own placement, which overrides both) and a box's |
| +sidecar reconcile pass turns the record into a running process. Common |
| +flags: |
| + |
| + -account <a> account (sqid or name; default "ops") |
| + -account-id <id> account sqid, matched as a sqid ONLY |
| + -console <origin> console to act through (default: the only logged-in one) |
| + |
| +create-only flags: |
| + |
| + -region <r> region code for the record, subject to the manifest's |
| + allow-list — or overridden entirely by the app's own |
| + placement, when it has one (carlos apps place) |
| + -channel <c> release channel the sidecar follows (default: the |
| + manifest's, then "stable") |
| + |
| +ls-only flags: |
| + |
| + -long print the synthetic host (<name>.<app>.<sqid>.sidecar.internal) |
| + instead of the short name |
| + |
| +There is no set-upstreams for a sidecar: it has no upstream set, and |
| +nothing dials it. |
| +` |
| + |
| +// requireSidecarsAPISession resolves signed-in mode for the sidecar record |
| +// verbs, refusing direct mode by NAME — sidecars' own advice, never |
| +// instances' (Task 12 review finding, binding on this task): `carlos add` |
| +// explicitly refuses to mint a sidecar host (its reserved-suffix rail), so |
| +// a refusal here that pointed at that verb would send an operator |
| +// somewhere that cannot help them. There is no box-side equivalent for a |
| +// sidecar record at all — this family is console-authorized only. |
| +func requireSidecarsAPISession(verb, consoleFlag string) *apiSession { |
| + sess, err := resolveMode(consoleFlag) |
| + if err != nil { |
| + log.Fatal(err) |
| + } |
| + if sess == nil { |
| + log.Fatalf("carlos sidecars %s works through a console, and this shell is in direct bucket mode\n"+ |
| + "(CARLOS_DEPLOYMENT_BUCKET/CARLOS_DEPLOYMENT_DIR is set). Sidecar records are console-authorized\n"+ |
| + "writes with no direct-bucket equivalent; unset the store env and run: carlos auth login -console <url>.\n"+ |
| + "There is no box-side way to mint a sidecar host by hand — carlos add refuses the reserved suffix", verb) |
| + } |
| + return sess |
| +} |
| + |
| +func runSidecarsCreate(args []string) { |
| + fs := flag.NewFlagSet("sidecars create", flag.ExitOnError) |
| + app := fs.String("app", "", "app the sidecar belongs to") |
| + region := fs.String("region", "", "region code for the record (subject to the manifest's allow-list, or the app's own placement)") |
| + channel := fs.String("channel", "", "release channel the sidecar follows (default: the manifest's, then stable)") |
| + consoleFlag := fs.String("console", "", "console to create through (default: the only logged-in one)") |
| + acctRaw := fs.String("account", "", "account (sqid or name) the app belongs to (default: your only account, or the configured/chosen one)") |
| + acctIDRaw := fs.String("account-id", "", "account sqid, matched as a sqid ONLY — supersedes -account") |
| + fs.Parse(args) |
| + if fs.NArg() != 1 || strings.TrimSpace(fs.Arg(0)) == "" { |
| + log.Fatal(sidecarsRecordsUsage) |
| + } |
| + name := strings.TrimSpace(fs.Arg(0)) |
| + sess := requireSidecarsAPISession("create", *consoleFlag) |
| + printUsing(stdout, sess) |
| + acct := resolvedAccount(sess, acctRaw, acctIDRaw) |
| + app2 := resolvedApp(sess, acct, *app, sidecarsRecordsUsage) |
| + rec, err := sess.sidecarCreate(context.Background(), acct, app2, name, strings.TrimSpace(*region), strings.TrimSpace(*channel)) |
| + if err != nil { |
| + log.Fatalf("sidecars create: %v", err) |
| + } |
| + fmt.Fprintf(stdout, "%s %s (channel %s)\n", colorFor(stdout).ok("created sidecar"), rec.Name, rec.Channel) |
| + mutedf(stdout, " host: %s — it comes live on the next reconcile pass on its box", rec.Host) |
| +} |
| + |
| +// sidecarVersionCell renders `ls`'s VERSION column: the raw running version |
| +// when the sidecar has reported one via /v1/ready, or the literal |
| +// "unknown" when it never has — the honesty rule this branch was built |
| +// around (design §9.2, binding on this task): a stamp that overclaims is |
| +// worse than an absent one, so this must NEVER render the record's own |
| +// Channel (the desired version) as a stand-in for a version nobody has |
| +// confirmed is actually running. |
| +func sidecarVersionCell(running string) string { |
| + if running == "" { |
| + return "unknown" |
| + } |
| + return running |
| +} |
| + |
| +// sidecarPlacementCell renders a record's placement, defaulting the same |
| +// way provision.SidecarManifest.PlacementOrDefault does server-side: empty |
| +// means "any". |
| +func sidecarPlacementCell(placement string) string { |
| + if placement == "" { |
| + return provision.PlacementAny |
| + } |
| + return placement |
| +} |
| + |
| +func runSidecarsList(args []string) { |
| + fs := flag.NewFlagSet("sidecars ls", flag.ExitOnError) |
| + app := fs.String("app", "", "app whose sidecar records to list") |
| + long := fs.Bool("long", false, "print the synthetic host instead of the short name") |
| + consoleFlag := fs.String("console", "", "console to list through (default: the only logged-in one)") |
| + acctRaw := fs.String("account", "", "account (sqid or name) the app belongs to (default: your only account, or the configured/chosen one)") |
| + acctIDRaw := fs.String("account-id", "", "account sqid, matched as a sqid ONLY — supersedes -account") |
| + fs.Parse(args) |
| + sess := requireSidecarsAPISession("ls", *consoleFlag) |
| + acct := resolvedAccount(sess, acctRaw, acctIDRaw) |
| + app2 := resolvedApp(sess, acct, *app, sidecarsRecordsUsage) |
| + recs, truncated, err := sess.sidecarList(context.Background(), acct, app2) |
| + if err != nil { |
| + log.Fatalf("sidecars ls: %v", err) |
| + } |
| + w := newTable(stdout) |
| + idCol := "NAME" |
| + if *long { |
| + idCol = "HOST" |
| + } |
| + fmt.Fprintln(w, idCol+"\tREGION\tCHANNEL\tPOOL\tPLACEMENT\tVERSION\tCREATED\tBY") |
| + for _, r := range recs { |
| + id := r.Name |
| + if *long { |
| + id = r.Host |
| + } |
| + fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n", |
| + id, r.Region, r.Channel, r.Pool, sidecarPlacementCell(r.Placement), |
| + sidecarVersionCell(r.RunningVersion), r.CreatedAt, r.CreatedBy) |
| + } |
| + w.Flush() |
| + if truncated { |
| + mutedf(stdout, "(truncated at 500 records)") |
| + } |
| +} |
| + |
| +func runSidecarsWake(args []string) { |
| + fs := flag.NewFlagSet("sidecars wake", flag.ExitOnError) |
| + app := fs.String("app", "", "app the sidecar belongs to") |
| + consoleFlag := fs.String("console", "", "console to wake through (default: the only logged-in one)") |
| + acctRaw := fs.String("account", "", "account (sqid or name) the app belongs to (default: your only account, or the configured/chosen one)") |
| + acctIDRaw := fs.String("account-id", "", "account sqid, matched as a sqid ONLY — supersedes -account") |
| + fs.Parse(args) |
| + if fs.NArg() != 1 || strings.TrimSpace(fs.Arg(0)) == "" { |
| + log.Fatal(sidecarsRecordsUsage) |
| + } |
| + name := strings.TrimSpace(fs.Arg(0)) |
| + sess := requireSidecarsAPISession("wake", *consoleFlag) |
| + printUsing(stdout, sess) |
| + acct := resolvedAccount(sess, acctRaw, acctIDRaw) |
| + app2 := resolvedApp(sess, acct, *app, sidecarsRecordsUsage) |
| + res, err := sess.sidecarWake(context.Background(), acct, app2, name) |
| + if err != nil { |
| + log.Fatalf("sidecars wake: %v", err) |
| + } |
| + fmt.Fprintf(stdout, "%s %s\n", colorFor(stdout).ok("requested wake for"), name) |
| + mutedf(stdout, " host: %s — the box notices within seconds of the next serial move", res.Host) |
| +} |
| + |
| +func runSidecarsRm(args []string) { |
| + fs := flag.NewFlagSet("sidecars rm", flag.ExitOnError) |
| + app := fs.String("app", "", "app the sidecar belongs to") |
| + consoleFlag := fs.String("console", "", "console to delete through (default: the only logged-in one)") |
| + acctRaw := fs.String("account", "", "account (sqid or name) the app belongs to (default: your only account, or the configured/chosen one)") |
| + acctIDRaw := fs.String("account-id", "", "account sqid, matched as a sqid ONLY — supersedes -account") |
| + fs.Parse(args) |
| + if fs.NArg() != 1 || strings.TrimSpace(fs.Arg(0)) == "" { |
| + log.Fatal(sidecarsRecordsUsage) |
| + } |
| + name := strings.TrimSpace(fs.Arg(0)) |
| + sess := requireSidecarsAPISession("rm", *consoleFlag) |
| + printUsing(stdout, sess) |
| + acct := resolvedAccount(sess, acctRaw, acctIDRaw) |
| + app2 := resolvedApp(sess, acct, *app, sidecarsRecordsUsage) |
| + if err := sess.sidecarDelete(context.Background(), acct, app2, name); err != nil { |
| + log.Fatalf("sidecars rm: %v", err) |
| + } |
| + fmt.Fprintf(stdout, "%s %s — its process stops on the next reconcile pass; parked data stays\n", |
| + colorFor(stdout).ok("deleted sidecar"), name) |
| +} |
| + |
| +// runSidecarsLogs delegates to the existing `carlos logs` path for the |
| +// sidecar's own derived host: logs key on host, not on Kind (design §9.3), |
| +// so there is no separate sidecar-log machinery to keep in sync. The name |
| +// is resolved to its host through sidecarGet rather than computed locally, |
| +// so this client never has to re-derive provision.SidecarHost's shape. |
| +func runSidecarsLogs(args []string) { |
| + fs := flag.NewFlagSet("sidecars logs", flag.ExitOnError) |
| + app := fs.String("app", "", "app the sidecar belongs to") |
| + consoleFlag := fs.String("console", "", "console to read through (default: the only logged-in one)") |
| + acctRaw := fs.String("account", "", "account (sqid or name) the app belongs to (default: your only account, or the configured/chosen one)") |
| + acctIDRaw := fs.String("account-id", "", "account sqid, matched as a sqid ONLY — supersedes -account") |
| + fs.Parse(args) |
| + if fs.NArg() != 1 || strings.TrimSpace(fs.Arg(0)) == "" { |
| + log.Fatal(sidecarsRecordsUsage) |
| + } |
| + name := strings.TrimSpace(fs.Arg(0)) |
| + sess := requireSidecarsAPISession("logs", *consoleFlag) |
| + acct := resolvedAccount(sess, acctRaw, acctIDRaw) |
| + app2 := resolvedApp(sess, acct, *app, sidecarsRecordsUsage) |
| + rec, err := sess.sidecarGet(context.Background(), acct, app2, name) |
| + if err != nil { |
| + log.Fatalf("sidecars logs: %v", err) |
| + } |
| + logsArgs := []string{"-app", app2, "-host", rec.Host} |
| + if *consoleFlag != "" { |
| + logsArgs = append(logsArgs, "-console", *consoleFlag) |
| + } |
| + if *acctRaw != "" { |
| + logsArgs = append(logsArgs, "-account", *acctRaw) |
| + } |
| + if *acctIDRaw != "" { |
| + logsArgs = append(logsArgs, "-account-id", *acctIDRaw) |
| + } |
| + runLogs(logsArgs) |
| +} |
| + |
| +// runSidecars dispatches carlos sidecars' subcommand words BEFORE any flag |
| +// parsing — runInstances' own contract (cli.go): a subcommand word is not |
| +// a flag. Unlike instances, every word here is required; there is no bare |
| +// box-local listing form for a sidecar (it has no route to list). |
| +func runSidecars(args []string) { |
| + if len(args) == 0 { |
| + log.Fatal(sidecarsEnableUsage) |
| + } |
| + switch args[0] { |
| + case "enable": |
| + runSidecarsEnable(args[1:]) |
| + case "create": |
| + runSidecarsCreate(args[1:]) |
| + case "ls": |
| + runSidecarsList(args[1:]) |
| + case "wake": |
| + runSidecarsWake(args[1:]) |
| + case "rm": |
| + runSidecarsRm(args[1:]) |
| + case "logs": |
| + runSidecarsLogs(args[1:]) |
| + default: |
| + log.Fatalf("carlos sidecars: unknown subcommand %q\n%s", args[0], sidecarsEnableUsage) |
| + } |
| +} |