cmd/gitbay-runner/config.go

98 lines · 3219 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}