config.go
| 1 | // Package config reads all settings from environment variables. |
| 2 | // Names and defaults match the table in the README. |
| 3 | package config |
| 4 | |
| 5 | import ( |
| 6 | "fmt" |
| 7 | "log" |
| 8 | "net/url" |
| 9 | "os" |
| 10 | "path/filepath" |
| 11 | "strconv" |
| 12 | ) |
| 13 | |
| 14 | type 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. |
| 54 | func 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 | |
| 70 | func 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. |
| 79 | func 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 | |
| 89 | func 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. |
| 152 | func (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. |
| 163 | func (c *Config) DBPath() string { return filepath.Join(c.DataDir, "hearthforge.db") } |
| 164 | func (c *Config) ReposDir() string { return filepath.Join(c.DataDir, "repos") } |
| 165 | func (c *Config) AvatarsDir() string { return filepath.Join(c.DataDir, "avatars") } |
| 166 | func (c *Config) ReleasesDir() string { return filepath.Join(c.DataDir, "releases") } |
| 167 | func (c *Config) AllowedSignersPath() string { return filepath.Join(c.DataDir, "allowed_signers") } |
| 168 | func (c *Config) CIArtifactsDir() string { return filepath.Join(c.DataDir, "ci", "artifacts") } |
| 169 | func (c *Config) RegistryDir() string { return filepath.Join(c.DataDir, "registry") } |
| 170 |