From 08da9dcb848e8b19da78f4c54b3219235aec65ef Mon Sep 17 00:00:00 2001 From: tiennm99 Date: Sun, 28 Jun 2026 14:05:48 +0700 Subject: [PATCH] fix(config): discover mounted yml config --- README.md | 4 +++- config.go | 23 +++++++++++++++++++++++ config_test.go | 36 ++++++++++++++++++++++++++++++++++++ main.go | 9 ++++++--- 4 files changed, 68 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 2b39c54..13aed92 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ Successor to the `*-keepalive` family: one binary, one image, six datastore adap ## Configuration -By default, keepalive reads `keepalive.yaml` from the current working directory. One deployment can keep any number of services alive. +By default, keepalive reads the first config file it finds: `keepalive.yaml`, `keepalive.yml`, `/keepalive.yaml`, then `/keepalive.yml`. One deployment can keep any number of services alive. ```yaml interval: 1m @@ -66,6 +66,8 @@ docker run -d --name keepalive --restart unless-stopped \ keepalive:local ``` +If your host config file is named `keepalive.yml`, mount it to `/keepalive.yml` instead. + ## Quick start (local) ```bash diff --git a/config.go b/config.go index 642d986..1dd5a78 100644 --- a/config.go +++ b/config.go @@ -1,6 +1,7 @@ package main import ( + "errors" "fmt" "net" "net/url" @@ -18,6 +19,13 @@ const ( defaultCounterKey = "counter" ) +var defaultConfigFiles = []string{ + "keepalive.yaml", + "keepalive.yml", + "/keepalive.yaml", + "/keepalive.yml", +} + type appConfig struct { Interval string `json:"interval" yaml:"interval"` CounterKey string `json:"counter_key" yaml:"counter_key"` @@ -52,6 +60,21 @@ func loadConfigFile(path string) ([]serviceConfig, error) { return normalizeConfig(raw) } +func defaultConfigFile() (string, error) { + return firstExistingConfigFile(defaultConfigFiles) +} + +func firstExistingConfigFile(paths []string) (string, error) { + for _, path := range paths { + if _, err := os.Stat(path); err == nil { + return path, nil + } else if !errors.Is(err, os.ErrNotExist) { + return "", err + } + } + return "", fmt.Errorf("config file not found (looked for: %s)", strings.Join(paths, ", ")) +} + func normalizeConfig(raw appConfig) ([]serviceConfig, error) { if len(raw.Services) == 0 { return nil, fmt.Errorf("services must contain at least one service") diff --git a/config_test.go b/config_test.go index 78925e3..1ed3556 100644 --- a/config_test.go +++ b/config_test.go @@ -1,6 +1,9 @@ package main import ( + "os" + "path/filepath" + "strings" "testing" "time" ) @@ -35,6 +38,39 @@ func TestKeepaliveExampleConfigParses(t *testing.T) { } } +func TestFirstExistingConfigFileFindsYMLFallback(t *testing.T) { + dir := t.TempDir() + ymlPath := filepath.Join(dir, "keepalive.yml") + if err := os.WriteFile(ymlPath, []byte("services: []\n"), 0o600); err != nil { + t.Fatalf("write config: %v", err) + } + + got, err := firstExistingConfigFile([]string{ + filepath.Join(dir, "keepalive.yaml"), + ymlPath, + }) + if err != nil { + t.Fatalf("firstExistingConfigFile returned error: %v", err) + } + if got != ymlPath { + t.Fatalf("firstExistingConfigFile() = %q, want %q", got, ymlPath) + } +} + +func TestFirstExistingConfigFileReportsCandidates(t *testing.T) { + dir := t.TempDir() + _, err := firstExistingConfigFile([]string{ + filepath.Join(dir, "keepalive.yaml"), + filepath.Join(dir, "keepalive.yml"), + }) + if err == nil { + t.Fatal("firstExistingConfigFile returned nil error") + } + if !strings.Contains(err.Error(), "keepalive.yaml") || !strings.Contains(err.Error(), "keepalive.yml") { + t.Fatalf("error %q does not list expected candidates", err) + } +} + func TestNormalizeConfigSuffixesDuplicateGeneratedNames(t *testing.T) { services, err := normalizeConfig(appConfig{ Services: []serviceFileConfig{ diff --git a/main.go b/main.go index b24a7d9..a4dc2a9 100644 --- a/main.go +++ b/main.go @@ -11,10 +11,13 @@ import ( "github.com/tiennm99/keepalive/adapter" ) -const defaultConfigFile = "keepalive.yaml" - func main() { - services, err := loadConfigFile(defaultConfigFile) + configPath, err := defaultConfigFile() + if err != nil { + log.Fatalf("load config: %v", err) + } + + services, err := loadConfigFile(configPath) if err != nil { log.Fatalf("load config: %v", err) }