cmd/gitbay-runner/config.go
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}