v2.1.0 0fb867d

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

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 7

Tags 106

↑↓ moveenter openesc close