v2.2.0 64d39bf

service.go RawHistoryFolder

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}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 8

Tags 106

↑↓ moveenter openesc close