Runtime configuration changes
All checks were successful
Alpine 3.20 Success

Through an RPC command, because systemd documentation told us to.
This commit is contained in:
Přemysl Eric Janouch 2024-12-26 12:02:50 +01:00
parent 2bd231b84f
commit 4a7fc55c92
Signed by: p
GPG Key ID: A0420B94F92B9493
2 changed files with 64 additions and 29 deletions

View File

@ -37,6 +37,8 @@ Commands
*restart* [_ID_]...:: *restart* [_ID_]...::
Schedule tasks with the given IDs to be rerun. Schedule tasks with the given IDs to be rerun.
Run this command without arguments to pick up external database changes. Run this command without arguments to pick up external database changes.
*reload*::
Reload configuration.
Configuration Configuration
------------- -------------

91
acid.go
View File

@ -26,6 +26,7 @@ import (
"strconv" "strconv"
"strings" "strings"
"sync" "sync"
"sync/atomic"
"syscall" "syscall"
ttemplate "text/template" ttemplate "text/template"
"time" "time"
@ -40,9 +41,9 @@ var (
projectName = "acid" projectName = "acid"
projectVersion = "?" projectVersion = "?"
gConfig Config = Config{Listen: ":http"} gConfigPath string
gNotifyScript *ttemplate.Template gConfig atomic.Pointer[Config]
gDB *sql.DB gDB *sql.DB
gNotifierSignal = make(chan struct{}, 1) gNotifierSignal = make(chan struct{}, 1)
gExecutorSignal = make(chan struct{}, 1) gExecutorSignal = make(chan struct{}, 1)
@ -52,6 +53,8 @@ var (
gRunning = make(map[int64]*RunningTask) gRunning = make(map[int64]*RunningTask)
) )
func getConfig() *Config { return gConfig.Load() }
// --- Config ------------------------------------------------------------------ // --- Config ------------------------------------------------------------------
type Config struct { type Config struct {
@ -65,6 +68,8 @@ type Config struct {
Runners map[string]ConfigRunner `yaml:"runners"` // script runners Runners map[string]ConfigRunner `yaml:"runners"` // script runners
Projects map[string]ConfigProject `yaml:"projects"` // configured projects Projects map[string]ConfigProject `yaml:"projects"` // configured projects
notifyTemplate *ttemplate.Template
} }
type ConfigRunner struct { type ConfigRunner struct {
@ -86,8 +91,9 @@ type ConfigProject struct {
func (cf *ConfigProject) AutomaticRunners() (runners []string) { func (cf *ConfigProject) AutomaticRunners() (runners []string) {
// We pass through unknown runner names, // We pass through unknown runner names,
// so that they can cause reference errors later. // so that they can cause reference errors later.
config := getConfig()
for runner := range cf.Runners { for runner := range cf.Runners {
if r, _ := gConfig.Runners[runner]; !r.Manual { if r, _ := config.Runners[runner]; !r.Manual {
runners = append(runners, runner) runners = append(runners, runner)
} }
} }
@ -102,17 +108,28 @@ type ConfigProjectRunner struct {
Timeout string `yaml:"timeout"` // timeout duration Timeout string `yaml:"timeout"` // timeout duration
} }
func parseConfig(path string) error { // loadConfig reloads configuration.
if f, err := os.Open(path); err != nil { // 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 return err
} else if err = yaml.NewDecoder(f).Decode(&gConfig); err != nil { } else if err = yaml.NewDecoder(f).Decode(new); err != nil {
return err 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 var err error
gNotifyScript, err = new.notifyTemplate, err =
ttemplate.New("notify").Funcs(shellFuncs).Parse(gConfig.Notify) ttemplate.New("notify").Funcs(shellFuncs).Parse(new.Notify)
return err if err != nil {
return err
}
gConfig.Store(new)
return nil
} }
var shellFuncs = ttemplate.FuncMap{ var shellFuncs = ttemplate.FuncMap{
@ -137,7 +154,7 @@ var shellFuncs = ttemplate.FuncMap{
// --- Utilities --------------------------------------------------------------- // --- Utilities ---------------------------------------------------------------
func giteaSign(b []byte) string { func giteaSign(b []byte) string {
payloadHmac := hmac.New(sha256.New, []byte(gConfig.Secret)) payloadHmac := hmac.New(sha256.New, []byte(getConfig().Secret))
payloadHmac.Write(b) payloadHmac.Write(b)
return hex.EncodeToString(payloadHmac.Sum(nil)) 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) ( func giteaNewRequest(ctx context.Context, method, path string, body io.Reader) (
*http.Request, error) { *http.Request, error) {
req, err := http.NewRequestWithContext( req, err := http.NewRequestWithContext(
ctx, method, gConfig.Gitea+path, body) ctx, method, getConfig().Gitea+path, body)
if req != nil { if req != nil {
req.Header.Set("Authorization", "token "+gConfig.Token) req.Header.Set("Authorization", "token "+getConfig().Token)
req.Header.Set("Accept", "application/json") req.Header.Set("Accept", "application/json")
} }
return req, err return req, err
@ -397,7 +414,7 @@ func handlePush(w http.ResponseWriter, r *http.Request) {
log.Printf("received push: %s %s\n", log.Printf("received push: %s %s\n",
event.Repository.FullName, event.HeadCommit.ID) event.Repository.FullName, event.HeadCommit.ID)
project, ok := gConfig.Projects[event.Repository.FullName] project, ok := getConfig().Projects[event.Repository.FullName]
if !ok { if !ok {
// This is okay, don't set any commit statuses. // This is okay, don't set any commit statuses.
fmt.Fprintf(w, "The project is not configured.") fmt.Fprintf(w, "The project is not configured.")
@ -506,7 +523,7 @@ func rpcEnqueue(ctx context.Context,
return fmt.Errorf("%s: %w", ref, err) return fmt.Errorf("%s: %w", ref, err)
} }
project, ok := gConfig.Projects[owner+"/"+repo] project, ok := getConfig().Projects[owner+"/"+repo]
if !ok { if !ok {
return fmt.Errorf("project configuration not found") return fmt.Errorf("project configuration not found")
} }
@ -558,6 +575,17 @@ func rpcRestart(ctx context.Context,
return nil 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 { var rpcCommands = map[string]struct {
@ -570,6 +598,8 @@ var rpcCommands = map[string]struct {
"Create or restart tasks for the given reference."}, "Create or restart tasks for the given reference."},
"restart": {rpcRestart, "[ID]...", "restart": {rpcRestart, "[ID]...",
"Schedule tasks with the given IDs to be rerun."}, "Schedule tasks with the given IDs to be rerun."},
"reload": {rpcReload, "",
"Reload configuration."},
} }
func rpcPrintCommands(w io.Writer) { func rpcPrintCommands(w io.Writer) {
@ -656,7 +686,7 @@ func handleRPC(w http.ResponseWriter, r *http.Request) {
func notifierRunCommand(ctx context.Context, task Task) { func notifierRunCommand(ctx context.Context, task Task) {
script := bytes.NewBuffer(nil) 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) log.Printf("error: notify: %s", err)
return return
} }
@ -683,13 +713,14 @@ func notifierNotify(ctx context.Context, task Task) error {
TargetURL string `json:"target_url"` TargetURL string `json:"target_url"`
}{} }{}
runner, ok := gConfig.Runners[task.Runner] config := getConfig()
runner, ok := config.Runners[task.Runner]
if !ok { if !ok {
log.Printf("task %d has an unknown runner %s\n", task.ID, task.Runner) log.Printf("task %d has an unknown runner %s\n", task.ID, task.Runner)
return nil return nil
} }
payload.Context = runner.Name 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 { switch task.State {
case taskStateNew: case taskStateNew:
@ -807,13 +838,14 @@ type RunningTask struct {
// newRunningTask prepares a task for running, without executing anything yet. // newRunningTask prepares a task for running, without executing anything yet.
func newRunningTask(task Task) (*RunningTask, error) { func newRunningTask(task Task) (*RunningTask, error) {
rt := &RunningTask{DB: task} rt := &RunningTask{DB: task}
config := getConfig()
var ok bool var ok bool
rt.Runner, ok = gConfig.Runners[rt.DB.Runner] rt.Runner, ok = config.Runners[rt.DB.Runner]
if !ok { if !ok {
return nil, fmt.Errorf("unknown runner: %s", rt.DB.Runner) 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 { if !ok {
return nil, fmt.Errorf("project configuration not found") 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) FullName() string { return t.Owner + "/" + t.Repo }
func (t *Task) RunnerName() string { func (t *Task) RunnerName() string {
if runner, ok := gConfig.Runners[t.Runner]; !ok { if runner, ok := getConfig().Runners[t.Runner]; !ok {
return t.Runner return t.Runner
} else { } else {
return runner.Name return runner.Name
@ -1389,20 +1421,20 @@ func (t *Task) RunnerName() string {
} }
func (t *Task) URL() 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 { 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 { func (t *Task) CommitURL() string {
return fmt.Sprintf("%s/%s/%s/commit/%s", 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 { 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 { func (t *Task) update() error {
@ -1509,7 +1541,7 @@ func callRPC(args []string) error {
} }
req, err := http.NewRequest(http.MethodPost, 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 { if err != nil {
return err return err
} }
@ -1580,7 +1612,8 @@ func main() {
return return
} }
if err := parseConfig(flag.Arg(0)); err != nil { gConfigPath = flag.Arg(0)
if err := loadConfig(); err != nil {
log.Fatalln(err) log.Fatalln(err)
} }
if flag.NArg() > 1 { if flag.NArg() > 1 {
@ -1590,7 +1623,7 @@ func main() {
return return
} }
if err := dbOpen(gConfig.DB); err != nil { if err := dbOpen(getConfig().DB); err != nil {
log.Fatalln(err) log.Fatalln(err)
} }
defer gDB.Close() defer gDB.Close()
@ -1599,7 +1632,7 @@ func main() {
ctx, stop := signal.NotifyContext( ctx, stop := signal.NotifyContext(
context.Background(), syscall.SIGINT, syscall.SIGTERM) context.Background(), syscall.SIGINT, syscall.SIGTERM)
server := &http.Server{Addr: gConfig.Listen} server := &http.Server{Addr: getConfig().Listen}
http.HandleFunc("/{$}", handleTasks) http.HandleFunc("/{$}", handleTasks)
http.HandleFunc("/task/{id}", handleTask) http.HandleFunc("/task/{id}", handleTask)
http.HandleFunc("/push", handlePush) http.HandleFunc("/push", handlePush)