config.go
⎇
Raw
1// Package config reads all settings from environment variables.
2// Names and defaults match the table in the README.
3package config
4
5import (
6 "fmt"
7 "log"
8 "net/url"
9 "os"
10 "path/filepath"
11 "strconv"
12)
13
14type Config struct {
15 Port int
16 SSHPort int
17 DataDir string
18 OwnerDisplayName string
19 BaseURL string
20 PublicHTTPS bool
21 PublicOrigin string
22 RegistrationType string // enabled | disabled | queue
23 RegisterQuestion string
24 MaxUploadBytes int64
25 MaxUserUploadBytes int64
26 InlineMaxBytes int64
27 MaxRenderBytes int64
28 MaxRawDownloadBytes int64
29 SSHDisabled bool
30 SSHHostKeyPath string
31 ScannedRepoPrivate bool
32 TrustedProxy bool
33 RateLimitDisabled bool
34 CommitterName string
35 CommitterEmail string
36 ExtraAllowedSigners string
37 MaxTitleBytes int
38 MaxTextBodyBytes int
39 MaxUsernameBytes int
40 MaxPasswordBytes int
41 CIDockerSocket string
42 CIMaxHistory int
43 CIDefaultTimeout int
44 CIMaxConcurrent int
45 CIMaxArtifactBytes int64
46 MaxConcurrentArchives int
47}
48
49// intEnv returns def when unset or unparsable. min clamps the result;
50// pass it where 0 would break the feature instead of disabling it.
51func intEnv(key string, def, min int64) int64 {
52 v := os.Getenv(key)
53 if v == "" {
54 return def
55 }
56 n, err := strconv.ParseInt(v, 10, 64)
57 if err != nil {
58 log.Printf("config: %s=%q is not a number, using %d", key, v, def)
59 return def
60 }
61 if n < min {
62 return min
63 }
64 return n
65}
66
67func strEnv(key, def string) string {
68 if v := os.Getenv(key); v != "" {
69 return v
70 }
71 return def
72}
73
74// boolEnv treats anything but "", "0" and "false" as true. A value that looks
75// like neither is almost always a typo, so it is logged.
76func boolEnv(key string) bool {
77 v := os.Getenv(key)
78 switch v {
79 case "", "0", "false", "1", "true":
80 default:
81 log.Printf("config: %s=%q is not a boolean, reading it as true", key, v)
82 }
83 return v != "" && v != "0" && v != "false"
84}
85
86func Load() (*Config, error) {
87 port := int(intEnv("PORT", 3000, 1))
88 owner := strEnv("OWNER_DISPLAY_NAME", "Admin")
89 dataDir, err := filepath.Abs(strEnv("DATA_DIR", "./data"))
90 if err != nil {
91 return nil, err
92 }
93 c := &Config{
94 Port: port,
95 SSHPort: int(intEnv("SSH_PORT", 2222, 1)),
96 DataDir: dataDir,
97 OwnerDisplayName: owner,
98 BaseURL: strEnv("BASE_URL", fmt.Sprintf("http://localhost:%d", port)),
99 RegistrationType: strEnv("REGISTRATION_TYPE", "enabled"),
100 RegisterQuestion: os.Getenv("REGISTER_QUESTION"),
101 MaxUploadBytes: intEnv("MAX_UPLOAD_BYTES", 10<<20, 0),
102 MaxUserUploadBytes: intEnv("MAX_USER_UPLOAD_BYTES", 2<<20, 0),
103 InlineMaxBytes: intEnv("INLINE_MAX_BYTES", 512<<10, 0),
104 MaxRenderBytes: intEnv("MAX_RENDER_BYTES", 10<<20, 0),
105 MaxRawDownloadBytes: intEnv("MAX_RAW_DOWNLOAD_BYTES", 0, 0),
106 SSHDisabled: boolEnv("SSH_DISABLED"),
107 SSHHostKeyPath: strEnv("SSH_HOST_KEY_PATH", filepath.Join(dataDir, "ssh_host_key")),
108 ScannedRepoPrivate: os.Getenv("SCANNED_REPO_PRIVATE") != "0" && os.Getenv("SCANNED_REPO_PRIVATE") != "false",
109 TrustedProxy: boolEnv("TRUSTED_PROXY"),
110 RateLimitDisabled: boolEnv("RATE_LIMIT_DISABLED"),
111 CommitterName: strEnv("COMMITTER_NAME", owner),
112 ExtraAllowedSigners: os.Getenv("EXTRA_ALLOWED_SIGNERS_PATH"),
113 MaxTitleBytes: int(intEnv("MAX_TITLE_BYTES", 500, 0)),
114 MaxTextBodyBytes: int(intEnv("MAX_TEXT_BODY_BYTES", 100_000, 0)),
115 MaxUsernameBytes: int(intEnv("MAX_USERNAME_BYTES", 64, 0)),
116 MaxPasswordBytes: int(intEnv("MAX_PASSWORD_BYTES", 1024, 0)),
117 CIDockerSocket: os.Getenv("CI_DOCKER_SOCKET"),
118 CIMaxHistory: int(intEnv("CI_MAX_HISTORY", 50, 1)),
119 CIDefaultTimeout: int(intEnv("CI_DEFAULT_TIMEOUT", 3600, 1)),
120 CIMaxConcurrent: int(intEnv("CI_MAX_CONCURRENT", 2, 1)),
121 CIMaxArtifactBytes: intEnv("CI_MAX_ARTIFACT_BYTES", 512<<20, 1),
122 MaxConcurrentArchives: int(intEnv("MAX_CONCURRENT_ARCHIVE_JOBS", 2, 1)),
123 }
124 switch c.RegistrationType {
125 case "enabled", "disabled", "queue":
126 default:
127 return nil, fmt.Errorf("REGISTRATION_TYPE %q must be enabled, disabled or queue", c.RegistrationType)
128 }
129 u, err := url.Parse(c.BaseURL)
130 if err != nil || u.Host == "" {
131 return nil, fmt.Errorf("BASE_URL %q is not a valid URL", c.BaseURL)
132 }
133 c.PublicHTTPS = u.Scheme == "https"
134 c.PublicOrigin = u.Scheme + "://" + u.Host
135 c.CommitterEmail = strEnv("COMMITTER_EMAIL", owner+"@"+u.Hostname())
136 return c, nil
137}
138
139// Derived paths under DataDir.
140func (c *Config) DBPath() string { return filepath.Join(c.DataDir, "hearthforge.db") }
141func (c *Config) ReposDir() string { return filepath.Join(c.DataDir, "repos") }
142func (c *Config) AvatarsDir() string { return filepath.Join(c.DataDir, "avatars") }
143func (c *Config) ReleasesDir() string { return filepath.Join(c.DataDir, "releases") }
144func (c *Config) AllowedSignersPath() string { return filepath.Join(c.DataDir, "allowed_signers") }
145func (c *Config) CIArtifactsDir() string { return filepath.Join(c.DataDir, "ci", "artifacts") }
146