Files
browser/platform/config/config.go
T

188 lines
5.5 KiB
Go

package config
import (
"bufio"
"os"
"path/filepath"
"strconv"
"strings"
)
const defaultAddr = ":8080"
const defaultDataDir = ".platform-data"
const defaultStorageBackend = "file"
const defaultBuilderDockerBinary = "docker"
// defaultBuilderImage names an explicit toolchain version rather than a
// floating tag such as latest. Operators who want digest pinning override
// PLATFORM_BUILDER_IMAGE with an image@sha256:... reference.
const defaultBuilderImage = "browser-platform-distribution-builder:1.0.0"
const defaultBuilderTimeoutSeconds = 1800
type Config struct {
Addr string
StorageBackend string
MySQLDSN string
DataDir string
MetadataPath string
LogDir string
ArtifactDir string
LogBodyBackend string
BootstrapAdminEmail string
BootstrapAdminPassword string
SecretEnvelopeKey string
AIProviderMode string
BuilderDockerBinary string
BuilderImage string
BuilderSourceDir string
BuilderWorkspaceDir string
BuilderCacheDir string
BuilderTimeoutSeconds int
}
func Load() Config {
loadLocalEnvFiles()
addr := os.Getenv("PLATFORM_ADDR")
if addr == "" {
addr = defaultAddr
}
dataDir := strings.TrimSpace(os.Getenv("PLATFORM_DATA_DIR"))
if dataDir == "" {
dataDir = defaultDataDir
}
metadataPath := strings.TrimSpace(os.Getenv("PLATFORM_METADATA_PATH"))
if metadataPath == "" {
metadataPath = filepath.Join(dataDir, "metadata.json")
}
logDir := strings.TrimSpace(os.Getenv("PLATFORM_LOG_DIR"))
if logDir == "" {
logDir = filepath.Join(dataDir, "logs")
}
artifactDir := strings.TrimSpace(os.Getenv("PLATFORM_ARTIFACT_DIR"))
if artifactDir == "" {
artifactDir = filepath.Join(dataDir, "artifacts")
}
storageBackend := strings.TrimSpace(os.Getenv("PLATFORM_STORAGE_BACKEND"))
if storageBackend == "" {
storageBackend = defaultStorageBackend
}
logBodyBackend := strings.TrimSpace(os.Getenv("PLATFORM_LOG_BODY_BACKEND"))
builderWorkspaceDir := strings.TrimSpace(os.Getenv("PLATFORM_BUILDER_WORKSPACE_DIR"))
if builderWorkspaceDir == "" {
builderWorkspaceDir = filepath.Join(dataDir, "distribution-builds")
}
builderCacheDir := strings.TrimSpace(os.Getenv("PLATFORM_BUILDER_CACHE_DIR"))
if builderCacheDir == "" {
builderCacheDir = filepath.Join(dataDir, "distribution-build-cache")
}
return Config{
Addr: addr,
StorageBackend: storageBackend,
MySQLDSN: strings.TrimSpace(os.Getenv("PLATFORM_MYSQL_DSN")),
DataDir: dataDir,
MetadataPath: metadataPath,
LogDir: logDir,
ArtifactDir: artifactDir,
LogBodyBackend: logBodyBackend,
BootstrapAdminEmail: strings.TrimSpace(os.Getenv("PLATFORM_BOOTSTRAP_ADMIN_EMAIL")),
BootstrapAdminPassword: os.Getenv("PLATFORM_BOOTSTRAP_ADMIN_PASSWORD"),
SecretEnvelopeKey: os.Getenv("PLATFORM_SECRET_ENVELOPE_KEY"),
AIProviderMode: defaultString(strings.TrimSpace(os.Getenv("PLATFORM_AI_PROVIDER_MODE")), "live"),
BuilderDockerBinary: defaultString(strings.TrimSpace(os.Getenv("PLATFORM_BUILDER_DOCKER_BINARY")), defaultBuilderDockerBinary),
BuilderImage: defaultString(strings.TrimSpace(os.Getenv("PLATFORM_BUILDER_IMAGE")), defaultBuilderImage),
BuilderSourceDir: strings.TrimSpace(os.Getenv("PLATFORM_BUILDER_SOURCE_DIR")),
BuilderWorkspaceDir: builderWorkspaceDir,
BuilderCacheDir: builderCacheDir,
BuilderTimeoutSeconds: defaultPositiveInt(strings.TrimSpace(os.Getenv("PLATFORM_BUILDER_TIMEOUT_SECONDS")), defaultBuilderTimeoutSeconds),
}
}
func defaultPositiveInt(value string, fallback int) int {
parsed, err := strconv.Atoi(value)
if err != nil || parsed <= 0 {
return fallback
}
return parsed
}
func defaultString(value, fallback string) string {
if value == "" {
return fallback
}
return value
}
func loadLocalEnvFiles() {
candidates := []string{".env", filepath.Join("platform", ".env")}
for _, path := range candidates {
loadEnvFile(path)
}
}
func loadEnvFile(path string) {
file, err := os.Open(path)
if err != nil {
return
}
defer file.Close()
scanner := bufio.NewScanner(file)
for scanner.Scan() {
key, value, ok := parseEnvLine(scanner.Text())
if !ok {
continue
}
if _, exists := os.LookupEnv(key); exists {
continue
}
_ = os.Setenv(key, value)
}
}
func parseEnvLine(line string) (string, string, bool) {
line = strings.TrimSpace(line)
if line == "" || strings.HasPrefix(line, "#") {
return "", "", false
}
line = strings.TrimSpace(strings.TrimPrefix(line, "export "))
key, value, found := strings.Cut(line, "=")
if !found {
return "", "", false
}
key = strings.TrimSpace(key)
if key == "" || strings.ContainsAny(key, " \t") {
return "", "", false
}
value = strings.TrimSpace(stripInlineComment(strings.TrimSpace(value)))
if len(value) >= 2 {
if (value[0] == '"' && value[len(value)-1] == '"') || (value[0] == '\'' && value[len(value)-1] == '\'') {
value = value[1 : len(value)-1]
}
}
return key, value, true
}
func stripInlineComment(value string) string {
inSingleQuote := false
inDoubleQuote := false
for index, char := range value {
switch char {
case '\'':
if !inDoubleQuote {
inSingleQuote = !inSingleQuote
}
case '"':
if !inSingleQuote {
inDoubleQuote = !inDoubleQuote
}
case '#':
if !inSingleQuote && !inDoubleQuote && index > 0 && value[index-1] == ' ' {
return strings.TrimSpace(value[:index])
}
}
}
return value
}