feat(devops): add config loading

Loads ~/.core/config.yaml with image source preferences.
Defaults to auto-detection with host-uk/core-images.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Snider 2026-01-29 02:04:01 +00:00
parent cead09feb2
commit 7275961769
4 changed files with 138 additions and 0 deletions

86
pkg/devops/config.go Normal file
View file

@ -0,0 +1,86 @@
package devops
import (
"os"
"path/filepath"
"gopkg.in/yaml.v3"
)
// Config holds global devops configuration from ~/.core/config.yaml.
type Config struct {
Version int `yaml:"version"`
Images ImagesConfig `yaml:"images"`
}
// ImagesConfig holds image source configuration.
type ImagesConfig struct {
Source string `yaml:"source"` // auto, github, registry, cdn
GitHub GitHubConfig `yaml:"github,omitempty"`
Registry RegistryConfig `yaml:"registry,omitempty"`
CDN CDNConfig `yaml:"cdn,omitempty"`
}
// GitHubConfig holds GitHub Releases configuration.
type GitHubConfig struct {
Repo string `yaml:"repo"` // owner/repo format
}
// RegistryConfig holds container registry configuration.
type RegistryConfig struct {
Image string `yaml:"image"` // e.g., ghcr.io/host-uk/core-devops
}
// CDNConfig holds CDN/S3 configuration.
type CDNConfig struct {
URL string `yaml:"url"` // base URL for downloads
}
// DefaultConfig returns sensible defaults.
func DefaultConfig() *Config {
return &Config{
Version: 1,
Images: ImagesConfig{
Source: "auto",
GitHub: GitHubConfig{
Repo: "host-uk/core-images",
},
Registry: RegistryConfig{
Image: "ghcr.io/host-uk/core-devops",
},
},
}
}
// ConfigPath returns the path to the config file.
func ConfigPath() (string, error) {
home, err := os.UserHomeDir()
if err != nil {
return "", err
}
return filepath.Join(home, ".core", "config.yaml"), nil
}
// LoadConfig loads configuration from ~/.core/config.yaml.
// Returns default config if file doesn't exist.
func LoadConfig() (*Config, error) {
configPath, err := ConfigPath()
if err != nil {
return DefaultConfig(), nil
}
data, err := os.ReadFile(configPath)
if err != nil {
if os.IsNotExist(err) {
return DefaultConfig(), nil
}
return nil, err
}
cfg := DefaultConfig()
if err := yaml.Unmarshal(data, cfg); err != nil {
return nil, err
}
return cfg, nil
}

48
pkg/devops/config_test.go Normal file
View file

@ -0,0 +1,48 @@
package devops
import (
"os"
"path/filepath"
"testing"
)
func TestLoadConfig_Good_Default(t *testing.T) {
// Use temp home dir
tmpDir := t.TempDir()
t.Setenv("HOME", tmpDir)
cfg, err := LoadConfig()
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if cfg.Images.Source != "auto" {
t.Errorf("expected source 'auto', got %q", cfg.Images.Source)
}
}
func TestLoadConfig_Good_FromFile(t *testing.T) {
tmpDir := t.TempDir()
t.Setenv("HOME", tmpDir)
configDir := filepath.Join(tmpDir, ".core")
os.MkdirAll(configDir, 0755)
configContent := `version: 1
images:
source: github
github:
repo: myorg/images
`
os.WriteFile(filepath.Join(configDir, "config.yaml"), []byte(configContent), 0644)
cfg, err := LoadConfig()
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if cfg.Images.Source != "github" {
t.Errorf("expected source 'github', got %q", cfg.Images.Source)
}
if cfg.Images.GitHub.Repo != "myorg/images" {
t.Errorf("expected repo 'myorg/images', got %q", cfg.Images.GitHub.Repo)
}
}

View file

@ -4,4 +4,6 @@ go 1.25
require github.com/host-uk/core v0.0.0
require gopkg.in/yaml.v3 v3.0.1 // indirect
replace github.com/host-uk/core => ../..

View file

@ -1,4 +1,6 @@
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=