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