repositories / bugabinga.net
bugabinga.net
personal infrastructure for bugabinga!
owned by admin
services/luci/internal/config/config.go
Rawpackage 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
BuildContextMaxBytes int64
BuildContextMaxFiles int
BuildCacheMaxBytes int64
BuildCacheMaxAge time.Duration
BuildCleanupTimeout time.Duration
BuildMinFreeBytes 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)
}
}
for _, setting := range []struct {
name string
value *int64
defaultValue string
}{
{"LUCI_BUILD_CONTEXT_MAX_BYTES", &cfg.BuildContextMaxBytes, "1GiB"},
{"LUCI_BUILD_CACHE_MAX_BYTES", &cfg.BuildCacheMaxBytes, "10GiB"},
{"LUCI_BUILD_MIN_FREE_BYTES", &cfg.BuildMinFreeBytes, "5GiB"},
} {
*setting.value, err = parseBytes(optional(setting.name, setting.defaultValue))
if err != nil {
return Config{}, fmt.Errorf("invalid %s: %w", setting.name, err)
}
if *setting.value < 1 {
return Config{}, fmt.Errorf("invalid %s: must be positive", setting.name)
}
}
if cfg.BuildContextMaxFiles, err = strconv.Atoi(optional("LUCI_BUILD_CONTEXT_MAX_FILES", "10000")); err != nil || cfg.BuildContextMaxFiles < 1 {
return Config{}, fmt.Errorf("invalid LUCI_BUILD_CONTEXT_MAX_FILES")
}
for _, setting := range []struct {
name string
value *time.Duration
defaultValue string
}{
{"LUCI_BUILD_CACHE_MAX_AGE", &cfg.BuildCacheMaxAge, "168h"},
{"LUCI_BUILD_CLEANUP_TIMEOUT", &cfg.BuildCleanupTimeout, "10s"},
} {
*setting.value, err = time.ParseDuration(optional(setting.name, setting.defaultValue))
if err != nil || *setting.value <= 0 {
return Config{}, fmt.Errorf("invalid %s", setting.name)
}
}
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
}