package main import ( "encoding/hex" "fmt" "os" "github.com/BurntSushi/toml" ) type rawConfig struct { Schema string `toml:"schema"` Config struct { GitLabURL string `toml:"gitlab_url"` GitLabAPIKey string `toml:"gitlab_api_key"` } `toml:"config"` Customers map[string]rawCustomer `toml:"customers"` } type rawCustomer struct { Key string `toml:"key"` Repos []string `toml:"repos"` } type ResolvedConfig struct { GitLabURL string GitLabAPIKey string Customers map[string]ResolvedCustomer } type ResolvedCustomer struct { Key []byte Repos []string } func LoadConfig(path string) (*ResolvedConfig, error) { data, err := os.ReadFile(path) if err != nil { return nil, fmt.Errorf("reading config: %w", err) } expanded := os.Expand(string(data), os.Getenv) var raw rawConfig if err := toml.Unmarshal([]byte(expanded), &raw); err != nil { return nil, fmt.Errorf("parsing config: %w", err) } if err := validateRawConfig(&raw); err != nil { return nil, err } resolved := &ResolvedConfig{ GitLabURL: raw.Config.GitLabURL, GitLabAPIKey: raw.Config.GitLabAPIKey, Customers: make(map[string]ResolvedCustomer, len(raw.Customers)), } for name, cust := range raw.Customers { key, err := hex.DecodeString(cust.Key) if err != nil { return nil, fmt.Errorf("customer %q: invalid hex key: %w", name, err) } resolved.Customers[name] = ResolvedCustomer{ Key: key, Repos: cust.Repos, } } return resolved, nil } func validateRawConfig(raw *rawConfig) error { if raw.Schema != "v1" { return fmt.Errorf("unsupported schema: %q (expected \"v1\")", raw.Schema) } if raw.Config.GitLabURL == "" { return fmt.Errorf("config.gitlab_url is required") } if raw.Config.GitLabAPIKey == "" { return fmt.Errorf("config.gitlab_api_key is required") } if len(raw.Customers) == 0 { return fmt.Errorf("at least one customer is required") } for name, cust := range raw.Customers { if len(cust.Key) != 64 { return fmt.Errorf("customer %q: key must be 64 hex characters (got %d)", name, len(cust.Key)) } if len(cust.Repos) == 0 { return fmt.Errorf("customer %q: repos list must not be empty", name) } } return nil }