322 lines
9.0 KiB
Go
322 lines
9.0 KiB
Go
package main
|
|
|
|
// coders_cli.go — `privatae-bastion coders ...`: the owner sizes this machine
|
|
// for coder runs (coders.go). Machine-wide, stored in <configDir>/coders/config.json.
|
|
|
|
import (
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"net"
|
|
"os"
|
|
"runtime"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
const codersUsage = `usage:
|
|
privatae-bastion coders set [--max N] [--mem MB] [--cpus F] [--engines a,b|all] [--allow-lan host[:port],...]
|
|
[--image-prefix <registry/path/>|default]
|
|
privatae-bastion coders on | off
|
|
privatae-bastion coders drop [--host H]
|
|
privatae-bastion coders status [--json]`
|
|
|
|
func cmdCoders(args []string) error {
|
|
if len(args) == 0 {
|
|
return errors.New(codersUsage)
|
|
}
|
|
switch args[0] {
|
|
case "set":
|
|
return cmdCodersSet(args[1:])
|
|
case "on":
|
|
c, err := coderTurnOn(loadCoderConfig())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
fmt.Printf("coder runs ON: up to %d at a time, %d MiB and %s CPUs each, engines %s\n",
|
|
c.Max, c.MemPerRun, strconv.FormatFloat(c.CPUsPerRun, 'f', -1, 64), strings.Join(c.Engines, ","))
|
|
return nil
|
|
case "off":
|
|
c := loadCoderConfig()
|
|
c.Enabled = false
|
|
if err := saveCoderConfig(c); err != nil {
|
|
return err
|
|
}
|
|
fmt.Println("coder runs OFF: no new run starts here; runs already going continue until they are stopped or reaped")
|
|
return nil
|
|
case "drop":
|
|
host, rest := takeHostFlag(args[1:])
|
|
if len(rest) != 0 {
|
|
return errors.New(codersUsage)
|
|
}
|
|
return cmdCodersDrop(host)
|
|
case "status":
|
|
asJSON := len(args) > 1 && args[1] == "--json"
|
|
return cmdCodersStatus(asJSON)
|
|
}
|
|
return errors.New(codersUsage)
|
|
}
|
|
|
|
// coderApplySet parses `coders set` flags onto c. Bounds: max 1..100, mem
|
|
// 512..65536, cpus 0.25..NumCPU, engines a subset of the coding engines.
|
|
func coderApplySet(c coderConfig, args []string, numCPU int) (coderConfig, error) {
|
|
if len(args) == 0 {
|
|
return c, errors.New(codersUsage)
|
|
}
|
|
for i := 0; i < len(args); i++ {
|
|
flag := args[i]
|
|
if i+1 >= len(args) {
|
|
return c, fmt.Errorf("%s needs a value", flag)
|
|
}
|
|
v := args[i+1]
|
|
i++
|
|
switch flag {
|
|
case "--max":
|
|
n, err := strconv.Atoi(v)
|
|
if err != nil || n < 1 || n > 100 {
|
|
return c, errors.New("--max must be 1-100")
|
|
}
|
|
c.Max = n
|
|
case "--mem":
|
|
n, err := strconv.Atoi(v)
|
|
if err != nil || n < 512 || n > 65536 {
|
|
return c, errors.New("--mem must be 512-65536 (MiB per run)")
|
|
}
|
|
c.MemPerRun = n
|
|
case "--cpus":
|
|
f, err := strconv.ParseFloat(v, 64)
|
|
if err != nil || f < 0.25 || f > float64(numCPU) {
|
|
return c, fmt.Errorf("--cpus must be 0.25-%d", numCPU)
|
|
}
|
|
c.CPUsPerRun = f
|
|
case "--engines":
|
|
if v == "all" {
|
|
c.Engines = append([]string(nil), coderEngines...)
|
|
break
|
|
}
|
|
var eng []string
|
|
for _, e := range strings.Split(v, ",") {
|
|
e = strings.TrimSpace(e)
|
|
if e == "" {
|
|
continue
|
|
}
|
|
if !isCoderEngine(e) {
|
|
return c, fmt.Errorf("--engines: %q is not a coding engine (%s)", e, strings.Join(coderEngines, ", "))
|
|
}
|
|
if !containsStr(eng, e) {
|
|
eng = append(eng, e)
|
|
}
|
|
}
|
|
if len(eng) == 0 {
|
|
return c, errors.New("--engines names no engine")
|
|
}
|
|
c.Engines = eng
|
|
case "--allow-lan":
|
|
var allow []string
|
|
for _, h := range strings.Split(v, ",") {
|
|
h = strings.TrimSpace(h)
|
|
if h == "" {
|
|
continue
|
|
}
|
|
if strings.ContainsAny(h, " /\t") {
|
|
return c, fmt.Errorf("--allow-lan: %q is not a host[:port]", h)
|
|
}
|
|
if hh, p, err := net.SplitHostPort(h); err == nil {
|
|
if n, err := strconv.Atoi(p); err != nil || n < 1 || n > 65535 || hh == "" {
|
|
return c, fmt.Errorf("--allow-lan: %q has a bad port", h)
|
|
}
|
|
}
|
|
allow = append(allow, h)
|
|
}
|
|
c.AllowLAN = allow
|
|
case "--image-prefix":
|
|
// Where run images may come from: <prefix>harness-<engine>@sha256:...
|
|
// "default" (or the default itself) clears it, so the file holds
|
|
// only a prefix the owner chose.
|
|
if v == "default" || v == coderDefaultImagePrefix {
|
|
c.ImagePrefix = ""
|
|
break
|
|
}
|
|
if why := coderPrefixRefusal(v); why != "" {
|
|
return c, errors.New(why)
|
|
}
|
|
c.ImagePrefix = v
|
|
default:
|
|
return c, fmt.Errorf("unknown flag %s\n%s", flag, codersUsage)
|
|
}
|
|
}
|
|
return c, nil
|
|
}
|
|
|
|
func cmdCodersSet(args []string) error {
|
|
c, err := coderApplySet(loadCoderConfig(), args, runtime.NumCPU())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := saveCoderConfig(c); err != nil {
|
|
return err
|
|
}
|
|
fmt.Println("coder settings saved (on/off unchanged: `privatae-bastion coders on`)")
|
|
return nil
|
|
}
|
|
|
|
// coderTurnOn fills every unset value with its default and enables. max
|
|
// defaults to how many runs fit in the memory budget (coderBudgetMB),
|
|
// clamped to 1..100, and refuses when the machine cannot hold one run.
|
|
func coderTurnOn(c coderConfig) (coderConfig, error) {
|
|
if c.MemPerRun == 0 {
|
|
c.MemPerRun = coderDefaultMemMB
|
|
}
|
|
if c.CPUsPerRun == 0 {
|
|
c.CPUsPerRun = coderDefaultCPUs
|
|
if n := float64(runtime.NumCPU()); c.CPUsPerRun > n {
|
|
c.CPUsPerRun = n
|
|
}
|
|
}
|
|
if len(c.Engines) == 0 {
|
|
c.Engines = append([]string(nil), coderEngines...)
|
|
}
|
|
if c.Max == 0 {
|
|
total, _, ok := coderMemInfo()
|
|
if !ok {
|
|
return c, errors.New("cannot read this machine's memory; set it with `coders set --max N`")
|
|
}
|
|
n := coderBudgetMB(int(total>>20)) / c.MemPerRun
|
|
if n < 1 {
|
|
return c, fmt.Errorf("this machine has %d MiB; one run of %d MiB plus %d MiB for the machine does not fit",
|
|
total>>20, c.MemPerRun, coderReserveMB(int(total>>20)))
|
|
}
|
|
if n > 100 {
|
|
n = 100
|
|
}
|
|
c.Max = n
|
|
}
|
|
c.Enabled = true
|
|
return c, saveCoderConfig(c)
|
|
}
|
|
|
|
// cmdCodersDrop deletes the standing coder grant(s) of every pairing, or of
|
|
// host, and stops those pairings' runs here.
|
|
func cmdCodersDrop(host string) error {
|
|
cfgs, err := loadPairings()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
var targets []*Config
|
|
if host != "" {
|
|
c, err := findPairing(cfgs, host)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if c == nil {
|
|
return fmt.Errorf("no pairing for %q (paired: %s)", host, strings.Join(hostNames(cfgs), ", "))
|
|
}
|
|
targets = []*Config{c}
|
|
} else {
|
|
targets = cfgs
|
|
}
|
|
owners := map[string]bool{}
|
|
for _, c := range targets {
|
|
owners[c.HostName] = true
|
|
ids, err := coderGrantDropAll(c.dcnDir(), "dropped at this machine (coders drop)")
|
|
if err != nil {
|
|
return fmt.Errorf("%s: %w", c.HostName, err)
|
|
}
|
|
fmt.Printf("%s: %d coder grant(s) dropped\n", c.HostName, len(ids))
|
|
}
|
|
rt := coderRuntime()
|
|
stopped, failed := 0, 0
|
|
for _, r := range loadCoderRecords() {
|
|
if !owners[r.Owner] {
|
|
continue
|
|
}
|
|
if rt == "" {
|
|
failed++
|
|
continue
|
|
}
|
|
r := r
|
|
if _, err := coderTeardown(rt, r.RunID, &r); err != nil {
|
|
failed++
|
|
fmt.Fprintf(os.Stderr, "run %s: %v\n", r.RunID, err)
|
|
continue
|
|
}
|
|
stopped++
|
|
}
|
|
fmt.Printf("%d run(s) stopped", stopped)
|
|
if failed > 0 {
|
|
fmt.Printf(", %d could not be confirmed stopped (the reaper retries)", failed)
|
|
}
|
|
fmt.Println()
|
|
return nil
|
|
}
|
|
|
|
func cmdCodersStatus(asJSON bool) error {
|
|
c := loadCoderConfig()
|
|
cfgs, _ := loadPairings()
|
|
type grantView struct {
|
|
Host string `json:"host"`
|
|
Engines []string `json:"engines"`
|
|
Max int `json:"max"`
|
|
Expires string `json:"expires"`
|
|
}
|
|
grants := []grantView{}
|
|
for _, p := range cfgs {
|
|
dir := p.dcnDir()
|
|
f, err := dcnLoadFile(dir)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
for _, rec := range f.Coder {
|
|
raw, err := base64.StdEncoding.DecodeString(rec.TermsCanonical)
|
|
var t coderTerms
|
|
if err != nil || json.Unmarshal(raw, &t) != nil {
|
|
continue
|
|
}
|
|
grants = append(grants, grantView{Host: p.HostName, Engines: t.Engines, Max: t.Max, Expires: t.Expires})
|
|
}
|
|
}
|
|
sort.Slice(grants, func(i, j int) bool { return grants[i].Host < grants[j].Host })
|
|
runs := coderReports("", true)
|
|
if asJSON {
|
|
b, _ := json.MarshalIndent(map[string]any{"config": c, "grants": grants, "runs": runs}, "", " ")
|
|
fmt.Println(string(b))
|
|
return nil
|
|
}
|
|
state := "OFF"
|
|
if c.Enabled {
|
|
state = "ON"
|
|
}
|
|
fmt.Printf("coder runs: %s max %d %d MiB / %s CPUs per run engines %s\n", state, c.Max, c.MemPerRun,
|
|
strconv.FormatFloat(c.CPUsPerRun, 'f', -1, 64), strings.Join(c.Engines, ","))
|
|
if total, _, ok := coderMemInfo(); ok && c.MemPerRun > 0 {
|
|
mb := int(total >> 20)
|
|
fmt.Printf("memory for runs: %d of %d MiB (%d kept for the machine), so %d run(s) of %d MiB at a time\n",
|
|
coderBudgetMB(mb), mb, coderReserveMB(mb), max(coderBudgetMB(mb)/c.MemPerRun, 0), c.MemPerRun)
|
|
}
|
|
if len(c.AllowLAN) > 0 {
|
|
fmt.Printf("LAN hosts runs may reach: %s\n", strings.Join(c.AllowLAN, ", "))
|
|
}
|
|
fmt.Printf("run images: %sharness-<engine>@sha256:<digest>\n", c.imagePrefix())
|
|
for _, g := range grants {
|
|
fmt.Printf("grant: %s engines %s max %d until %s\n", g.Host, strings.Join(g.Engines, ","), g.Max, g.Expires)
|
|
}
|
|
now := time.Now()
|
|
for _, r := range runs {
|
|
left := ""
|
|
if dl, err := time.Parse(time.RFC3339, r.Deadline); err == nil {
|
|
left = dl.Sub(now).Round(time.Minute).String() + " left"
|
|
}
|
|
fmt.Printf("run %s %s %s port %d %s\n", r.RunID, r.Engine, r.State, r.TunnelPort, left)
|
|
if r.StoppedBy != "" {
|
|
fmt.Printf(" stopped to protect this machine: %s\n", r.StoppedBy)
|
|
}
|
|
}
|
|
if len(runs) == 0 {
|
|
fmt.Println("no runs on this machine")
|
|
}
|
|
return nil
|
|
}
|