detached 3dca810

fix(config): add Reload method to properly re-read config from disk

The /admin/reload endpoint was calling Load() without first calling
ReadInConfig(), meaning the config file wasn't being re-read from disk.
This caused the in-memory config to be out of sync with the file.

- Add Reload() method to ConfigService interface
- Implement Reload() in config service that calls ReadInConfig() then Load()
- Update handleReload to use Reload() instead of Load()
- Regenerate mocks
commit
3dca8105ce39e2c24f2814a5a511864c8d522cbf
author
brice · · 2026-01-18 08:02 UTC
parent
27a9e5c
signature
unsigned
3 files · +74 -0
M internal/boundaries/in/config.go +4 -0
@@ -11,6 +11,10 @@ type ConfigService interface {
1111 // Load loads the configuration from the configured source.
1212 Load(ctx context.Context) error
1313
14+ // Reload re-reads the configuration file from disk and loads it into memory.
15+ // This is different from Load() which only loads from the cached viper values.
16+ Reload(ctx context.Context) error
17+
1418 // GetRoutes returns all configured routes.
1519 GetRoutes(ctx context.Context) []domain.Route
1620
@@ -11,6 +11,10 @@ type ConfigService interface {
11 // Load loads the configuration from the configured source.11 // Load loads the configuration from the configured source.
12 Load(ctx context.Context) error12 Load(ctx context.Context) error
13 13
14+ // Reload re-reads the configuration file from disk and loads it into memory.
15+ // This is different from Load() which only loads from the cached viper values.
16+ Reload(ctx context.Context) error
17+
14 // GetRoutes returns all configured routes.18 // GetRoutes returns all configured routes.
15 GetRoutes(ctx context.Context) []domain.Route19 GetRoutes(ctx context.Context) []domain.Route
16 20
M internal/boundaries/in/mocks/mock_config_service.go +51 -0
@@ -805,6 +805,57 @@ func (_c *MockConfigService_Load_Call) RunAndReturn(run func(ctx context.Context
805805 return _c
806806 }
807807
808+// Reload provides a mock function for the type MockConfigService
809+func (_mock *MockConfigService) Reload(ctx context.Context) error {
810+ ret := _mock.Called(ctx)
811+
812+ if len(ret) == 0 {
813+ panic("no return value specified for Reload")
814+ }
815+
816+ var r0 error
817+ if returnFunc, ok := ret.Get(0).(func(context.Context) error); ok {
818+ r0 = returnFunc(ctx)
819+ } else {
820+ r0 = ret.Error(0)
821+ }
822+ return r0
823+}
824+
825+// MockConfigService_Reload_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Reload'
826+type MockConfigService_Reload_Call struct {
827+ *mock.Call
828+}
829+
830+// Reload is a helper method to define mock.On call
831+// - ctx context.Context
832+func (_e *MockConfigService_Expecter) Reload(ctx interface{}) *MockConfigService_Reload_Call {
833+ return &MockConfigService_Reload_Call{Call: _e.mock.On("Reload", ctx)}
834+}
835+
836+func (_c *MockConfigService_Reload_Call) Run(run func(ctx context.Context)) *MockConfigService_Reload_Call {
837+ _c.Call.Run(func(args mock.Arguments) {
838+ var arg0 context.Context
839+ if args[0] != nil {
840+ arg0 = args[0].(context.Context)
841+ }
842+ run(
843+ arg0,
844+ )
845+ })
846+ return _c
847+}
848+
849+func (_c *MockConfigService_Reload_Call) Return(err error) *MockConfigService_Reload_Call {
850+ _c.Call.Return(err)
851+ return _c
852+}
853+
854+func (_c *MockConfigService_Reload_Call) RunAndReturn(run func(ctx context.Context) error) *MockConfigService_Reload_Call {
855+ _c.Call.Return(run)
856+ return _c
857+}
858+
808859 // RemoveAttachment provides a mock function for the type MockConfigService
809860 func (_mock *MockConfigService) RemoveAttachment(ctx context.Context, domainOrGroup string, image string) error {
810861 ret := _mock.Called(ctx, domainOrGroup, image)
@@ -805,6 +805,57 @@ func (_c *MockConfigService_Load_Call) RunAndReturn(run func(ctx context.Context
805 return _c805 return _c
806 }806 }
807 807
808+// Reload provides a mock function for the type MockConfigService
809+func (_mock *MockConfigService) Reload(ctx context.Context) error {
810+ ret := _mock.Called(ctx)
811+
812+ if len(ret) == 0 {
813+ panic("no return value specified for Reload")
814+ }
815+
816+ var r0 error
817+ if returnFunc, ok := ret.Get(0).(func(context.Context) error); ok {
818+ r0 = returnFunc(ctx)
819+ } else {
820+ r0 = ret.Error(0)
821+ }
822+ return r0
823+}
824+
825+// MockConfigService_Reload_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Reload'
826+type MockConfigService_Reload_Call struct {
827+ *mock.Call
828+}
829+
830+// Reload is a helper method to define mock.On call
831+// - ctx context.Context
832+func (_e *MockConfigService_Expecter) Reload(ctx interface{}) *MockConfigService_Reload_Call {
833+ return &MockConfigService_Reload_Call{Call: _e.mock.On("Reload", ctx)}
834+}
835+
836+func (_c *MockConfigService_Reload_Call) Run(run func(ctx context.Context)) *MockConfigService_Reload_Call {
837+ _c.Call.Run(func(args mock.Arguments) {
838+ var arg0 context.Context
839+ if args[0] != nil {
840+ arg0 = args[0].(context.Context)
841+ }
842+ run(
843+ arg0,
844+ )
845+ })
846+ return _c
847+}
848+
849+func (_c *MockConfigService_Reload_Call) Return(err error) *MockConfigService_Reload_Call {
850+ _c.Call.Return(err)
851+ return _c
852+}
853+
854+func (_c *MockConfigService_Reload_Call) RunAndReturn(run func(ctx context.Context) error) *MockConfigService_Reload_Call {
855+ _c.Call.Return(run)
856+ return _c
857+}
858+
808 // RemoveAttachment provides a mock function for the type MockConfigService859 // RemoveAttachment provides a mock function for the type MockConfigService
809 func (_mock *MockConfigService) RemoveAttachment(ctx context.Context, domainOrGroup string, image string) error {860 func (_mock *MockConfigService) RemoveAttachment(ctx context.Context, domainOrGroup string, image string) error {
810 ret := _mock.Called(ctx, domainOrGroup, image)861 ret := _mock.Called(ctx, domainOrGroup, image)
M internal/usecase/config/service.go +19 -0
@@ -85,6 +85,25 @@ func (s *Service) Load(ctx context.Context) error {
8585 return nil
8686 }
8787
88+// Reload re-reads the configuration file from disk and loads it into memory.
89+// This should be used when you want to pick up external changes to the config file.
90+// It differs from Load() which only loads from cached viper values.
91+func (s *Service) Reload(ctx context.Context) error {
92+ ctx = zerowrap.CtxWithFields(ctx, map[string]any{
93+ zerowrap.FieldLayer: "usecase",
94+ zerowrap.FieldUseCase: "Reload",
95+ })
96+ log := zerowrap.FromCtx(ctx)
97+
98+ // Re-read the config file from disk
99+ if err := s.viper.ReadInConfig(); err != nil {
100+ return log.WrapErr(err, "failed to read config file")
101+ }
102+
103+ // Load the values into memory
104+ return s.Load(ctx)
105+}
106+
88107 // loadConfigValues loads simple config values from viper.
89108 func (s *Service) loadConfigValues() Config {
90109 // Prefer gordon_domain over registry_domain
@@ -85,6 +85,25 @@ func (s *Service) Load(ctx context.Context) error {
85 return nil85 return nil
86 }86 }
87 87
88+// Reload re-reads the configuration file from disk and loads it into memory.
89+// This should be used when you want to pick up external changes to the config file.
90+// It differs from Load() which only loads from cached viper values.
91+func (s *Service) Reload(ctx context.Context) error {
92+ ctx = zerowrap.CtxWithFields(ctx, map[string]any{
93+ zerowrap.FieldLayer: "usecase",
94+ zerowrap.FieldUseCase: "Reload",
95+ })
96+ log := zerowrap.FromCtx(ctx)
97+
98+ // Re-read the config file from disk
99+ if err := s.viper.ReadInConfig(); err != nil {
100+ return log.WrapErr(err, "failed to read config file")
101+ }
102+
103+ // Load the values into memory
104+ return s.Load(ctx)
105+}
106+
88 // loadConfigValues loads simple config values from viper.107 // loadConfigValues loads simple config values from viper.
89 func (s *Service) loadConfigValues() Config {108 func (s *Service) loadConfigValues() Config {
90 // Prefer gordon_domain over registry_domain109 // Prefer gordon_domain over registry_domain

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