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