Skip to content

Commit f028895

Browse files
committed
adding stuff
1 parent 331ecbf commit f028895

2 files changed

Lines changed: 296 additions & 0 deletions

File tree

internal/config/config.go

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
package config
2+
3+
import (
4+
"bytes"
5+
"fmt"
6+
"os"
7+
"strings"
8+
9+
"gopkg.in/yaml.v3"
10+
)
11+
12+
// FileConfig represents the top-level YAML configuration file.
13+
type FileConfig struct {
14+
WorkerID string `yaml:"worker_id"`
15+
Cleanup *bool `yaml:"cleanup"`
16+
Backend BackendConfig `yaml:"backend"`
17+
}
18+
19+
// BackendConfig contains the backend selection.
20+
type BackendConfig struct {
21+
Docker *DockerConfig `yaml:"docker"`
22+
}
23+
24+
// DockerConfig holds Docker-backend-specific configuration.
25+
type DockerConfig struct {
26+
Volumes []string `yaml:"volumes"`
27+
Environment []EnvEntry `yaml:"environment"`
28+
}
29+
30+
// EnvEntry represents a single environment variable in the config file.
31+
// If Value is nil, the variable is inherited from the host process environment.
32+
type EnvEntry struct {
33+
Name string `yaml:"name"`
34+
Value *string `yaml:"value"`
35+
}
36+
37+
// Load reads and validates a YAML config file.
38+
func Load(path string) (*FileConfig, error) {
39+
data, err := os.ReadFile(path)
40+
if err != nil {
41+
return nil, fmt.Errorf("failed to read config file: %w", err)
42+
}
43+
44+
var cfg FileConfig
45+
decoder := yaml.NewDecoder(bytes.NewReader(data))
46+
decoder.KnownFields(true)
47+
if err := decoder.Decode(&cfg); err != nil {
48+
return nil, fmt.Errorf("failed to parse config file: %w", err)
49+
}
50+
51+
if err := cfg.validate(); err != nil {
52+
return nil, fmt.Errorf("invalid config: %w", err)
53+
}
54+
55+
return &cfg, nil
56+
}
57+
58+
func (c *FileConfig) validate() error {
59+
if c.Backend.Docker != nil {
60+
if err := validateEnvEntries(c.Backend.Docker.Environment); err != nil {
61+
return fmt.Errorf("docker.environment: %w", err)
62+
}
63+
}
64+
65+
return nil
66+
}
67+
68+
func validateEnvEntries(entries []EnvEntry) error {
69+
for i, entry := range entries {
70+
if entry.Name == "" {
71+
return fmt.Errorf("entry %d: name is required", i)
72+
}
73+
if strings.ContainsAny(entry.Name, " \t") {
74+
return fmt.Errorf("entry %d: name %q contains whitespace", i, entry.Name)
75+
}
76+
}
77+
return nil
78+
}
79+
80+
// ResolveEnv converts environment entries to a map, resolving host-inherited values.
81+
func ResolveEnv(entries []EnvEntry) map[string]string {
82+
result := make(map[string]string, len(entries))
83+
for _, entry := range entries {
84+
if entry.Value != nil {
85+
result[entry.Name] = *entry.Value
86+
} else {
87+
result[entry.Name] = os.Getenv(entry.Name)
88+
}
89+
}
90+
return result
91+
}

internal/config/config_test.go

Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,205 @@
1+
package config
2+
3+
import (
4+
"os"
5+
"path/filepath"
6+
"testing"
7+
)
8+
9+
func writeTestConfig(t *testing.T, content string) string {
10+
t.Helper()
11+
path := filepath.Join(t.TempDir(), "config.yaml")
12+
if err := os.WriteFile(path, []byte(content), 0644); err != nil {
13+
t.Fatalf("failed to write test config: %v", err)
14+
}
15+
return path
16+
}
17+
18+
func TestLoadValidDockerConfig(t *testing.T) {
19+
path := writeTestConfig(t, `
20+
worker_id: "my-worker"
21+
cleanup: false
22+
backend:
23+
docker:
24+
volumes:
25+
- "/data:/data:ro"
26+
- "/cache:/cache"
27+
environment:
28+
- name: FOO
29+
value: "bar"
30+
- name: BAZ
31+
value: "qux"
32+
`)
33+
34+
cfg, err := Load(path)
35+
if err != nil {
36+
t.Fatalf("unexpected error: %v", err)
37+
}
38+
39+
if cfg.WorkerID != "my-worker" {
40+
t.Errorf("worker_id = %q, want %q", cfg.WorkerID, "my-worker")
41+
}
42+
if cfg.Cleanup == nil || *cfg.Cleanup != false {
43+
t.Errorf("cleanup = %v, want false", cfg.Cleanup)
44+
}
45+
if cfg.Backend.Docker == nil {
46+
t.Fatal("expected docker backend to be set")
47+
}
48+
if len(cfg.Backend.Docker.Volumes) != 2 {
49+
t.Errorf("volumes count = %d, want 2", len(cfg.Backend.Docker.Volumes))
50+
}
51+
if len(cfg.Backend.Docker.Environment) != 2 {
52+
t.Errorf("environment count = %d, want 2", len(cfg.Backend.Docker.Environment))
53+
}
54+
}
55+
56+
func TestLoadMinimalConfig(t *testing.T) {
57+
path := writeTestConfig(t, `
58+
worker_id: "minimal"
59+
`)
60+
61+
cfg, err := Load(path)
62+
if err != nil {
63+
t.Fatalf("unexpected error: %v", err)
64+
}
65+
66+
if cfg.WorkerID != "minimal" {
67+
t.Errorf("worker_id = %q, want %q", cfg.WorkerID, "minimal")
68+
}
69+
if cfg.Cleanup != nil {
70+
t.Errorf("cleanup should be nil when not specified, got %v", *cfg.Cleanup)
71+
}
72+
if cfg.Backend.Docker != nil {
73+
t.Error("docker backend should be nil when not specified")
74+
}
75+
}
76+
77+
func TestLoadCleanupDefaultTrue(t *testing.T) {
78+
path := writeTestConfig(t, `
79+
worker_id: "test"
80+
cleanup: true
81+
`)
82+
83+
cfg, err := Load(path)
84+
if err != nil {
85+
t.Fatalf("unexpected error: %v", err)
86+
}
87+
88+
if cfg.Cleanup == nil || *cfg.Cleanup != true {
89+
t.Errorf("cleanup = %v, want true", cfg.Cleanup)
90+
}
91+
}
92+
93+
func TestLoadInvalidYAML(t *testing.T) {
94+
path := writeTestConfig(t, `
95+
worker_id: "test"
96+
bad_indent: true
97+
`)
98+
99+
_, err := Load(path)
100+
if err == nil {
101+
t.Fatal("expected error for invalid YAML")
102+
}
103+
}
104+
105+
func TestLoadUnknownField(t *testing.T) {
106+
path := writeTestConfig(t, `
107+
worker_id: "test"
108+
unknown_field: "value"
109+
`)
110+
111+
_, err := Load(path)
112+
if err == nil {
113+
t.Fatal("expected error for unknown field")
114+
}
115+
}
116+
117+
func TestLoadEmptyEnvName(t *testing.T) {
118+
path := writeTestConfig(t, `
119+
backend:
120+
docker:
121+
environment:
122+
- name: ""
123+
value: "bar"
124+
`)
125+
126+
_, err := Load(path)
127+
if err == nil {
128+
t.Fatal("expected error for empty env name")
129+
}
130+
}
131+
132+
func TestLoadEnvNameWithWhitespace(t *testing.T) {
133+
path := writeTestConfig(t, `
134+
backend:
135+
docker:
136+
environment:
137+
- name: "MY VAR"
138+
value: "bar"
139+
`)
140+
141+
_, err := Load(path)
142+
if err == nil {
143+
t.Fatal("expected error for env name with whitespace")
144+
}
145+
}
146+
147+
func TestLoadEnvInheritFromHost(t *testing.T) {
148+
path := writeTestConfig(t, `
149+
backend:
150+
docker:
151+
environment:
152+
- name: MY_HOST_VAR
153+
`)
154+
155+
cfg, err := Load(path)
156+
if err != nil {
157+
t.Fatalf("unexpected error: %v", err)
158+
}
159+
160+
env := cfg.Backend.Docker.Environment[0]
161+
if env.Name != "MY_HOST_VAR" {
162+
t.Errorf("name = %q, want %q", env.Name, "MY_HOST_VAR")
163+
}
164+
if env.Value != nil {
165+
t.Errorf("value should be nil for host-inherited var, got %q", *env.Value)
166+
}
167+
}
168+
169+
func TestResolveEnv(t *testing.T) {
170+
explicit := "explicit_value"
171+
entries := []EnvEntry{
172+
{Name: "EXPLICIT", Value: &explicit},
173+
{Name: "FROM_HOST"},
174+
}
175+
176+
t.Setenv("FROM_HOST", "host_value")
177+
178+
result := ResolveEnv(entries)
179+
180+
if result["EXPLICIT"] != "explicit_value" {
181+
t.Errorf("EXPLICIT = %q, want %q", result["EXPLICIT"], "explicit_value")
182+
}
183+
if result["FROM_HOST"] != "host_value" {
184+
t.Errorf("FROM_HOST = %q, want %q", result["FROM_HOST"], "host_value")
185+
}
186+
}
187+
188+
func TestResolveEnvMissingHostVar(t *testing.T) {
189+
entries := []EnvEntry{
190+
{Name: "NONEXISTENT_VAR_12345"},
191+
}
192+
193+
result := ResolveEnv(entries)
194+
195+
if result["NONEXISTENT_VAR_12345"] != "" {
196+
t.Errorf("expected empty string for missing host var, got %q", result["NONEXISTENT_VAR_12345"])
197+
}
198+
}
199+
200+
func TestLoadFileNotFound(t *testing.T) {
201+
_, err := Load("/nonexistent/path/config.yaml")
202+
if err == nil {
203+
t.Fatal("expected error for missing file")
204+
}
205+
}

0 commit comments

Comments
 (0)