internal/cli/main.go
Ref: Size: 4.0 KiB History
// main.go is the eitri client's command dispatch. It lives here rather than in
// cmd/eitri so it is testable and coverage-gated (arch R14: main packages are
// wiring only).
package cli
import (
"context"
"errors"
"fmt"
"io"
"os"
)
// Usage is the top-level help text, printed by cmd/eitri on ErrUsage.
const Usage = `usage:
eitri init [--token <pat>]
eitri ssh <vm> [ssh args / remote command...]
eitri ca upload [<tenant>] <ca-public-key-file>
eitri --version
init is the guided first run: it proves a token, settles your tenant's SSH CA,
and writes ~/.eitri/config.json, confirming every step before it acts.
env: all optional — each EITRI_* variable overrides the config file, which
overrides the hosted defaults at eitri.sh. init and ca need EITRI_TOKEN (a
PAT) unless one is given another way; ca's tenant is optional (argument,
else EITRI_TENANT, else the token's own). EITRI_TENANT disambiguates a user
CA registered in more than one tenant, and EITRI_GATE pins the gate the
plane would otherwise name itself. EITRI_CA, EITRI_KEY, EITRI_KNOWN_HOSTS
default under ~/.ssh; EITRI_CONFIG moves the config file.`
// ErrUsage marks a bad invocation: the caller prints Usage and exits 2 rather
// than treating it as a runtime failure.
var ErrUsage = errors.New("bad usage")
// Main dispatches the eitri command line (everything after the binary name,
// --version excluded — that stays in cmd/eitri). stdout carries command output;
// prompts and confirmations go to stderr inside the subcommands, except init,
// whose whole output IS the dialogue and so belongs on stdout.
func Main(args []string, stdout io.Writer) error {
if len(args) < 1 {
return ErrUsage
}
switch args[0] {
case "init":
return runInit(args[1:], stdout)
case "ssh":
return runSSH(args[1:], stdout)
case "ca":
return runCA(args[1:], stdout)
default:
return ErrUsage
}
}
const initUsage = "usage: eitri init [--token <pat>]"
// runInit parses init's one flag. A token on the command line is visible in the
// shell's history and in ps, so it is offered rather than required: with no
// --token, init takes EITRI_TOKEN, and with neither it asks, unechoed.
func runInit(args []string, stdout io.Writer) error {
token := ""
for len(args) > 0 {
switch args[0] {
case "-h", "--help":
fmt.Fprintln(stdout, initUsage)
return nil
case "--token":
if len(args) < 2 {
return errors.New(initUsage)
}
token, args = args[1], args[2:]
default:
return errors.New(initUsage)
}
}
env, err := FromEnv()
if err != nil {
return err
}
cfgPath, err := ConfigPath()
if err != nil {
return err
}
return RunInit(context.Background(), env, cfgPath, token, os.Stdin, stdout)
}
func runSSH(args []string, stdout io.Writer) error {
if len(args) >= 1 && (args[0] == "-h" || args[0] == "--help") {
fmt.Fprintln(stdout, "usage: eitri ssh <vm> [ssh args / remote command...]")
return nil
}
if len(args) < 1 {
return fmt.Errorf("usage: eitri ssh <vm> [ssh args / remote command...]")
}
env, err := FromEnv()
if err != nil {
return err
}
return RunSSH(context.Background(), env, args[0], args[1:])
}
func runCA(args []string, stdout io.Writer) error {
if len(args) < 1 || args[0] != "upload" {
return fmt.Errorf("usage: eitri ca upload [<tenant>] <ca-public-key-file>")
}
rest := args[1:]
if len(rest) < 1 || len(rest) > 2 {
return fmt.Errorf("usage: eitri ca upload [<tenant>] <ca-public-key-file>")
}
// One arg: the token names the tenant, unless EITRI_TENANT pins one. Two
// args: the leading positional pins the tenant explicitly (for a token that
// can act for more than one).
tenant, pub := os.Getenv("EITRI_TENANT"), rest[0]
if len(rest) == 2 {
tenant, pub = rest[0], rest[1]
}
token := os.Getenv("EITRI_TOKEN")
if token == "" {
return fmt.Errorf("set EITRI_TOKEN (a personal access token)")
}
env, err := FromEnv()
if err != nil {
return err
}
out, err := UploadUserCA(context.Background(), env.URL, token, tenant, pub)
if err != nil {
return err
}
fmt.Fprintln(stdout, out)
return nil
}