1// Package config implements the configuration management use case.
2package config
3
4import (
5 "context"
6 "strings"
7 "sync"
8
9 "github.com/bnema/zerowrap"
10 "github.com/fsnotify/fsnotify"
11 "github.com/spf13/viper"
12
13 "gordon/internal/boundaries/out"
14 "gordon/internal/domain"
15)
16
17// Config holds the loaded configuration.
18type Config struct {
19 ServerPort int
20 RegistryPort int
21 RegistryDomain string
22 DataDir string
23 AutoRouteEnabled bool
24 NetworkIsolation bool
25 NetworkPrefix string
26 Routes map[string]string
27 ExternalRoutes map[string]string // domain -> "host:port"
28 RegistryAuthEnabled bool
29 RegistryAuthUsername string
30 RegistryAuthPassword string
31 VolumeAutoCreate bool
32 VolumePrefix string
33 VolumePreserve bool
34 NetworkGroups map[string][]string
35 Attachments map[string][]string
36}
37
38// Service implements the ConfigService interface.
39type Service struct {
40 viper *viper.Viper
41 eventBus out.EventPublisher
42 config Config
43 mu sync.RWMutex
44}
45
46// NewService creates a new config service.
47func NewService(v *viper.Viper, eventBus out.EventPublisher) *Service {
48 return &Service{
49 viper: v,
50 eventBus: eventBus,
51 }
52}
53
54// Load loads the configuration from the configured source.
55func (s *Service) Load(ctx context.Context) error {
56 ctx = zerowrap.CtxWithFields(ctx, map[string]any{
57 zerowrap.FieldLayer: "usecase",
58 zerowrap.FieldUseCase: "Load",
59 })
60 log := zerowrap.FromCtx(ctx)
61
62 s.mu.Lock()
63 defer s.mu.Unlock()
64
65 // Load configuration from viper
66 s.config = s.loadConfigValues()
67
68 // Load complex nested structures
69 s.config.Routes = loadStringMap(s.viper.Get("routes"))
70 s.config.ExternalRoutes = loadStringMap(s.viper.Get("external_routes"))
71 s.config.NetworkGroups = loadStringArrayMap(s.viper.Get("network_groups"))
72 s.config.Attachments = loadStringArrayMap(s.viper.Get("attachments"))
73
74 log.Info().
75 Int("server_port", s.config.ServerPort).
76 Int("registry_port", s.config.RegistryPort).
77 Int(zerowrap.FieldCount, len(s.config.Routes)).
78 Msg("configuration loaded")
79
80 return nil
81}
82
83// loadConfigValues loads simple config values from viper.
84func (s *Service) loadConfigValues() Config {
85 return Config{
86 ServerPort: s.viper.GetInt("server.port"),
87 RegistryPort: s.viper.GetInt("server.registry_port"),
88 RegistryDomain: s.viper.GetString("server.registry_domain"),
89 DataDir: s.viper.GetString("server.data_dir"),
90 AutoRouteEnabled: s.viper.GetBool("auto_route.enabled"),
91 NetworkIsolation: s.viper.GetBool("network_isolation.enabled"),
92 NetworkPrefix: s.viper.GetString("network_isolation.network_prefix"),
93 RegistryAuthEnabled: s.viper.GetBool("registry_auth.enabled"),
94 RegistryAuthUsername: s.viper.GetString("registry_auth.username"),
95 RegistryAuthPassword: s.viper.GetString("registry_auth.password"),
96 VolumeAutoCreate: s.viper.GetBool("volumes.auto_create"),
97 VolumePrefix: s.viper.GetString("volumes.prefix"),
98 VolumePreserve: s.viper.GetBool("volumes.preserve"),
99 Routes: make(map[string]string),
100 ExternalRoutes: make(map[string]string),
101 NetworkGroups: make(map[string][]string),
102 Attachments: make(map[string][]string),
103 }
104}
105
106// loadStringMap loads a map[string]string from a viper value.
107func loadStringMap(raw any) map[string]string {
108 result := make(map[string]string)
109 if raw == nil {
110 return result
111 }
112 m, ok := raw.(map[string]any)
113 if !ok {
114 return result
115 }
116 for k, v := range m {
117 if vs, ok := v.(string); ok {
118 result[k] = vs
119 }
120 }
121 return result
122}
123
124// loadStringArrayMap loads a map[string][]string from a viper value.
125func loadStringArrayMap(raw any) map[string][]string {
126 result := make(map[string][]string)
127 if raw == nil {
128 return result
129 }
130 m, ok := raw.(map[string]any)
131 if !ok {
132 return result
133 }
134 for k, v := range m {
135 if arr, ok := v.([]any); ok {
136 var strs []string
137 for _, item := range arr {
138 if s, ok := item.(string); ok {
139 strs = append(strs, s)
140 }
141 }
142 result[k] = strs
143 }
144 }
145 return result
146}
147
148// GetRoutes returns all configured routes.
149func (s *Service) GetRoutes(_ context.Context) []domain.Route {
150 s.mu.RLock()
151 defer s.mu.RUnlock()
152
153 var routes []domain.Route
154 for domainName, image := range s.config.Routes {
155 route := domain.Route{
156 Image: image,
157 HTTPS: true,
158 }
159
160 // Check if domain has http:// prefix
161 if strings.HasPrefix(domainName, "http://") {
162 route.Domain = strings.TrimPrefix(domainName, "http://")
163 route.HTTPS = false
164 } else {
165 route.Domain = domainName
166 }
167
168 routes = append(routes, route)
169 }
170
171 return routes
172}
173
174// AddRoute adds a new route to the configuration.
175func (s *Service) AddRoute(ctx context.Context, route domain.Route) error {
176 ctx = zerowrap.CtxWithFields(ctx, map[string]any{
177 zerowrap.FieldLayer: "usecase",
178 zerowrap.FieldUseCase: "AddRoute",
179 "domain": route.Domain,
180 })
181 log := zerowrap.FromCtx(ctx)
182
183 s.mu.Lock()
184 defer s.mu.Unlock()
185
186 if s.config.Routes == nil {
187 s.config.Routes = make(map[string]string)
188 }
189
190 s.config.Routes[route.Domain] = route.Image
191
192 log.Info().Str("image", route.Image).Msg("route added to configuration")
193 return nil
194}
195
196// UpdateRoute updates an existing route.
197func (s *Service) UpdateRoute(ctx context.Context, route domain.Route) error {
198 ctx = zerowrap.CtxWithFields(ctx, map[string]any{
199 zerowrap.FieldLayer: "usecase",
200 zerowrap.FieldUseCase: "UpdateRoute",
201 "domain": route.Domain,
202 })
203 log := zerowrap.FromCtx(ctx)
204
205 s.mu.Lock()
206 defer s.mu.Unlock()
207
208 if _, exists := s.config.Routes[route.Domain]; !exists {
209 return domain.ErrRouteNotFound
210 }
211
212 s.config.Routes[route.Domain] = route.Image
213
214 log.Info().Str("image", route.Image).Msg("route updated")
215 return nil
216}
217
218// RemoveRoute removes a route from the configuration.
219func (s *Service) RemoveRoute(ctx context.Context, domainName string) error {
220 ctx = zerowrap.CtxWithFields(ctx, map[string]any{
221 zerowrap.FieldLayer: "usecase",
222 zerowrap.FieldUseCase: "RemoveRoute",
223 "domain": domainName,
224 })
225 log := zerowrap.FromCtx(ctx)
226
227 s.mu.Lock()
228 defer s.mu.Unlock()
229
230 if _, exists := s.config.Routes[domainName]; !exists {
231 return domain.ErrRouteNotFound
232 }
233
234 delete(s.config.Routes, domainName)
235
236 log.Info().Msg("route removed")
237 return nil
238}
239
240// Watch starts watching for configuration changes.
241func (s *Service) Watch(ctx context.Context, onChange func()) error {
242 log := zerowrap.FromCtx(ctx)
243
244 s.viper.OnConfigChange(func(e fsnotify.Event) {
245 log.Info().Str("file", e.Name).Msg("config file changed")
246
247 if err := s.viper.ReadInConfig(); err != nil {
248 log.WrapErr(err, "failed to reload config")
249 return
250 }
251
252 if err := s.Load(ctx); err != nil {
253 log.WrapErr(err, "failed to load updated config")
254 return
255 }
256
257 // Publish config reload event
258 if s.eventBus != nil {
259 if err := s.eventBus.Publish(domain.EventConfigReload, domain.ConfigReloadPayload{
260 Source: "file",
261 }); err != nil {
262 log.WrapErr(err, "failed to publish config reload event")
263 }
264 }
265
266 if onChange != nil {
267 onChange()
268 }
269 })
270
271 s.viper.WatchConfig()
272 log.Info().Msg("watching for configuration changes")
273
274 return nil
275}
276
277// GetServerPort returns the configured server port.
278func (s *Service) GetServerPort() int {
279 s.mu.RLock()
280 defer s.mu.RUnlock()
281 return s.config.ServerPort
282}
283
284// GetRegistryPort returns the configured registry port.
285func (s *Service) GetRegistryPort() int {
286 s.mu.RLock()
287 defer s.mu.RUnlock()
288 return s.config.RegistryPort
289}
290
291// GetRegistryDomain returns the configured registry domain.
292func (s *Service) GetRegistryDomain() string {
293 s.mu.RLock()
294 defer s.mu.RUnlock()
295 return s.config.RegistryDomain
296}
297
298// GetDataDir returns the configured data directory.
299func (s *Service) GetDataDir() string {
300 s.mu.RLock()
301 defer s.mu.RUnlock()
302 return s.config.DataDir
303}
304
305// IsAutoRouteEnabled returns whether auto-route is enabled.
306func (s *Service) IsAutoRouteEnabled() bool {
307 s.mu.RLock()
308 defer s.mu.RUnlock()
309 return s.config.AutoRouteEnabled
310}
311
312// IsNetworkIsolationEnabled returns whether network isolation is enabled.
313func (s *Service) IsNetworkIsolationEnabled() bool {
314 s.mu.RLock()
315 defer s.mu.RUnlock()
316 return s.config.NetworkIsolation
317}
318
319// GetNetworkPrefix returns the prefix for created networks.
320func (s *Service) GetNetworkPrefix() string {
321 s.mu.RLock()
322 defer s.mu.RUnlock()
323 return s.config.NetworkPrefix
324}
325
326// GetConfig returns a copy of the current configuration.
327func (s *Service) GetConfig() Config {
328 s.mu.RLock()
329 defer s.mu.RUnlock()
330 return s.config
331}
332
333// GetRegistryAuthConfig returns registry authentication configuration.
334func (s *Service) GetRegistryAuthConfig() (enabled bool, username, password string) {
335 s.mu.RLock()
336 defer s.mu.RUnlock()
337 return s.config.RegistryAuthEnabled, s.config.RegistryAuthUsername, s.config.RegistryAuthPassword
338}
339
340// GetVolumeConfig returns volume configuration.
341func (s *Service) GetVolumeConfig() (autoCreate bool, prefix string, preserve bool) {
342 s.mu.RLock()
343 defer s.mu.RUnlock()
344 return s.config.VolumeAutoCreate, s.config.VolumePrefix, s.config.VolumePreserve
345}
346
347// GetNetworkGroups returns network group configuration.
348func (s *Service) GetNetworkGroups() map[string][]string {
349 s.mu.RLock()
350 defer s.mu.RUnlock()
351
352 result := make(map[string][]string)
353 for k, v := range s.config.NetworkGroups {
354 result[k] = append([]string{}, v...)
355 }
356 return result
357}
358
359// GetAttachments returns attachment configuration.
360func (s *Service) GetAttachments() map[string][]string {
361 s.mu.RLock()
362 defer s.mu.RUnlock()
363
364 result := make(map[string][]string)
365 for k, v := range s.config.Attachments {
366 result[k] = append([]string{}, v...)
367 }
368 return result
369}
370
371// GetExternalRoutes returns all configured external routes.
372func (s *Service) GetExternalRoutes() map[string]string {
373 s.mu.RLock()
374 defer s.mu.RUnlock()
375
376 result := make(map[string]string)
377 for k, v := range s.config.ExternalRoutes {
378 result[k] = v
379 }
380 return result
381}
382
383// ExtractDomainFromImageName extracts domain from image names like "myapp.bamen.dev:latest".
384func ExtractDomainFromImageName(imageName string) (string, bool) {
385 parts := strings.Split(imageName, ":")
386 imageNamePart := parts[0]
387
388 // Simple domain check - contains at least one dot and valid characters
389 if strings.Contains(imageNamePart, ".") && !strings.HasPrefix(imageNamePart, ".") && !strings.HasSuffix(imageNamePart, ".") {
390 // Additional check: should not look like a registry path
391 if !strings.Contains(imageNamePart, "/") || strings.Count(imageNamePart, ".") > 0 {
392 return imageNamePart, true
393 }
394 }
395
396 return "", false
397}