From a20840e7d82786c7d18ba58a5462da5c3a137361 Mon Sep 17 00:00:00 2001 From: Lemon-miaow Date: Sat, 26 Sep 2026 12:03:23 +0800 Subject: [PATCH] =?UTF-8?q?feat(config):=20[database]=20deployment=20?= =?UTF-8?q?=E6=8C=87=E5=90=91=E9=9B=86=E7=BE=A4=E5=86=85=E7=9A=84=E6=95=B0?= =?UTF-8?q?=E6=8D=AE=E5=BA=93?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/config/config.go | 15 +++++++++++++++ internal/config/config_test.go | 19 +++++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/internal/config/config.go b/internal/config/config.go index ae61b92..23fbec4 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -20,6 +20,9 @@ import ( // takes in its comma-separated --accept flag. var scanIDPattern = regexp.MustCompile(`^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$`) +// dnsLabel is a Kubernetes namespace or object name (RFC 1123 label). +var dnsLabel = regexp.MustCompile(`^[a-z0-9]([-a-z0-9]{0,61}[a-z0-9])?$`) + // Config is the parsed felis.toml. type Config struct { Server ServerConfig `toml:"server"` @@ -120,6 +123,12 @@ type ServerConfig struct { // DatabaseConfig is the [database] table. type DatabaseConfig struct { URL string `toml:"url"` + // Deployment is the k3s Deployment the database runs as, "namespace/name" + // ("felis/felis-postgres"), written into the host copy only. The host + // carries no PostgreSQL client, so `felis db backup`/`restore` and the + // pre-migration snapshot run pg_dump, pg_restore and psql inside its + // postgres container. Empty runs the tools on PATH against url. + Deployment string `toml:"deployment"` } // VelocityConfig is the [velocity] table. @@ -531,6 +540,12 @@ func (c *Config) Validate() error { if c.Database.URL == "" { return fmt.Errorf("config: [database] url is required") } + if d := c.Database.Deployment; d != "" { + ns, name, ok := strings.Cut(d, "/") + if !ok || !dnsLabel.MatchString(ns) || !dnsLabel.MatchString(name) { + return fmt.Errorf("config: [database] deployment %q is not namespace/name", d) + } + } if c.Server.RootDomain == "" { return fmt.Errorf("config: [server] root_domain is required") } diff --git a/internal/config/config_test.go b/internal/config/config_test.go index 60cb089..d85d7a9 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -171,6 +171,25 @@ root_domain = "mc.example.net" } } +func TestLoadDatabaseDeployment(t *testing.T) { + withDeployment := func(v string) string { + return strings.Replace(validTOML, `url = "postgres://felis:secret@db:5432/felis"`, + `url = "postgres://felis:secret@db:5432/felis"`+"\ndeployment = \""+v+"\"", 1) + } + cfg, err := config.Load(writeTOML(t, withDeployment("felis/felis-postgres"))) + if err != nil { + t.Fatalf("Load: %v", err) + } + if cfg.Database.Deployment != "felis/felis-postgres" { + t.Fatalf("deployment = %q", cfg.Database.Deployment) + } + for _, bad := range []string{"felis-postgres", "felis/", "/felis-postgres", "Felis/pg", "felis/pg; rm -rf /", "a/b/c"} { + if _, err := config.Load(writeTOML(t, withDeployment(bad))); err == nil || !strings.Contains(err.Error(), "namespace/name") { + t.Errorf("deployment %q: err = %v, want a namespace/name refusal", bad, err) + } + } +} + func TestLoadRejectsMissingRootDomain(t *testing.T) { _, err := config.Load(writeTOML(t, ` [database]