a73x

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
}