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