package config import ( "fmt" "os" "strconv" "strings" "time" ) type Config struct { DataDir string RepoRoots []string HTTPAddr string BaseURL string InboxDir string PodmanSocket string SSHAddr string SSHAuthorizedKeys string ThemeCSS string DefaultTimeout time.Duration DefaultMemory string DefaultCPUs string CacheMaxVersions int CacheMaxBytes int64 LogLevel string PublishRegistryAuthFile string PublishPkgRoot string PublishSiteRoot string } func Load() (Config, error) { var cfg Config var err error if cfg.DataDir, err = required("LUCI_DATA_DIR"); err != nil { return Config{}, err } repoRoots, err := required("LUCI_REPO_ROOTS") if err != nil { return Config{}, err } cfg.RepoRoots = splitList(repoRoots) if cfg.HTTPAddr, err = required("LUCI_HTTP_ADDR"); err != nil { return Config{}, err } if cfg.BaseURL, err = required("LUCI_BASE_URL"); err != nil { return Config{}, err } if cfg.InboxDir, err = required("LUCI_INBOX_DIR"); err != nil { return Config{}, err } if cfg.PodmanSocket, err = required("LUCI_PODMAN_SOCKET"); err != nil { return Config{}, err } cfg.SSHAddr = optional("LUCI_SSH_ADDR", "") cfg.SSHAuthorizedKeys = optional("LUCI_SSH_AUTHORIZED_KEYS", "") cfg.ThemeCSS = optional("LUCI_THEME_CSS", "") cfg.DefaultMemory = optional("LUCI_DEFAULT_MEMORY", "2g") cfg.DefaultCPUs = optional("LUCI_DEFAULT_CPUS", "2") cfg.LogLevel = optional("LUCI_LOG_LEVEL", "info") cfg.PublishRegistryAuthFile = optional("LUCI_PUBLISH_REGISTRY_AUTH_FILE", "") cfg.PublishPkgRoot = optional("LUCI_PUBLISH_PKG_ROOT", "") cfg.PublishSiteRoot = optional("LUCI_PUBLISH_SITE_ROOT", "") timeoutText := optional("LUCI_DEFAULT_TIMEOUT", "20m") cfg.DefaultTimeout, err = time.ParseDuration(timeoutText) if err != nil { return Config{}, fmt.Errorf("invalid LUCI_DEFAULT_TIMEOUT: %w", err) } if versionsText := optional("LUCI_CACHE_MAX_VERSIONS", "3"); versionsText != "" { cfg.CacheMaxVersions, err = strconv.Atoi(versionsText) if err != nil || cfg.CacheMaxVersions < 1 { return Config{}, fmt.Errorf("invalid LUCI_CACHE_MAX_VERSIONS: %q", versionsText) } } if bytesText := optional("LUCI_CACHE_MAX_BYTES", "2GiB"); bytesText != "" { cfg.CacheMaxBytes, err = parseBytes(bytesText) if err != nil { return Config{}, fmt.Errorf("invalid LUCI_CACHE_MAX_BYTES: %w", err) } } return cfg, nil } // parseBytes accepts a plain byte count or a binary suffix (KiB, MiB, GiB, // TiB, and their one-letter forms). func parseBytes(text string) (int64, error) { units := map[string]int64{"": 1, "k": 1 << 10, "m": 1 << 20, "g": 1 << 30, "t": 1 << 40} trimmed := strings.ToLower(strings.TrimSpace(text)) if trimmed == "" { return 0, fmt.Errorf("invalid byte size %q", text) } for _, suffix := range []string{"kib", "mib", "gib", "tib"} { if strings.HasSuffix(trimmed, suffix) { trimmed = strings.TrimSuffix(trimmed, suffix) + suffix[:1] break } } trimmed = strings.TrimSuffix(trimmed, "b") unit := "" if last := trimmed[len(trimmed)-1]; last >= 'a' && last <= 'z' { if _, ok := units[string(last)]; ok { unit = string(last) trimmed = trimmed[:len(trimmed)-1] } } value, err := strconv.ParseInt(trimmed, 10, 64) if err != nil || value < 0 || value > (1<<63-1)/units[unit] { return 0, fmt.Errorf("invalid byte size %q", text) } return value * units[unit], nil } func required(name string) (string, error) { value, ok := os.LookupEnv(name) if !ok || strings.TrimSpace(value) == "" { return "", fmt.Errorf("missing required env %s", name) } return value, nil } func optional(name string, fallback string) string { value, ok := os.LookupEnv(name) if !ok || strings.TrimSpace(value) == "" { return fallback } return value } func splitList(value string) []string { parts := strings.Split(value, ":") out := make([]string, 0, len(parts)) for _, part := range parts { part = strings.TrimSpace(part) if part != "" { out = append(out, part) } } return out }