This repository has no description
21

Configure Feed

Select the types of activity you want to include in your feed.

feat: toml config

isabel (Jun 13, 2026, 1:19 PM +0100) 10bcf817 e05293df

+146 -119
+2 -2
cmd/snot/check.go
··· 11 11 12 12 type CheckCmd struct{} 13 13 14 - func (c *CheckCmd) Run() error { 14 + func (c *CheckCmd) Run(cli *CLI) error { 15 15 ctx := context.Background() 16 16 17 - cfg, err := config.Load(ctx) 17 + cfg, err := config.Load(ctx, cli.Config) 18 18 if err != nil { 19 19 return fmt.Errorf("config: %w", err) 20 20 }
+4 -2
cmd/snot/main.go
··· 8 8 9 9 type CLI struct { 10 10 Version kong.VersionFlag `help:"Show version and exit."` 11 + Config string `help:"Path to the TOML config file." default:"/etc/snot/config.toml" env:"SNOT_CONFIG" type:"path"` 11 12 12 13 Serve ServeCmd `cmd:"" help:"Run the knot server."` 13 14 Check CheckCmd `cmd:"" help:"Validate configuration: database, Forgejo user, repository root."` ··· 18 19 var cli CLI 19 20 kctx := kong.Parse(&cli, 20 21 kong.Name("snot"), 21 - kong.Description("A tangled knot server backed by a Forgejo instance. Configuration via SNOT_* environment variables."), 22 + kong.Description("A tangled knot server backed by a Forgejo instance. "+ 23 + "Configured via a TOML file (--config), with SNOT_* env vars overriding individual keys."), 22 24 kong.UsageOnError(), 23 25 kong.Vars{"version": Version}, 24 26 ) 25 27 26 - kctx.FatalIfErrorf(kctx.Run()) 28 + kctx.FatalIfErrorf(kctx.Run(&cli)) 27 29 }
+2 -2
cmd/snot/repos.go
··· 13 13 14 14 type ReposCmd struct{} 15 15 16 - func (c *ReposCmd) Run() error { 16 + func (c *ReposCmd) Run(cli *CLI) error { 17 17 ctx := context.Background() 18 18 19 - cfg, err := config.Load(ctx) 19 + cfg, err := config.Load(ctx, cli.Config) 20 20 if err != nil { 21 21 return fmt.Errorf("config: %w", err) 22 22 }
+2 -2
cmd/snot/serve.go
··· 25 25 26 26 type ServeCmd struct{} 27 27 28 - func (c *ServeCmd) Run() error { 28 + func (c *ServeCmd) Run(cli *CLI) error { 29 29 ctx := context.Background() 30 30 logger := slog.New(slog.NewTextHandler(os.Stderr, nil)) 31 31 32 - cfg, err := config.Load(ctx) 32 + cfg, err := config.Load(ctx, cli.Config) 33 33 if err != nil { 34 34 return fmt.Errorf("config: %w", err) 35 35 }
+1 -1
default.nix
··· 5 5 6 6 src = ./.; 7 7 8 - vendorHash = "sha256-uoHU3nySdQ/h1M9MWyrW/9h963byGV5AYvzW2xPkXyE="; 8 + vendorHash = "sha256-DH7PTmkZHjl8VJ4l/2zGg5qYq3+LqI4SvrE4nbNU9yo="; 9 9 10 10 subPackages = [ "cmd/snot" ]; 11 11
+4
go.mod
··· 60 60 github.com/earthboundkid/versioninfo/v2 v2.24.1 // indirect 61 61 github.com/emirpasic/gods v1.18.1 // indirect 62 62 github.com/felixge/httpsnoop v1.0.4 // indirect 63 + github.com/fsnotify/fsnotify v1.9.0 // indirect 63 64 github.com/glebarez/go-sqlite v1.21.2 // indirect 64 65 github.com/go-enry/go-enry/v2 v2.9.6 // indirect 65 66 github.com/go-enry/go-oniguruma v1.2.1 // indirect ··· 107 108 github.com/klauspost/compress v1.18.0 // indirect 108 109 github.com/klauspost/cpuid/v2 v2.3.0 // indirect 109 110 github.com/knadh/koanf/maps v0.1.2 // indirect 111 + github.com/knadh/koanf/parsers/toml/v2 v2.2.1 // indirect 112 + github.com/knadh/koanf/providers/file v1.2.1 // indirect 110 113 github.com/landlock-lsm/go-landlock v0.8.1 // indirect 111 114 github.com/lucasb-eyer/go-colorful v1.3.0 // indirect 112 115 github.com/mattn/go-isatty v0.0.20 // indirect ··· 127 130 github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect 128 131 github.com/openbao/openbao/api/v2 v2.3.0 // indirect 129 132 github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b // indirect 133 + github.com/pelletier/go-toml/v2 v2.3.1 // indirect 130 134 github.com/pjbgf/sha1cd v0.3.2 // indirect 131 135 github.com/pkg/errors v0.9.1 // indirect 132 136 github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
+8
go.sum
··· 102 102 github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= 103 103 github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= 104 104 github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= 105 + github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= 106 + github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= 105 107 github.com/glebarez/go-sqlite v1.21.2 h1:3a6LFC4sKahUunAmynQKLZceZCOzUthkRkEAl9gAXWo= 106 108 github.com/glebarez/go-sqlite v1.21.2/go.mod h1:sfxdZyhQjTM2Wry3gVYWaW072Ri1WMdWJi0k6+3382k= 107 109 github.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GMw= ··· 265 267 github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= 266 268 github.com/knadh/koanf/maps v0.1.2 h1:RBfmAW5CnZT+PJ1CVc1QSJKf4Xu9kxfQgYVQSu8hpbo= 267 269 github.com/knadh/koanf/maps v0.1.2/go.mod h1:npD/QZY3V6ghQDdcQzl1W4ICNVTkohC8E73eI2xW4yI= 270 + github.com/knadh/koanf/parsers/toml/v2 v2.2.1 h1:bDF9KugExgzHrvNvfxxYgaxqJHSv+ZOoa0j30BYNhW4= 271 + github.com/knadh/koanf/parsers/toml/v2 v2.2.1/go.mod h1:Lul0orUj0zAWE2R5yWKATUPq5yl1a6hlggz87rtDKnQ= 268 272 github.com/knadh/koanf/providers/confmap v1.0.0 h1:mHKLJTE7iXEys6deO5p6olAiZdG5zwp8Aebir+/EaRE= 269 273 github.com/knadh/koanf/providers/confmap v1.0.0/go.mod h1:txHYHiI2hAtF0/0sCmcuol4IDcuQbKTybiB1nOcUo1A= 270 274 github.com/knadh/koanf/providers/env/v2 v2.0.0 h1:Ad5H3eun722u+FvchiIcEIJZsZ2M6oxCkgZfWN5B5KY= 271 275 github.com/knadh/koanf/providers/env/v2 v2.0.0/go.mod h1:1g01PE+Ve1gBfWNNw2wmULRP0tc8RJrjn5p2N/jNCIc= 276 + github.com/knadh/koanf/providers/file v1.2.1 h1:bEWbtQwYrA+W2DtdBrQWyXqJaJSG3KrP3AESOJYp9wM= 277 + github.com/knadh/koanf/providers/file v1.2.1/go.mod h1:bp1PM5f83Q+TOUu10J/0ApLBd9uIzg+n9UgthfY+nRA= 272 278 github.com/knadh/koanf/v2 v2.3.5 h1:2dXJUYaKGm4SGYeoAtBviq9+02JZo/pxQ2ssOd60rJg= 273 279 github.com/knadh/koanf/v2 v2.3.5/go.mod h1:gRb40VRAbd4iJMYYD5IxZ6hfuopFcXBpc9bbQpZwo28= 274 280 github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= ··· 353 359 github.com/oppiliappan/chroma/v2 v2.24.2/go.mod h1:NqVhfBR0lte5Ouh3DcthuUCTUpDC9cxBOfyMbMQPs3o= 354 360 github.com/oppiliappan/go-git/v5 v5.17.0 h1:CuJnpcIDxr0oiNaSHMconovSWnowHznVDG+AhjGuSEo= 355 361 github.com/oppiliappan/go-git/v5 v5.17.0/go.mod h1:q/FE8C3SPMoRN7LoH9vRFiBzidAOBWJPS1CqVS8DN+w= 362 + github.com/pelletier/go-toml/v2 v2.3.1 h1:MYEvvGnQjeNkRF1qUuGolNtNExTDwct51yp7olPtrEc= 363 + github.com/pelletier/go-toml/v2 v2.3.1/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= 356 364 github.com/pjbgf/sha1cd v0.3.2 h1:a9wb0bp1oC2TGwStyn0Umc/IGKQnEgF0vVaZ8QF8eo4= 357 365 github.com/pjbgf/sha1cd v0.3.2/go.mod h1:zQWigSxVmsHEZow5qaLtPYxpcKMMQpa09ixqBxuCS6A= 358 366 github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+35 -13
internal/config/config.go
··· 2 2 3 3 import ( 4 4 "context" 5 + "errors" 5 6 "fmt" 7 + "os" 6 8 "strconv" 7 9 "strings" 8 10 11 + "github.com/knadh/koanf/parsers/toml/v2" 9 12 "github.com/knadh/koanf/providers/confmap" 10 13 "github.com/knadh/koanf/providers/env/v2" 14 + "github.com/knadh/koanf/providers/file" 11 15 "github.com/knadh/koanf/v2" 12 16 ) 13 17 ··· 20 24 OwnerDid string 21 25 22 26 // UserMap maps repo-owner DIDs to the Forgejo users whose repos they 23 - // expose, parsed from SNOT_USER_MAP="did:plc:abc=isabel,did:plc:def=alice". 27 + // expose. In TOML this is the [users] table; SNOT_USER_MAP 28 + // ("did:plc:abc=isabel,did:plc:def=alice") overrides/extends it. 24 29 UserMap map[string]string 25 30 26 31 DbDsn string ··· 31 36 Dev bool 32 37 } 33 38 34 - // Load reads configuration from SNOT_* environment variables. 35 - func Load(ctx context.Context) (*Config, error) { 36 - return loadWith(nil) 39 + // Load reads configuration from the TOML file at path (optional; skipped if 40 + // absent), with SNOT_* environment variables overriding individual keys. 41 + func Load(ctx context.Context, path string) (*Config, error) { 42 + return loadWith(path, nil) 37 43 } 38 44 39 - // loadWith builds the config from the given environ source (nil = os.Environ), 40 - // applying defaults first and SNOT_* env vars on top. 41 - func loadWith(environ func() []string) (*Config, error) { 45 + // loadWith layers defaults < TOML file < SNOT_* env. environ defaults to 46 + // os.Environ when nil. 47 + func loadWith(path string, environ func() []string) (*Config, error) { 42 48 k := koanf.New(".") 43 49 44 50 _ = k.Load(confmap.Provider(map[string]any{ ··· 48 54 "dev": "false", 49 55 }, "."), nil) 50 56 57 + if path != "" { 58 + if err := k.Load(file.Provider(path), toml.Parser()); err != nil && !errors.Is(err, os.ErrNotExist) { 59 + return nil, fmt.Errorf("loading config %s: %w", path, err) 60 + } 61 + } 62 + 51 63 err := k.Load(env.Provider(".", env.Opt{ 52 64 Prefix: "SNOT_", 53 65 EnvironFunc: environ, ··· 59 71 return nil, fmt.Errorf("loading env: %w", err) 60 72 } 61 73 74 + users := k.StringMap("users") 75 + if users == nil { 76 + users = map[string]string{} 77 + } 78 + for did, user := range parseUserMap(k.String("user_map")) { 79 + users[did] = user 80 + } 81 + 62 82 dev, _ := strconv.ParseBool(k.String("dev")) 63 83 cfg := &Config{ 64 84 Hostname: k.String("hostname"), 65 85 ListenAddr: k.String("listen_addr"), 66 86 OwnerDid: k.String("owner_did"), 67 - UserMap: parseUserMap(k.String("user_map")), 87 + UserMap: users, 68 88 DbDsn: k.String("db_dsn"), 69 89 RepoRoot: k.String("repo_root"), 70 90 PushRemote: k.String("push_remote"), ··· 79 99 return cfg, nil 80 100 } 81 101 102 + // parseUserMap parses the SNOT_USER_MAP override form 103 + // "did:plc:abc=isabel,did:plc:def=alice". 82 104 func parseUserMap(s string) map[string]string { 83 105 m := map[string]string{} 84 106 for _, pair := range strings.Split(s, ",") { ··· 98 120 func (c *Config) validate() error { 99 121 var missing []string 100 122 if c.Hostname == "" { 101 - missing = append(missing, "SNOT_HOSTNAME") 123 + missing = append(missing, "hostname / SNOT_HOSTNAME") 102 124 } 103 125 if c.OwnerDid == "" { 104 - missing = append(missing, "SNOT_OWNER_DID") 126 + missing = append(missing, "owner_did / SNOT_OWNER_DID") 105 127 } 106 128 if len(c.UserMap) == 0 { 107 - missing = append(missing, "SNOT_USER_MAP") 129 + missing = append(missing, "[users] / SNOT_USER_MAP") 108 130 } 109 131 if c.DbDsn == "" { 110 - missing = append(missing, "SNOT_DB_DSN") 132 + missing = append(missing, "db_dsn / SNOT_DB_DSN") 111 133 } 112 134 if c.RepoRoot == "" { 113 - missing = append(missing, "SNOT_REPO_ROOT") 135 + missing = append(missing, "repo_root / SNOT_REPO_ROOT") 114 136 } 115 137 if len(missing) > 0 { 116 138 return fmt.Errorf("missing required config: %s", strings.Join(missing, ", "))
+69 -20
internal/config/config_test.go
··· 1 1 package config 2 2 3 3 import ( 4 + "os" 5 + "path/filepath" 4 6 "testing" 5 7 ) 6 8 7 - func load(t *testing.T, env map[string]string) (*Config, error) { 9 + func loadEnv(t *testing.T, env map[string]string) (*Config, error) { 8 10 t.Helper() 9 11 var environ []string 10 12 for k, v := range env { 11 13 environ = append(environ, k+"="+v) 12 14 } 13 - return loadWith(func() []string { return environ }) 15 + return loadWith("", func() []string { return environ }) 14 16 } 15 17 16 - func TestLoadDefaults(t *testing.T) { 17 - c, err := load(t, map[string]string{ 18 + func TestLoadDefaultsFromEnv(t *testing.T) { 19 + c, err := loadEnv(t, map[string]string{ 18 20 "SNOT_HOSTNAME": "knot.example.com", 19 21 "SNOT_OWNER_DID": "did:plc:abc123", 20 22 "SNOT_USER_MAP": "did:plc:abc123=isabel,did:plc:def456=alice", ··· 30 32 if c.StateDir != "/var/lib/snot" { 31 33 t.Errorf("StateDir = %q", c.StateDir) 32 34 } 35 + if c.UserMap["did:plc:abc123"] != "isabel" || c.UserMap["did:plc:def456"] != "alice" { 36 + t.Errorf("UserMap = %v", c.UserMap) 37 + } 38 + } 39 + 40 + func TestLoadFromTOML(t *testing.T) { 41 + dir := t.TempDir() 42 + path := filepath.Join(dir, "config.toml") 43 + toml := ` 44 + hostname = "knot.example.com" 45 + owner_did = "did:plc:abc123" 46 + db_dsn = "postgres:///forgejo" 47 + repo_root = "/repos" 48 + dev = true 49 + 50 + [users] 51 + "did:plc:abc123" = "isabel" 52 + "did:plc:def456" = "alice" 53 + ` 54 + if err := os.WriteFile(path, []byte(toml), 0o600); err != nil { 55 + t.Fatal(err) 56 + } 57 + 58 + c, err := loadWith(path, func() []string { return nil }) 59 + if err != nil { 60 + t.Fatal(err) 61 + } 62 + if c.Hostname != "knot.example.com" || c.RepoRoot != "/repos" || !c.Dev { 63 + t.Errorf("config = %+v", c) 64 + } 33 65 if c.PlcUrl != "https://plc.directory" { 34 - t.Errorf("PlcUrl = %q", c.PlcUrl) 66 + t.Errorf("default PlcUrl lost: %q", c.PlcUrl) 35 67 } 36 68 if c.UserMap["did:plc:abc123"] != "isabel" || c.UserMap["did:plc:def456"] != "alice" { 37 69 t.Errorf("UserMap = %v", c.UserMap) 38 70 } 39 71 } 40 72 41 - func TestLoadOverrides(t *testing.T) { 42 - c, err := load(t, map[string]string{ 43 - "SNOT_HOSTNAME": "knot.example.com", 44 - "SNOT_OWNER_DID": "did:plc:abc123", 45 - "SNOT_USER_MAP": "did:plc:abc123=isabel", 46 - "SNOT_DB_DSN": "postgres:///forgejo", 47 - "SNOT_REPO_ROOT": "/repos", 48 - "SNOT_LISTEN_ADDR": "127.0.0.1:9000", 49 - "SNOT_DEV": "true", 73 + func TestEnvOverridesTOML(t *testing.T) { 74 + dir := t.TempDir() 75 + path := filepath.Join(dir, "config.toml") 76 + toml := ` 77 + hostname = "from-file.example.com" 78 + owner_did = "did:plc:abc123" 79 + db_dsn = "postgres:///filedsn" 80 + repo_root = "/repos" 81 + 82 + [users] 83 + "did:plc:abc123" = "isabel" 84 + ` 85 + if err := os.WriteFile(path, []byte(toml), 0o600); err != nil { 86 + t.Fatal(err) 87 + } 88 + 89 + c, err := loadWith(path, func() []string { 90 + return []string{ 91 + "SNOT_HOSTNAME=from-env.example.com", 92 + "SNOT_DB_DSN=postgres:///envdsn", 93 + "SNOT_USER_MAP=did:plc:zzz=bob", 94 + } 50 95 }) 51 96 if err != nil { 52 97 t.Fatal(err) 53 98 } 54 - if c.ListenAddr != "127.0.0.1:9000" { 55 - t.Errorf("ListenAddr = %q", c.ListenAddr) 99 + if c.Hostname != "from-env.example.com" { 100 + t.Errorf("env should override hostname: %q", c.Hostname) 56 101 } 57 - if !c.Dev { 58 - t.Error("Dev should be true") 102 + if c.DbDsn != "postgres:///envdsn" { 103 + t.Errorf("env should override db_dsn: %q", c.DbDsn) 104 + } 105 + // env user_map merges into the file's [users] table. 106 + if c.UserMap["did:plc:abc123"] != "isabel" || c.UserMap["did:plc:zzz"] != "bob" { 107 + t.Errorf("UserMap = %v", c.UserMap) 59 108 } 60 109 } 61 110 62 111 func TestLoadMissingRequired(t *testing.T) { 63 - if _, err := load(t, map[string]string{}); err == nil { 64 - t.Fatal("expected error for missing required vars") 112 + if _, err := loadWith("", func() []string { return nil }); err == nil { 113 + t.Fatal("expected error for missing required config") 65 114 } 66 115 }
+19 -77
nix/module.nix
··· 13 13 mkIf 14 14 mkOption 15 15 mkPackageOption 16 - types 17 16 ; 17 + 18 + settingsFormat = pkgs.formats.toml { }; 19 + configFile = settingsFormat.generate "snot.toml" cfg.settings; 18 20 in 19 21 { 20 22 options.services.snot = { ··· 23 25 package = mkPackageOption pkgs "snot" { }; 24 26 25 27 settings = mkOption { 28 + type = settingsFormat.type; 29 + default = { }; 30 + example = { 31 + hostname = "knot.example.com"; 32 + owner_did = "did:plc:abc123"; 33 + repo_root = "/var/lib/forgejo/repositories"; 34 + users."did:plc:abc123" = "isabel"; 35 + }; 26 36 description = '' 27 - Environment variables to set for the service. Secrets should be 28 - specified using {option}`environmentFiles`. 37 + The configuration for snot. 29 38 ''; 30 - type = types.submodule { 31 - freeformType = types.attrsOf (types.nullOr types.str); 32 - options = { 33 - SNOT_HOSTNAME = mkOption { 34 - type = types.str; 35 - example = "knot.example.com"; 36 - description = "Public hostname of the shim (the knot domain)."; 37 - }; 38 - 39 - SNOT_LISTEN_ADDR = mkOption { 40 - type = types.str; 41 - default = "0.0.0.0:5555"; 42 - description = "Address for the shim to listen on."; 43 - }; 44 - 45 - SNOT_OWNER_DID = mkOption { 46 - type = types.str; 47 - example = "did:plc:abc123"; 48 - description = "atproto DID of the knot owner; governs the knot alone, not repos."; 49 - }; 50 - 51 - SNOT_USER_MAP = mkOption { 52 - type = types.str; 53 - example = "did:plc:abc123=isabel,did:plc:def456=alice"; 54 - description = "Comma-separated did=user pairs mapping repo-owner DIDs to Forgejo users."; 55 - }; 56 - 57 - SNOT_DB_DSN = mkOption { 58 - type = types.str; 59 - default = "postgres://snot@/forgejo?host=/run/postgresql"; 60 - description = '' 61 - Postgres DSN for the Forgejo database. The default uses 62 - unix-socket peer auth; create a `snot` role with 63 - SELECT grants on `"user"`, `repository`, `language_stat`, and 64 - `public_key`. 65 - ''; 66 - }; 67 - 68 - SNOT_REPO_ROOT = mkOption { 69 - type = types.str; 70 - default = "/var/lib/forgejo/repositories"; 71 - description = "Forgejo's repository storage directory."; 72 - }; 73 - 74 - SNOT_PUSH_REMOTE = mkOption { 75 - type = types.nullOr types.str; 76 - default = null; 77 - example = "git@git.example.com"; 78 - description = "SSH remote base suggested when an HTTP push is rejected."; 79 - }; 80 - 81 - SNOT_STATE_DIR = mkOption { 82 - type = types.str; 83 - default = "/var/lib/snot"; 84 - description = "Directory holding the signing key and rkey map."; 85 - }; 86 - 87 - SNOT_PLC_URL = mkOption { 88 - type = types.str; 89 - default = "https://plc.directory"; 90 - description = "URL of the DID PLC directory."; 91 - }; 92 - }; 93 - }; 94 39 }; 95 40 96 41 forgejoGroup = mkOption { 97 - type = types.str; 42 + type = lib.types.str; 98 43 default = "forgejo"; 99 44 description = "Group with read access to the repository root."; 100 45 }; 101 46 102 47 environmentFiles = mkOption { 103 - type = types.listOf types.path; 48 + type = lib.types.listOf lib.types.path; 104 49 default = [ ]; 105 50 description = '' 106 - Files to load environment variables from. Loaded variables override 107 - values set in {option}`settings`; use them for secrets such as a 108 - `SNOT_DB_DSN` containing a password. 51 + Files to load environment variables from. `SNOT_*` variables override 52 + the corresponding TOML keys; use them for secrets such as a `db_dsn` 53 + with a password. 109 54 ''; 110 55 }; 111 56 }; ··· 121 66 path = [ pkgs.git ]; 122 67 123 68 serviceConfig = { 124 - ExecStart = "${getExe cfg.package} serve"; 125 - Environment = lib.mapAttrsToList (k: v: "${k}=${v}") ( 126 - lib.filterAttrs (_: v: v != null) cfg.settings 127 - ); 69 + ExecStart = "${getExe cfg.package} serve --config ${configFile}"; 128 70 EnvironmentFile = cfg.environmentFiles; 129 71 130 72 DynamicUser = true; ··· 140 82 PrivateDevices = true; 141 83 ProtectSystem = "strict"; 142 84 ProtectHome = true; 143 - ReadOnlyPaths = [ cfg.settings.SNOT_REPO_ROOT ]; 85 + ReadOnlyPaths = [ (cfg.settings.repo_root or "/var/lib/forgejo/repositories") ]; 144 86 ProtectKernelTunables = true; 145 87 ProtectKernelModules = true; 146 88 ProtectControlGroups = true;