From 4a7fc55c92f02927005baa58fd7bdfab539ac3bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C5=99emysl=20Eric=20Janouch?= Date: Thu, 26 Dec 2024 12:02:50 +0100 Subject: [PATCH] Runtime configuration changes Through an RPC command, because systemd documentation told us to. --- acid.adoc | 2 ++ acid.go | 91 +++++++++++++++++++++++++++++++++++++------------------ 2 files changed, 64 insertions(+), 29 deletions(-) diff --git a/acid.adoc b/acid.adoc index 9d3cf67..10c3c57 100644 --- a/acid.adoc +++ b/acid.adoc @@ -37,6 +37,8 @@ Commands *restart* [_ID_]...:: Schedule tasks with the given IDs to be rerun. Run this command without arguments to pick up external database changes. +*reload*:: + Reload configuration. Configuration ------------- diff --git a/acid.go b/acid.go index db59c64..2a74b84 100644 --- a/acid.go +++ b/acid.go @@ -26,6 +26,7 @@ import ( "strconv" "strings" "sync" + "sync/atomic" "syscall" ttemplate "text/template" "time" @@ -40,9 +41,9 @@ var ( projectName = "acid" projectVersion = "?" - gConfig Config = Config{Listen: ":http"} - gNotifyScript *ttemplate.Template - gDB *sql.DB + gConfigPath string + gConfig atomic.Pointer[Config] + gDB *sql.DB gNotifierSignal = make(chan struct{}, 1) gExecutorSignal = make(chan struct{}, 1) @@ -52,6 +53,8 @@ var ( gRunning = make(map[int64]*RunningTask) ) +func getConfig() *Config { return gConfig.Load() } + // --- Config ------------------------------------------------------------------ type Config struct { @@ -65,6 +68,8 @@ type Config struct { Runners map[string]ConfigRunner `yaml:"runners"` // script runners Projects map[string]ConfigProject `yaml:"projects"` // configured projects + + notifyTemplate *ttemplate.Template } type ConfigRunner struct { @@ -86,8 +91,9 @@ type ConfigProject struct { func (cf *ConfigProject) AutomaticRunners() (runners []string) { // We pass through unknown runner names, // so that they can cause reference errors later. + config := getConfig() for runner := range cf.Runners { - if r, _ := gConfig.Runners[runner]; !r.Manual { + if r, _ := config.Runners[runner]; !r.Manual { runners = append(runners, runner) } } @@ -102,17 +108,28 @@ type ConfigProjectRunner struct { Timeout string `yaml:"timeout"` // timeout duration } -func parseConfig(path string) error { - if f, err := os.Open(path); err != nil { +// loadConfig reloads configuration. +// Beware that changes do not get applied globally at the same moment. +func loadConfig() error { + new := &Config{} + if f, err := os.Open(gConfigPath); err != nil { return err - } else if err = yaml.NewDecoder(f).Decode(&gConfig); err != nil { + } else if err = yaml.NewDecoder(f).Decode(new); err != nil { return err } + if old := getConfig(); old != nil && old.DB != new.DB { + return fmt.Errorf("the database file cannot be changed in runtime") + } var err error - gNotifyScript, err = - ttemplate.New("notify").Funcs(shellFuncs).Parse(gConfig.Notify) - return err + new.notifyTemplate, err = + ttemplate.New("notify").Funcs(shellFuncs).Parse(new.Notify) + if err != nil { + return err + } + + gConfig.Store(new) + return nil } var shellFuncs = ttemplate.FuncMap{ @@ -137,7 +154,7 @@ var shellFuncs = ttemplate.FuncMap{ // --- Utilities --------------------------------------------------------------- func giteaSign(b []byte) string { - payloadHmac := hmac.New(sha256.New, []byte(gConfig.Secret)) + payloadHmac := hmac.New(sha256.New, []byte(getConfig().Secret)) payloadHmac.Write(b) return hex.EncodeToString(payloadHmac.Sum(nil)) } @@ -145,9 +162,9 @@ func giteaSign(b []byte) string { func giteaNewRequest(ctx context.Context, method, path string, body io.Reader) ( *http.Request, error) { req, err := http.NewRequestWithContext( - ctx, method, gConfig.Gitea+path, body) + ctx, method, getConfig().Gitea+path, body) if req != nil { - req.Header.Set("Authorization", "token "+gConfig.Token) + req.Header.Set("Authorization", "token "+getConfig().Token) req.Header.Set("Accept", "application/json") } return req, err @@ -397,7 +414,7 @@ func handlePush(w http.ResponseWriter, r *http.Request) { log.Printf("received push: %s %s\n", event.Repository.FullName, event.HeadCommit.ID) - project, ok := gConfig.Projects[event.Repository.FullName] + project, ok := getConfig().Projects[event.Repository.FullName] if !ok { // This is okay, don't set any commit statuses. fmt.Fprintf(w, "The project is not configured.") @@ -506,7 +523,7 @@ func rpcEnqueue(ctx context.Context, return fmt.Errorf("%s: %w", ref, err) } - project, ok := gConfig.Projects[owner+"/"+repo] + project, ok := getConfig().Projects[owner+"/"+repo] if !ok { return fmt.Errorf("project configuration not found") } @@ -558,6 +575,17 @@ func rpcRestart(ctx context.Context, return nil } +func rpcReload(ctx context.Context, + w io.Writer, fs *flag.FlagSet, args []string) error { + if err := fs.Parse(args); err != nil { + return err + } + if fs.NArg() > 0 { + return errWrongUsage + } + return loadConfig() +} + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - var rpcCommands = map[string]struct { @@ -570,6 +598,8 @@ var rpcCommands = map[string]struct { "Create or restart tasks for the given reference."}, "restart": {rpcRestart, "[ID]...", "Schedule tasks with the given IDs to be rerun."}, + "reload": {rpcReload, "", + "Reload configuration."}, } func rpcPrintCommands(w io.Writer) { @@ -656,7 +686,7 @@ func handleRPC(w http.ResponseWriter, r *http.Request) { func notifierRunCommand(ctx context.Context, task Task) { script := bytes.NewBuffer(nil) - if err := gNotifyScript.Execute(script, &task); err != nil { + if err := getConfig().notifyTemplate.Execute(script, &task); err != nil { log.Printf("error: notify: %s", err) return } @@ -683,13 +713,14 @@ func notifierNotify(ctx context.Context, task Task) error { TargetURL string `json:"target_url"` }{} - runner, ok := gConfig.Runners[task.Runner] + config := getConfig() + runner, ok := config.Runners[task.Runner] if !ok { log.Printf("task %d has an unknown runner %s\n", task.ID, task.Runner) return nil } payload.Context = runner.Name - payload.TargetURL = fmt.Sprintf("%s/task/%d", gConfig.Root, task.ID) + payload.TargetURL = fmt.Sprintf("%s/task/%d", config.Root, task.ID) switch task.State { case taskStateNew: @@ -807,13 +838,14 @@ type RunningTask struct { // newRunningTask prepares a task for running, without executing anything yet. func newRunningTask(task Task) (*RunningTask, error) { rt := &RunningTask{DB: task} + config := getConfig() var ok bool - rt.Runner, ok = gConfig.Runners[rt.DB.Runner] + rt.Runner, ok = config.Runners[rt.DB.Runner] if !ok { return nil, fmt.Errorf("unknown runner: %s", rt.DB.Runner) } - project, ok := gConfig.Projects[rt.DB.FullName()] + project, ok := config.Projects[rt.DB.FullName()] if !ok { return nil, fmt.Errorf("project configuration not found") } @@ -1381,7 +1413,7 @@ type Task struct { func (t *Task) FullName() string { return t.Owner + "/" + t.Repo } func (t *Task) RunnerName() string { - if runner, ok := gConfig.Runners[t.Runner]; !ok { + if runner, ok := getConfig().Runners[t.Runner]; !ok { return t.Runner } else { return runner.Name @@ -1389,20 +1421,20 @@ func (t *Task) RunnerName() string { } func (t *Task) URL() string { - return fmt.Sprintf("%s/task/%d", gConfig.Root, t.ID) + return fmt.Sprintf("%s/task/%d", getConfig().Root, t.ID) } func (t *Task) RepoURL() string { - return fmt.Sprintf("%s/%s/%s", gConfig.Gitea, t.Owner, t.Repo) + return fmt.Sprintf("%s/%s/%s", getConfig().Gitea, t.Owner, t.Repo) } func (t *Task) CommitURL() string { return fmt.Sprintf("%s/%s/%s/commit/%s", - gConfig.Gitea, t.Owner, t.Repo, t.Hash) + getConfig().Gitea, t.Owner, t.Repo, t.Hash) } func (t *Task) CloneURL() string { - return fmt.Sprintf("%s/%s/%s.git", gConfig.Gitea, t.Owner, t.Repo) + return fmt.Sprintf("%s/%s/%s.git", getConfig().Gitea, t.Owner, t.Repo) } func (t *Task) update() error { @@ -1509,7 +1541,7 @@ func callRPC(args []string) error { } req, err := http.NewRequest(http.MethodPost, - fmt.Sprintf("%s/rpc", gConfig.Root), bytes.NewReader(body)) + fmt.Sprintf("%s/rpc", getConfig().Root), bytes.NewReader(body)) if err != nil { return err } @@ -1580,7 +1612,8 @@ func main() { return } - if err := parseConfig(flag.Arg(0)); err != nil { + gConfigPath = flag.Arg(0) + if err := loadConfig(); err != nil { log.Fatalln(err) } if flag.NArg() > 1 { @@ -1590,7 +1623,7 @@ func main() { return } - if err := dbOpen(gConfig.DB); err != nil { + if err := dbOpen(getConfig().DB); err != nil { log.Fatalln(err) } defer gDB.Close() @@ -1599,7 +1632,7 @@ func main() { ctx, stop := signal.NotifyContext( context.Background(), syscall.SIGINT, syscall.SIGTERM) - server := &http.Server{Addr: gConfig.Listen} + server := &http.Server{Addr: getConfig().Listen} http.HandleFunc("/{$}", handleTasks) http.HandleFunc("/task/{id}", handleTask) http.HandleFunc("/push", handlePush)