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 "strings"
13)
14
15type Config struct {
16 Port int
17 SSHPort int
18 DataDir string
19 OwnerDisplayName string
20 BaseURL string
21 PublicHTTPS bool
22 PublicOrigin string
23 PublicHost string // host[:port] of BaseURL, what image names start with
24 RegistrationType string // enabled | disabled | queue
25 RegisterQuestion string
26 MaxUploadBytes int64
27 MaxUserUploadBytes int64
28 InlineMaxBytes int64
29 MaxRenderBytes int64
30 MaxRawDownloadBytes int64
31 SSHDisabled bool
32 SSHHostKeyPath string
33 ScannedRepoPrivate bool
34 TrustedProxy bool
35 RateLimitDisabled bool
36 CommitterName string
37 CommitterEmail string
38 ExtraAllowedSigners string
39 MaxTitleBytes int
40 MaxTextBodyBytes int
41 MaxUsernameBytes int
42 MaxPasswordBytes int
43 CIDockerSocket string
44 CIMaxHistory int
45 CIDefaultTimeout int
46 CIMaxConcurrent int
47 CIMaxArtifactBytes int64
48 CINetwork string // engine network for CI containers, empty = engine default
49 CIVMImage string // empty = built from the embedded vm/
50 CIVMCPUs int
51 CIVMMemoryMB int64
52 CIVMDiskMB int64
53 CIMaxImageBytes int64
54 RegistryPull string // admin | users | public
55 MaxConcurrentArchives int
56}
57
58// intEnv returns def when unset or unparsable. min clamps the result;
59// pass it where 0 would break the feature instead of disabling it.
60func intEnv(key string, def, min int64) int64 {
61 v := os.Getenv(key)
62 if v == "" {
63 return def
64 }
65 n, err := strconv.ParseInt(v, 10, 64)
66 if err != nil {
67 log.Printf("config: %s=%q is not a number, using %d", key, v, def)
68 return def
69 }
70 if n < min {
71 return min
72 }
73 return n
74}
75
76func strEnv(key, def string) string {
77 if v := os.Getenv(key); v != "" {
78 return v
79 }
80 return def
81}
82
83// boolEnv treats anything but "", "0" and "false" as true. A value that looks
84// like neither is almost always a typo, so it is logged.
85func boolEnv(key string) bool {
86 v := os.Getenv(key)
87 switch v {
88 case "", "0", "false", "1", "true":
89 default:
90 log.Printf("config: %s=%q is not a boolean, reading it as true", key, v)
91 }
92 return v != "" && v != "0" && v != "false"
93}
94
95func Load() (*Config, error) {
96 port := int(intEnv("PORT", 3000, 1))
97 owner := strEnv("OWNER_DISPLAY_NAME", "Admin")
98 dataDir, err := filepath.Abs(strEnv("DATA_DIR", "./data"))
99 if err != nil {
100 return nil, err
101 }
102 c := &Config{
103 Port: port,
104 SSHPort: int(intEnv("SSH_PORT", 2222, 1)),
105 DataDir: dataDir,
106 OwnerDisplayName: owner,
107 BaseURL: strEnv("BASE_URL", fmt.Sprintf("http://localhost:%d", port)),
108 RegistrationType: strEnv("REGISTRATION_TYPE", "enabled"),
109 RegisterQuestion: os.Getenv("REGISTER_QUESTION"),
110 MaxUploadBytes: intEnv("MAX_UPLOAD_BYTES", 10<<20, 0),
111 MaxUserUploadBytes: intEnv("MAX_USER_UPLOAD_BYTES", 2<<20, 0),
112 InlineMaxBytes: intEnv("INLINE_MAX_BYTES", 512<<10, 0),
113 MaxRenderBytes: intEnv("MAX_RENDER_BYTES", 10<<20, 0),
114 MaxRawDownloadBytes: intEnv("MAX_RAW_DOWNLOAD_BYTES", 0, 0),
115 SSHDisabled: boolEnv("SSH_DISABLED"),
116 SSHHostKeyPath: strEnv("SSH_HOST_KEY_PATH", filepath.Join(dataDir, "ssh_host_key")),
117 ScannedRepoPrivate: os.Getenv("SCANNED_REPO_PRIVATE") != "0" && os.Getenv("SCANNED_REPO_PRIVATE") != "false",
118 TrustedProxy: boolEnv("TRUSTED_PROXY"),
119 RateLimitDisabled: boolEnv("RATE_LIMIT_DISABLED"),
120 CommitterName: strEnv("COMMITTER_NAME", owner),
121 ExtraAllowedSigners: os.Getenv("EXTRA_ALLOWED_SIGNERS_PATH"),
122 MaxTitleBytes: int(intEnv("MAX_TITLE_BYTES", 500, 0)),
123 MaxTextBodyBytes: int(intEnv("MAX_TEXT_BODY_BYTES", 100_000, 0)),
124 MaxUsernameBytes: int(intEnv("MAX_USERNAME_BYTES", 64, 0)),
125 MaxPasswordBytes: int(intEnv("MAX_PASSWORD_BYTES", 1024, 0)),
126 CIDockerSocket: os.Getenv("CI_DOCKER_SOCKET"),
127 CIMaxHistory: int(intEnv("CI_MAX_HISTORY", 50, 1)),
128 CIDefaultTimeout: int(intEnv("CI_DEFAULT_TIMEOUT", 3600, 1)),
129 CIMaxConcurrent: int(intEnv("CI_MAX_CONCURRENT", 2, 1)),
130 CIMaxArtifactBytes: intEnv("CI_MAX_ARTIFACT_BYTES", 512<<20, 1),
131 CINetwork: os.Getenv("CI_NETWORK"),
132 CIVMImage: os.Getenv("CI_VM_IMAGE"),
133 CIVMCPUs: int(intEnv("CI_VM_CPUS", 2, 1)),
134 CIVMMemoryMB: intEnv("CI_VM_MEMORY_MB", 2048, 1024),
135 CIVMDiskMB: intEnv("CI_VM_DISK_MB", 20480, 1024),
136 CIMaxImageBytes: intEnv("CI_MAX_IMAGE_BYTES", 4<<30, 1),
137 RegistryPull: strEnv("REGISTRY_PULL", "admin"),
138 MaxConcurrentArchives: int(intEnv("MAX_CONCURRENT_ARCHIVE_JOBS", 2, 1)),
139 }
140 switch c.RegistrationType {
141 case "enabled", "disabled", "queue":
142 default:
143 return nil, fmt.Errorf("REGISTRATION_TYPE %q must be enabled, disabled or queue", c.RegistrationType)
144 }
145 switch c.RegistryPull {
146 case "admin", "users", "public":
147 default:
148 return nil, fmt.Errorf("REGISTRY_PULL %q must be admin, users or public", c.RegistryPull)
149 }
150 u, err := url.Parse(c.BaseURL)
151 if err != nil || u.Host == "" {
152 return nil, fmt.Errorf("BASE_URL %q is not a valid URL", c.BaseURL)
153 }
154 c.PublicHTTPS = u.Scheme == "https"
155 // Browsers send Origin in this canonical form. The CSRF check and
156 // WebAuthn compare it verbatim.
157 host := strings.ToLower(u.Host)
158 switch u.Scheme {
159 case "https":
160 host = strings.TrimSuffix(host, ":443")
161 case "http":
162 host = strings.TrimSuffix(host, ":80")
163 }
164 c.PublicOrigin = u.Scheme + "://" + host
165 c.PublicHost = host
166 c.CommitterEmail = strEnv("COMMITTER_EMAIL", owner+"@"+u.Hostname())
167 return c, nil
168}
169
170// CanPullImages applies REGISTRY_PULL. Images of private repositories are
171// admin-only whatever the setting says.
172func (c *Config) CanPullImages(isPrivate, authed, isAdmin bool) bool {
173 if isAdmin {
174 return true
175 }
176 if isPrivate {
177 return false
178 }
179 return c.RegistryPull == "public" || (c.RegistryPull == "users" && authed)
180}
181
182// Derived paths under DataDir.
183func (c *Config) DBPath() string { return filepath.Join(c.DataDir, "hearthforge.db") }
184func (c *Config) ReposDir() string { return filepath.Join(c.DataDir, "repos") }
185func (c *Config) AvatarsDir() string { return filepath.Join(c.DataDir, "avatars") }
186func (c *Config) ReleasesDir() string { return filepath.Join(c.DataDir, "releases") }
187func (c *Config) AllowedSignersPath() string { return filepath.Join(c.DataDir, "allowed_signers") }
188func (c *Config) CIArtifactsDir() string { return filepath.Join(c.DataDir, "ci", "artifacts") }
189func (c *Config) RegistryDir() string { return filepath.Join(c.DataDir, "registry") }
190