cmd/gitbay-runner/config.go

f3f7422f62a73bb798f9cf9c5f4c344212a27f60
gitbay/cmd/gitbay-runner/config.go history · blame · raw

109 lines · 3842 bytes

  1package main
  2
  3import (
  4	"errors"
  5	"flag"
  6	"fmt"
  7	"os"
  8	"path/filepath"
  9	"strings"
 10
 11	"github.com/BurntSushi/toml"
 12)
 13
 14// The runner takes everything as flags, which does not work under a
 15// service manager. config.toml in the config directory carries the same
 16// names; a flag on the command line overrides it (#184).
 17
 18func configDir() string {
 19	if x := os.Getenv("XDG_CONFIG_HOME"); x != "" {
 20		return filepath.Join(x, "gitbay-runner")
 21	}
 22	return filepath.Join(os.Getenv("HOME"), ".config", "gitbay-runner")
 23}
 24
 25func defaultConfigPath() string { return filepath.Join(configDir(), "config.toml") }
 26
 27// configPathFromArgs finds -config before the flag set is parsed, since
 28// the file's values must be set before parsing for flags to override them.
 29func configPathFromArgs(args []string, def string) string {
 30	for i, a := range args {
 31		a = strings.TrimPrefix(a, "-")
 32		if a == "-config" || a == "config" {
 33			if i+1 < len(args) {
 34				return args[i+1]
 35			}
 36		}
 37		if v, ok := strings.CutPrefix(a, "config="); ok {
 38			return v
 39		}
 40		if v, ok := strings.CutPrefix(a, "-config="); ok {
 41			return v
 42		}
 43	}
 44	return def
 45}
 46
 47// configKeys is every key the file may carry: the flag names.
 48var configKeys = map[string]bool{"remote": true, "ssh-opts": true, "clone-base": true, "workdir": true,
 49	"poll": true, "timeout": true, "repos": true, "jobs": true, "image": true, "isolation": true,
 50	"memory": true, "cpus": true, "untrusted": true, "identity": true}
 51
 52// loadConfig reads path into flag name → value. Absent file: found is
 53// false and there is no error. An unknown key is an error, not a typo
 54// the runner silently ignores.
 55func loadConfig(path string) (values map[string]string, found bool, err error) {
 56	var raw map[string]any
 57	if _, err := toml.DecodeFile(path, &raw); errors.Is(err, os.ErrNotExist) {
 58		return nil, false, nil
 59	} else if err != nil {
 60		return nil, true, fmt.Errorf("%s: %w", path, err)
 61	}
 62	values = map[string]string{}
 63	for k, v := range raw {
 64		if !configKeys[k] {
 65			return nil, true, fmt.Errorf("%s: unknown key %s", path, k)
 66		}
 67		values[k] = fmt.Sprint(v)
 68	}
 69	return values, true, nil
 70}
 71
 72// applyConfig sets each value on the flag set, which is what parsing the
 73// command line would do; parse afterwards and the command line wins.
 74func applyConfig(fs *flag.FlagSet, values map[string]string) error {
 75	for k, v := range values {
 76		if fs.Lookup(k) == nil {
 77			return fmt.Errorf("config: unknown key %s", k)
 78		}
 79		if err := fs.Set(k, v); err != nil {
 80			return fmt.Errorf("config: %s: %w", k, err)
 81		}
 82	}
 83	return nil
 84}
 85
 86// identityOpts is what makes ssh and git use the runner's own key and no
 87// other. On a laptop the user's ~/.ssh/config names their full-scope key
 88// for the instance, and IdentitiesOnly keeps identities from the config,
 89// so the runner would authenticate as that key and, on an admin's
 90// machine, claim every repository's builds. -F /dev/null drops the
 91// config; known_hosts is unaffected, and a host seen for the first time
 92// is accepted, since a service cannot answer a prompt.
 93func identityOpts(path string) []string {
 94	if path == "" {
 95		return nil
 96	}
 97	return []string{"-F", "/dev/null", "-i", path, "-o", "IdentitiesOnly=yes", "-o", "StrictHostKeyChecking=accept-new"}
 98}
 99
100// sshOptions is every ssh invocation's option list: the identity, the
101// operator's -ssh-opts, then a bound on dead connections. Without one a
102// claim whose TCP session died under it (a laptop's network dropping)
103// blocks the poll loop indefinitely; ssh took thirteen hours on one
104// before a restart. ssh honours the first value of an option, so the
105// operator's come first and override these.
106func sshOptions(identity string, extra []string) []string {
107	opts := append(identityOpts(identity), extra...)
108	return append(opts, "-o", "ConnectTimeout=10", "-o", "ServerAliveInterval=15", "-o", "ServerAliveCountMax=3")
109}