- Config loader with env var expansion and validation - SQLite database with tasks and toolchain_versions tables - Gitea REST API client for repository and branch operations - K8s client for Job lifecycle management (with noop fallback for local dev) - REST API: tasks CRUD, retry, plugins, version management - Worker/scheduler with polling, concurrency control, and job tracking - Static frontend: generate, tasks, settings pages - Toolchain images: Go and Node.js Dockerfiles with entrypoint scripts - Helm chart with deployment, service, configmap, ingress, PVC - Multi-stage Dockerfile for service - Unit tests for config, database, gitclient, k8s, api, worker - E2E tests for idempotency, version switch, failure recovery
120 lines
2.6 KiB
Go
120 lines
2.6 KiB
Go
package config
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"regexp"
|
|
"strings"
|
|
|
|
"gopkg.in/yaml.v3"
|
|
)
|
|
|
|
type Config struct {
|
|
Server ServerConfig `yaml:"server"`
|
|
Git GitConfig `yaml:"git"`
|
|
Cluster ClusterConfig `yaml:"cluster"`
|
|
ToolchainRegistry string `yaml:"toolchain_registry"`
|
|
TargetRepoTemplate string `yaml:"target_repo_template"`
|
|
Plugins []PluginConfig `yaml:"plugins"`
|
|
}
|
|
|
|
type ServerConfig struct {
|
|
Addr string `yaml:"addr"`
|
|
PublicURL string `yaml:"public_url"`
|
|
}
|
|
|
|
type GitConfig struct {
|
|
Type string `yaml:"type"`
|
|
BaseURL string `yaml:"base_url"`
|
|
Token string `yaml:"token"`
|
|
}
|
|
|
|
type ClusterConfig struct {
|
|
Kubeconfig string `yaml:"kubeconfig"`
|
|
Namespace string `yaml:"namespace"`
|
|
JobTimeout int `yaml:"job_timeout"`
|
|
MaxConcurrent int `yaml:"max_concurrent"`
|
|
}
|
|
|
|
type PluginConfig struct {
|
|
Language string `yaml:"language"`
|
|
Image string `yaml:"image"`
|
|
}
|
|
|
|
var envVarRegex = regexp.MustCompile(`\$\{(\w+)\}`)
|
|
|
|
func Load(path string) (*Config, error) {
|
|
data, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("reading config file: %w", err)
|
|
}
|
|
|
|
expanded := expandEnvVars(string(data))
|
|
|
|
var cfg Config
|
|
if err := yaml.Unmarshal([]byte(expanded), &cfg); err != nil {
|
|
return nil, fmt.Errorf("parsing config: %w", err)
|
|
}
|
|
|
|
if err := cfg.validate(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &cfg, nil
|
|
}
|
|
|
|
func expandEnvVars(s string) string {
|
|
return envVarRegex.ReplaceAllStringFunc(s, func(match string) string {
|
|
varName := match[2 : len(match)-1]
|
|
if val, ok := os.LookupEnv(varName); ok {
|
|
return val
|
|
}
|
|
return match
|
|
})
|
|
}
|
|
|
|
func (c *Config) validate() error {
|
|
if c.Git.Type == "" {
|
|
return fmt.Errorf("git.type is required")
|
|
}
|
|
if c.Git.BaseURL == "" {
|
|
return fmt.Errorf("git.base_url is required")
|
|
}
|
|
if c.Git.Token == "" {
|
|
return fmt.Errorf("git.token is required")
|
|
}
|
|
if c.Cluster.Namespace == "" {
|
|
return fmt.Errorf("cluster.namespace is required")
|
|
}
|
|
if c.Cluster.JobTimeout == 0 {
|
|
c.Cluster.JobTimeout = 600
|
|
}
|
|
if c.Cluster.MaxConcurrent == 0 {
|
|
c.Cluster.MaxConcurrent = 2
|
|
}
|
|
if c.Server.Addr == "" {
|
|
c.Server.Addr = ":8080"
|
|
}
|
|
if c.TargetRepoTemplate == "" {
|
|
c.TargetRepoTemplate = "{proto_repo}-gen-{lang}"
|
|
}
|
|
for i, p := range c.Plugins {
|
|
if p.Language == "" {
|
|
return fmt.Errorf("plugins[%d].language is required", i)
|
|
}
|
|
if p.Image == "" {
|
|
return fmt.Errorf("plugins[%d].image is required", i)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (c *Config) GetPluginByLanguage(lang string) *PluginConfig {
|
|
for i := range c.Plugins {
|
|
if strings.EqualFold(c.Plugins[i].Language, lang) {
|
|
return &c.Plugins[i]
|
|
}
|
|
}
|
|
return nil
|
|
}
|