detached 458a175

feat(admin): add preview API endpoints — list, delete, extend

Adds GET /admin/previews (list all previews) and DELETE/PATCH
/admin/preview/{name} (teardown / extend TTL) following the existing
prefix-route dispatch pattern. A minimal previewService interface is
defined in the admin package; the concrete *preview.Service will be
wired in Task 13.
commit
458a175c6ed5cf7d23fb206906cfef02596b8a2e
author
brice · · 2026-03-19 05:07 UTC
committer
brice · · 2026-03-19 05:13 UTC
parent
03a3f23
signature
unsigned
5 files · +210 -64
M internal/adapters/in/http/admin/bootstrap_test.go +7 -7
@@ -24,7 +24,7 @@ func TestBootstrap_FullWorkflow_HappyPath(t *testing.T) {
2424 secretSvc := inmocks.NewMockSecretService(t)
2525 registrySvc := inmocks.NewMockRegistryService(t)
2626
27- handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, registrySvc, nil, testLogger(), nil)
27+ handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, registrySvc, nil, testLogger(), nil, nil)
2828
2929 payload := dto.BootstrapRequest{
3030 Domain: "app.example.com",
@@ -73,7 +73,7 @@ func TestBootstrap_Idempotent_Rerun(t *testing.T) {
7373 containerSvc := inmocks.NewMockContainerService(t)
7474 secretSvc := inmocks.NewMockSecretService(t)
7575
76- handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger(), nil)
76+ handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger(), nil, nil)
7777
7878 payload := dto.BootstrapRequest{
7979 Domain: "app.example.com",
@@ -137,7 +137,7 @@ func TestBootstrap_MissingDomain(t *testing.T) {
137137 containerSvc := inmocks.NewMockContainerService(t)
138138 secretSvc := inmocks.NewMockSecretService(t)
139139
140- handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger(), nil)
140+ handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger(), nil, nil)
141141
142142 body, err := json.Marshal(dto.BootstrapRequest{Image: "myapp:latest"})
143143 require.NoError(t, err)
@@ -158,7 +158,7 @@ func TestBootstrap_MissingImage(t *testing.T) {
158158 containerSvc := inmocks.NewMockContainerService(t)
159159 secretSvc := inmocks.NewMockSecretService(t)
160160
161- handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger(), nil)
161+ handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger(), nil, nil)
162162
163163 body, err := json.Marshal(dto.BootstrapRequest{Domain: "app.example.com"})
164164 require.NoError(t, err)
@@ -179,7 +179,7 @@ func TestBootstrap_NormalizesImageBeforeStoring(t *testing.T) {
179179 containerSvc := inmocks.NewMockContainerService(t)
180180 secretSvc := inmocks.NewMockSecretService(t)
181181
182- handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger(), nil)
182+ handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger(), nil, nil)
183183
184184 payload := dto.BootstrapRequest{Domain: "app.example.com", Image: "pitlane:v1"}
185185 configSvc.EXPECT().GetRegistryDomain().Return("reg.bnema.dev").Once()
@@ -225,7 +225,7 @@ func TestBootstrap_AddRouteValidationErrors(t *testing.T) {
225225 containerSvc := inmocks.NewMockContainerService(t)
226226 secretSvc := inmocks.NewMockSecretService(t)
227227
228- handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger(), nil)
228+ handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger(), nil, nil)
229229
230230 payload := dto.BootstrapRequest{Domain: "app.example.com", Image: "myapp:latest"}
231231 configSvc.EXPECT().GetRegistryDomain().Return("reg.bnema.dev").Once()
@@ -252,7 +252,7 @@ func TestBootstrap_PartialFailure_AttachmentError(t *testing.T) {
252252 containerSvc := inmocks.NewMockContainerService(t)
253253 secretSvc := inmocks.NewMockSecretService(t)
254254
255- handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger(), nil)
255+ handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger(), nil, nil)
256256
257257 payload := dto.BootstrapRequest{
258258 Domain: "app.example.com",
@@ -24,7 +24,7 @@ func TestBootstrap_FullWorkflow_HappyPath(t *testing.T) {
24 secretSvc := inmocks.NewMockSecretService(t)24 secretSvc := inmocks.NewMockSecretService(t)
25 registrySvc := inmocks.NewMockRegistryService(t)25 registrySvc := inmocks.NewMockRegistryService(t)
26 26
27- handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, registrySvc, nil, testLogger(), nil)27+ handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, registrySvc, nil, testLogger(), nil, nil)
28 28
29 payload := dto.BootstrapRequest{29 payload := dto.BootstrapRequest{
30 Domain: "app.example.com",30 Domain: "app.example.com",
@@ -73,7 +73,7 @@ func TestBootstrap_Idempotent_Rerun(t *testing.T) {
73 containerSvc := inmocks.NewMockContainerService(t)73 containerSvc := inmocks.NewMockContainerService(t)
74 secretSvc := inmocks.NewMockSecretService(t)74 secretSvc := inmocks.NewMockSecretService(t)
75 75
76- handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger(), nil)76+ handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger(), nil, nil)
77 77
78 payload := dto.BootstrapRequest{78 payload := dto.BootstrapRequest{
79 Domain: "app.example.com",79 Domain: "app.example.com",
@@ -137,7 +137,7 @@ func TestBootstrap_MissingDomain(t *testing.T) {
137 containerSvc := inmocks.NewMockContainerService(t)137 containerSvc := inmocks.NewMockContainerService(t)
138 secretSvc := inmocks.NewMockSecretService(t)138 secretSvc := inmocks.NewMockSecretService(t)
139 139
140- handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger(), nil)140+ handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger(), nil, nil)
141 141
142 body, err := json.Marshal(dto.BootstrapRequest{Image: "myapp:latest"})142 body, err := json.Marshal(dto.BootstrapRequest{Image: "myapp:latest"})
143 require.NoError(t, err)143 require.NoError(t, err)
@@ -158,7 +158,7 @@ func TestBootstrap_MissingImage(t *testing.T) {
158 containerSvc := inmocks.NewMockContainerService(t)158 containerSvc := inmocks.NewMockContainerService(t)
159 secretSvc := inmocks.NewMockSecretService(t)159 secretSvc := inmocks.NewMockSecretService(t)
160 160
161- handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger(), nil)161+ handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger(), nil, nil)
162 162
163 body, err := json.Marshal(dto.BootstrapRequest{Domain: "app.example.com"})163 body, err := json.Marshal(dto.BootstrapRequest{Domain: "app.example.com"})
164 require.NoError(t, err)164 require.NoError(t, err)
@@ -179,7 +179,7 @@ func TestBootstrap_NormalizesImageBeforeStoring(t *testing.T) {
179 containerSvc := inmocks.NewMockContainerService(t)179 containerSvc := inmocks.NewMockContainerService(t)
180 secretSvc := inmocks.NewMockSecretService(t)180 secretSvc := inmocks.NewMockSecretService(t)
181 181
182- handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger(), nil)182+ handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger(), nil, nil)
183 183
184 payload := dto.BootstrapRequest{Domain: "app.example.com", Image: "pitlane:v1"}184 payload := dto.BootstrapRequest{Domain: "app.example.com", Image: "pitlane:v1"}
185 configSvc.EXPECT().GetRegistryDomain().Return("reg.bnema.dev").Once()185 configSvc.EXPECT().GetRegistryDomain().Return("reg.bnema.dev").Once()
@@ -225,7 +225,7 @@ func TestBootstrap_AddRouteValidationErrors(t *testing.T) {
225 containerSvc := inmocks.NewMockContainerService(t)225 containerSvc := inmocks.NewMockContainerService(t)
226 secretSvc := inmocks.NewMockSecretService(t)226 secretSvc := inmocks.NewMockSecretService(t)
227 227
228- handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger(), nil)228+ handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger(), nil, nil)
229 229
230 payload := dto.BootstrapRequest{Domain: "app.example.com", Image: "myapp:latest"}230 payload := dto.BootstrapRequest{Domain: "app.example.com", Image: "myapp:latest"}
231 configSvc.EXPECT().GetRegistryDomain().Return("reg.bnema.dev").Once()231 configSvc.EXPECT().GetRegistryDomain().Return("reg.bnema.dev").Once()
@@ -252,7 +252,7 @@ func TestBootstrap_PartialFailure_AttachmentError(t *testing.T) {
252 containerSvc := inmocks.NewMockContainerService(t)252 containerSvc := inmocks.NewMockContainerService(t)
253 secretSvc := inmocks.NewMockSecretService(t)253 secretSvc := inmocks.NewMockSecretService(t)
254 254
255- handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger(), nil)255+ handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger(), nil, nil)
256 256
257 payload := dto.BootstrapRequest{257 payload := dto.BootstrapRequest{
258 Domain: "app.example.com",258 Domain: "app.example.com",
M internal/adapters/in/http/admin/handler.go +5 -0
@@ -47,6 +47,7 @@ type Handler struct {
4747 logSvc in.LogService
4848 volumeSvc in.VolumeService
4949 registrySvc registryDeployService
50+ previewSvc previewService
5051 eventBus out.EventPublisher
5152 log zerowrap.Logger
5253 }
@@ -142,6 +143,7 @@ func NewHandler(
142143 eventBus out.EventPublisher,
143144 log zerowrap.Logger,
144145 backupSvc in.BackupService,
146+ previewSvc previewService,
145147 imageSvcs ...in.ImageService,
146148 ) *Handler {
147149 var imageSvc in.ImageService
@@ -159,6 +161,7 @@ func NewHandler(
159161 secretSvc: secretSvc,
160162 logSvc: logSvc,
161163 registrySvc: registrySvc,
164+ previewSvc: previewSvc,
162165 eventBus: eventBus,
163166 log: log,
164167 }
@@ -239,6 +242,8 @@ func (h *Handler) matchRoute(path string) (routeHandler, bool) {
239242 {"/images", h.handleImages},
240243 {"/logs", h.handleLogs},
241244 {"/autoroute/allowed-domains", h.handleAutoRouteAllowedDomains},
245+ {"/previews", h.handlePreviewList},
246+ {"/preview", h.handlePreviewAction},
242247 }
243248 for _, route := range prefixRoutes {
244249 if path == route.prefix || strings.HasPrefix(path, route.prefix+"/") {
@@ -47,6 +47,7 @@ type Handler struct {
47 logSvc in.LogService47 logSvc in.LogService
48 volumeSvc in.VolumeService48 volumeSvc in.VolumeService
49 registrySvc registryDeployService49 registrySvc registryDeployService
50+ previewSvc previewService
50 eventBus out.EventPublisher51 eventBus out.EventPublisher
51 log zerowrap.Logger52 log zerowrap.Logger
52 }53 }
@@ -142,6 +143,7 @@ func NewHandler(
142 eventBus out.EventPublisher,143 eventBus out.EventPublisher,
143 log zerowrap.Logger,144 log zerowrap.Logger,
144 backupSvc in.BackupService,145 backupSvc in.BackupService,
146+ previewSvc previewService,
145 imageSvcs ...in.ImageService,147 imageSvcs ...in.ImageService,
146 ) *Handler {148 ) *Handler {
147 var imageSvc in.ImageService149 var imageSvc in.ImageService
@@ -159,6 +161,7 @@ func NewHandler(
159 secretSvc: secretSvc,161 secretSvc: secretSvc,
160 logSvc: logSvc,162 logSvc: logSvc,
161 registrySvc: registrySvc,163 registrySvc: registrySvc,
164+ previewSvc: previewSvc,
162 eventBus: eventBus,165 eventBus: eventBus,
163 log: log,166 log: log,
164 }167 }
@@ -239,6 +242,8 @@ func (h *Handler) matchRoute(path string) (routeHandler, bool) {
239 {"/images", h.handleImages},242 {"/images", h.handleImages},
240 {"/logs", h.handleLogs},243 {"/logs", h.handleLogs},
241 {"/autoroute/allowed-domains", h.handleAutoRouteAllowedDomains},244 {"/autoroute/allowed-domains", h.handleAutoRouteAllowedDomains},
245+ {"/previews", h.handlePreviewList},
246+ {"/preview", h.handlePreviewAction},
242 }247 }
243 for _, route := range prefixRoutes {248 for _, route := range prefixRoutes {
244 if path == route.prefix || strings.HasPrefix(path, route.prefix+"/") {249 if path == route.prefix || strings.HasPrefix(path, route.prefix+"/") {
M internal/adapters/in/http/admin/handler_test.go +57 -57
@@ -58,7 +58,7 @@ func TestHandler_RoutesGet_RequiresReadScope(t *testing.T) {
5858 containerSvc := inmocks.NewMockContainerService(t)
5959 secretSvc := inmocks.NewMockSecretService(t)
6060
61- handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger(), nil)
61+ handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger(), nil, nil)
6262
6363 tests := []struct {
6464 name string
@@ -121,7 +121,7 @@ func TestHandler_RoutesPost_RequiresWriteScope(t *testing.T) {
121121 secretSvc := inmocks.NewMockSecretService(t)
122122 registrySvc := inmocks.NewMockRegistryService(t)
123123
124- handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, registrySvc, nil, testLogger(), nil)
124+ handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, registrySvc, nil, testLogger(), nil, nil)
125125
126126 routeJSON := `{"domain": "app.example.com", "image": "myapp:latest"}`
127127
@@ -181,7 +181,7 @@ func TestHandler_RoutesPut_RequiresWriteScope(t *testing.T) {
181181 secretSvc := inmocks.NewMockSecretService(t)
182182 registrySvc := inmocks.NewMockRegistryService(t)
183183
184- handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, registrySvc, nil, testLogger(), nil)
184+ handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, registrySvc, nil, testLogger(), nil, nil)
185185
186186 routeJSON := `{"image": "myapp:v2"}`
187187
@@ -227,7 +227,7 @@ func TestHandler_RoutesPost_AllowsRouteCreationWithoutRegistryManifest(t *testin
227227 registrySvc := inmocks.NewMockRegistryService(t)
228228 route := domain.Route{Domain: "app.example.com", Image: "myapp:latest"}
229229
230- handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, registrySvc, nil, testLogger(), nil)
230+ handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, registrySvc, nil, testLogger(), nil, nil)
231231
232232 configSvc.EXPECT().AddRoute(mock.Anything, route).Return(nil).Once()
233233
@@ -250,7 +250,7 @@ func TestHandler_RoutesPut_AllowsRouteUpdateWithoutRegistryManifest(t *testing.T
250250 registrySvc := inmocks.NewMockRegistryService(t)
251251 route := domain.Route{Domain: "app.example.com", Image: "myapp:v2"}
252252
253- handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, registrySvc, nil, testLogger(), nil)
253+ handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, registrySvc, nil, testLogger(), nil, nil)
254254
255255 configSvc.EXPECT().UpdateRoute(mock.Anything, route).Return(nil).Once()
256256
@@ -271,7 +271,7 @@ func TestHandler_AttachmentsByImageGet_OneTarget(t *testing.T) {
271271 containerSvc := inmocks.NewMockContainerService(t)
272272 secretSvc := inmocks.NewMockSecretService(t)
273273
274- handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger(), nil)
274+ handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger(), nil, nil)
275275
276276 configSvc.EXPECT().FindAttachmentTargetsByImage(mock.Anything, "postgres:16").Return([]string{"app.example.com"}).Once()
277277
@@ -291,7 +291,7 @@ func TestHandler_AttachmentsByImageGet_MultipleTargets(t *testing.T) {
291291 containerSvc := inmocks.NewMockContainerService(t)
292292 secretSvc := inmocks.NewMockSecretService(t)
293293
294- handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger(), nil)
294+ handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger(), nil, nil)
295295
296296 configSvc.EXPECT().FindAttachmentTargetsByImage(mock.Anything, "postgres").Return([]string{"app.example.com", "workers"}).Once()
297297
@@ -311,7 +311,7 @@ func TestHandler_AttachmentsByImageGet_URLDecodedImage(t *testing.T) {
311311 containerSvc := inmocks.NewMockContainerService(t)
312312 secretSvc := inmocks.NewMockSecretService(t)
313313
314- handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger(), nil)
314+ handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger(), nil, nil)
315315
316316 configSvc.EXPECT().FindAttachmentTargetsByImage(mock.Anything, "registry/org/image:tag").Return([]string{"app.example.com"}).Once()
317317
@@ -331,7 +331,7 @@ func TestHandler_AttachmentsByImageGet_EmptyResultSet(t *testing.T) {
331331 containerSvc := inmocks.NewMockContainerService(t)
332332 secretSvc := inmocks.NewMockSecretService(t)
333333
334- handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger(), nil)
334+ handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger(), nil, nil)
335335
336336 configSvc.EXPECT().FindAttachmentTargetsByImage(mock.Anything, "missing:latest").Return([]string{}).Once()
337337
@@ -351,7 +351,7 @@ func TestHandler_AttachmentsByImageGet_MissingImagePath(t *testing.T) {
351351 containerSvc := inmocks.NewMockContainerService(t)
352352 secretSvc := inmocks.NewMockSecretService(t)
353353
354- handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger(), nil)
354+ handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger(), nil, nil)
355355
356356 req := httptest.NewRequest("GET", "/admin/attachments/by-image", nil)
357357 req = req.WithContext(ctxWithScopes("admin:config:read"))
@@ -369,7 +369,7 @@ func TestHandler_AttachmentsByImageGet_InvalidEncoding(t *testing.T) {
369369 containerSvc := inmocks.NewMockContainerService(t)
370370 secretSvc := inmocks.NewMockSecretService(t)
371371
372- handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger(), nil)
372+ handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger(), nil, nil)
373373
374374 req := httptest.NewRequest("GET", "/admin/attachments/by-image/placeholder", nil)
375375 req.URL = &url.URL{Path: "/admin/attachments/by-image/%zz"}
@@ -388,7 +388,7 @@ func TestHandler_AttachmentsByImageGet_RequiresReadScope(t *testing.T) {
388388 containerSvc := inmocks.NewMockContainerService(t)
389389 secretSvc := inmocks.NewMockSecretService(t)
390390
391- handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger(), nil)
391+ handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger(), nil, nil)
392392
393393 req := httptest.NewRequest("GET", "/admin/attachments/by-image/postgres:16", nil)
394394 req = req.WithContext(ctxWithScopes("admin:routes:read"))
@@ -406,7 +406,7 @@ func TestHandler_RoutesDelete_RequiresWriteScope(t *testing.T) {
406406 containerSvc := inmocks.NewMockContainerService(t)
407407 secretSvc := inmocks.NewMockSecretService(t)
408408
409- handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger(), nil)
409+ handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger(), nil, nil)
410410
411411 tests := []struct {
412412 name string
@@ -450,7 +450,7 @@ func TestHandler_SecretsGet_RequiresReadScope(t *testing.T) {
450450 containerSvc := inmocks.NewMockContainerService(t)
451451 secretSvc := inmocks.NewMockSecretService(t)
452452
453- handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger(), nil)
453+ handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger(), nil, nil)
454454
455455 tests := []struct {
456456 name string
@@ -507,7 +507,7 @@ func TestHandler_SecretsPost_RequiresWriteScope(t *testing.T) {
507507 containerSvc := inmocks.NewMockContainerService(t)
508508 secretSvc := inmocks.NewMockSecretService(t)
509509
510- handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger(), nil)
510+ handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger(), nil, nil)
511511
512512 secretsJSON := `{"API_KEY": "secret123"}`
513513
@@ -556,7 +556,7 @@ func TestHandler_SecretsDelete_RequiresWriteScope(t *testing.T) {
556556 containerSvc := inmocks.NewMockContainerService(t)
557557 secretSvc := inmocks.NewMockSecretService(t)
558558
559- handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger(), nil)
559+ handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger(), nil, nil)
560560
561561 tests := []struct {
562562 name string
@@ -600,7 +600,7 @@ func TestHandler_AttachmentSecretsPost(t *testing.T) {
600600 containerSvc := inmocks.NewMockContainerService(t)
601601 secretSvc := inmocks.NewMockSecretService(t)
602602
603- handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger(), nil)
603+ handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger(), nil, nil)
604604
605605 secretSvc.EXPECT().SetAttachment(mock.Anything, "app.example.com", "redis", mock.AnythingOfType("map[string]string")).Return(nil)
606606
@@ -626,7 +626,7 @@ func TestHandler_AttachmentSecretsDelete(t *testing.T) {
626626 containerSvc := inmocks.NewMockContainerService(t)
627627 secretSvc := inmocks.NewMockSecretService(t)
628628
629- handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger(), nil)
629+ handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger(), nil, nil)
630630
631631 secretSvc.EXPECT().DeleteAttachment(mock.Anything, "app.example.com", "redis", "REDIS_URL").Return(nil)
632632
@@ -652,7 +652,7 @@ func TestHandler_Bootstrap_HappyPath(t *testing.T) {
652652 secretSvc := inmocks.NewMockSecretService(t)
653653 registrySvc := inmocks.NewMockRegistryService(t)
654654
655- handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, registrySvc, nil, testLogger(), nil)
655+ handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, registrySvc, nil, testLogger(), nil, nil)
656656
657657 configSvc.EXPECT().GetRegistryDomain().Return("reg.bnema.dev").Once()
658658 configSvc.EXPECT().AddRoute(mock.Anything, domain.Route{Domain: "app.example.com", Image: "reg.bnema.dev/myapp"}).Return(nil).Once()
@@ -699,7 +699,7 @@ func TestHandler_Bootstrap_RequiresPermissions(t *testing.T) {
699699 containerSvc := inmocks.NewMockContainerService(t)
700700 secretSvc := inmocks.NewMockSecretService(t)
701701
702- handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger(), nil)
702+ handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger(), nil, nil)
703703
704704 tests := []struct {
705705 name string
@@ -741,7 +741,7 @@ func TestHandler_Status_RequiresReadScope(t *testing.T) {
741741 containerSvc := inmocks.NewMockContainerService(t)
742742 secretSvc := inmocks.NewMockSecretService(t)
743743
744- handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger(), nil)
744+ handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger(), nil, nil)
745745
746746 tests := []struct {
747747 name string
@@ -795,7 +795,7 @@ func TestHandler_Config_RequiresReadScope(t *testing.T) {
795795 containerSvc := inmocks.NewMockContainerService(t)
796796 secretSvc := inmocks.NewMockSecretService(t)
797797
798- handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger(), nil)
798+ handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger(), nil, nil)
799799
800800 tests := []struct {
801801 name string
@@ -852,7 +852,7 @@ func TestHandler_Reload_RequiresWriteScope(t *testing.T) {
852852 containerSvc := inmocks.NewMockContainerService(t)
853853 secretSvc := inmocks.NewMockSecretService(t)
854854
855- handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger(), nil)
855+ handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger(), nil, nil)
856856
857857 tests := []struct {
858858 name string
@@ -906,7 +906,7 @@ func TestHandler_RoutesGet_ReturnsRoutes(t *testing.T) {
906906 containerSvc := inmocks.NewMockContainerService(t)
907907 secretSvc := inmocks.NewMockSecretService(t)
908908
909- handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger(), nil)
909+ handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger(), nil, nil)
910910
911911 expectedRoutes := []domain.Route{
912912 {Domain: "app1.example.com", Image: "app1:latest"},
@@ -941,7 +941,7 @@ func TestHandler_RoutesGet_SingleRoute(t *testing.T) {
941941 containerSvc := inmocks.NewMockContainerService(t)
942942 secretSvc := inmocks.NewMockSecretService(t)
943943
944- handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger(), nil)
944+ handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger(), nil, nil)
945945
946946 configSvc.EXPECT().GetRoute(mock.Anything, "app.example.com").Return(&domain.Route{
947947 Domain: "app.example.com", Image: "app:latest",
@@ -972,7 +972,7 @@ func TestHandler_RoutesGet_NotFound(t *testing.T) {
972972 containerSvc := inmocks.NewMockContainerService(t)
973973 secretSvc := inmocks.NewMockSecretService(t)
974974
975- handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger(), nil)
975+ handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger(), nil, nil)
976976
977977 configSvc.EXPECT().GetRoute(mock.Anything, "nonexistent.example.com").Return(nil, domain.ErrRouteNotFound)
978978
@@ -1016,7 +1016,7 @@ func TestHandler_RoutesPost_MissingFields(t *testing.T) {
10161016 secretSvc := inmocks.NewMockSecretService(t)
10171017 registrySvc := inmocks.NewMockRegistryService(t)
10181018
1019- handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, registrySvc, nil, testLogger(), nil)
1019+ handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, registrySvc, nil, testLogger(), nil, nil)
10201020
10211021 configSvc.EXPECT().AddRoute(mock.Anything, mock.Anything).Return(tt.mockErr)
10221022
@@ -1037,7 +1037,7 @@ func TestHandler_RoutesPut_MissingDomain(t *testing.T) {
10371037 containerSvc := inmocks.NewMockContainerService(t)
10381038 secretSvc := inmocks.NewMockSecretService(t)
10391039
1040- handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger(), nil)
1040+ handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger(), nil, nil)
10411041
10421042 req := httptest.NewRequest("PUT", "/admin/routes/", bytes.NewBufferString(`{"image": "app:latest"}`))
10431043 req = req.WithContext(ctxWithScopes("admin:routes:write"))
@@ -1054,7 +1054,7 @@ func TestHandler_RoutesDelete_MissingDomain(t *testing.T) {
10541054 containerSvc := inmocks.NewMockContainerService(t)
10551055 secretSvc := inmocks.NewMockSecretService(t)
10561056
1057- handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger(), nil)
1057+ handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger(), nil, nil)
10581058
10591059 req := httptest.NewRequest("DELETE", "/admin/routes/", nil)
10601060 req = req.WithContext(ctxWithScopes("admin:routes:write"))
@@ -1071,7 +1071,7 @@ func TestHandler_Secrets_MissingDomain(t *testing.T) {
10711071 containerSvc := inmocks.NewMockContainerService(t)
10721072 secretSvc := inmocks.NewMockSecretService(t)
10731073
1074- handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger(), nil)
1074+ handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger(), nil, nil)
10751075
10761076 req := httptest.NewRequest("GET", "/admin/secrets/", nil)
10771077 req = req.WithContext(ctxWithScopes("admin:secrets:read"))
@@ -1088,7 +1088,7 @@ func TestHandler_SecretsDelete_MissingKey(t *testing.T) {
10881088 containerSvc := inmocks.NewMockContainerService(t)
10891089 secretSvc := inmocks.NewMockSecretService(t)
10901090
1091- handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger(), nil)
1091+ handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger(), nil, nil)
10921092
10931093 req := httptest.NewRequest("DELETE", "/admin/secrets/app.example.com", nil)
10941094 req = req.WithContext(ctxWithScopes("admin:secrets:write"))
@@ -1105,7 +1105,7 @@ func TestHandler_MethodNotAllowed(t *testing.T) {
11051105 containerSvc := inmocks.NewMockContainerService(t)
11061106 secretSvc := inmocks.NewMockSecretService(t)
11071107
1108- handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger(), nil)
1108+ handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger(), nil, nil)
11091109
11101110 tests := []struct {
11111111 method string
@@ -1138,7 +1138,7 @@ func TestHandler_Restart_InvalidDomain(t *testing.T) {
11381138 containerSvc := inmocks.NewMockContainerService(t)
11391139 secretSvc := inmocks.NewMockSecretService(t)
11401140
1141- handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger(), nil)
1141+ handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger(), nil, nil)
11421142
11431143 tests := []struct {
11441144 name string
@@ -1168,7 +1168,7 @@ func TestHandler_Restart_ContainerNotFound(t *testing.T) {
11681168 containerSvc := inmocks.NewMockContainerService(t)
11691169 secretSvc := inmocks.NewMockSecretService(t)
11701170
1171- handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger(), nil)
1171+ handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger(), nil, nil)
11721172
11731173 containerSvc.EXPECT().Restart(mock.Anything, "nonexistent.example.com", false).Return(domain.ErrContainerNotFound)
11741174
@@ -1187,7 +1187,7 @@ func TestHandler_Restart_GenericError(t *testing.T) {
11871187 containerSvc := inmocks.NewMockContainerService(t)
11881188 secretSvc := inmocks.NewMockSecretService(t)
11891189
1190- handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger(), nil)
1190+ handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger(), nil, nil)
11911191
11921192 containerSvc.EXPECT().Restart(mock.Anything, "test.example.com", false).Return(fmt.Errorf("docker daemon error: secret details"))
11931193
@@ -1209,7 +1209,7 @@ func TestHandler_Restart_Success(t *testing.T) {
12091209 containerSvc := inmocks.NewMockContainerService(t)
12101210 secretSvc := inmocks.NewMockSecretService(t)
12111211
1212- handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger(), nil)
1212+ handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger(), nil, nil)
12131213
12141214 tests := []struct {
12151215 name string
@@ -1256,7 +1256,7 @@ func TestHandler_Tags_InvalidRepository(t *testing.T) {
12561256 containerSvc := inmocks.NewMockContainerService(t)
12571257 secretSvc := inmocks.NewMockSecretService(t)
12581258
1259- handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger(), nil)
1259+ handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger(), nil, nil)
12601260
12611261 tests := []struct {
12621262 name string
@@ -1287,7 +1287,7 @@ func TestHandler_Tags_Success(t *testing.T) {
12871287 secretSvc := inmocks.NewMockSecretService(t)
12881288 registrySvc := inmocks.NewMockRegistryService(t)
12891289
1290- handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, registrySvc, nil, testLogger(), nil)
1290+ handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, registrySvc, nil, testLogger(), nil, nil)
12911291
12921292 registrySvc.EXPECT().ListTags(mock.Anything, "myapp").Return([]string{"latest", "v1.0"}, nil)
12931293
@@ -1314,7 +1314,7 @@ func TestHandler_Tags_Error(t *testing.T) {
13141314 secretSvc := inmocks.NewMockSecretService(t)
13151315 registrySvc := inmocks.NewMockRegistryService(t)
13161316
1317- handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, registrySvc, nil, testLogger(), nil)
1317+ handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, registrySvc, nil, testLogger(), nil, nil)
13181318
13191319 registrySvc.EXPECT().ListTags(mock.Anything, "myapp").Return(nil, fmt.Errorf("registry error"))
13201320
@@ -1334,7 +1334,7 @@ func TestHandler_AttachmentSecretsPost_RequiresWriteScope(t *testing.T) {
13341334 containerSvc := inmocks.NewMockContainerService(t)
13351335 secretSvc := inmocks.NewMockSecretService(t)
13361336
1337- handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger(), nil)
1337+ handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger(), nil, nil)
13381338
13391339 body := `{"REDIS_URL": "redis://localhost:6379"}`
13401340 req := httptest.NewRequest("POST", "/admin/secrets/app.example.com/attachments/redis", bytes.NewBufferString(body))
@@ -1352,7 +1352,7 @@ func TestHandler_AttachmentSecretsDelete_RequiresWriteScope(t *testing.T) {
13521352 containerSvc := inmocks.NewMockContainerService(t)
13531353 secretSvc := inmocks.NewMockSecretService(t)
13541354
1355- handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger(), nil)
1355+ handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger(), nil, nil)
13561356
13571357 req := httptest.NewRequest("DELETE", "/admin/secrets/app.example.com/attachments/redis/REDIS_URL", nil)
13581358 req = req.WithContext(ctxWithScopes("admin:secrets:read"))
@@ -1369,7 +1369,7 @@ func TestHandler_AttachmentSecretsDelete_RequiresKey(t *testing.T) {
13691369 containerSvc := inmocks.NewMockContainerService(t)
13701370 secretSvc := inmocks.NewMockSecretService(t)
13711371
1372- handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger(), nil)
1372+ handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger(), nil, nil)
13731373
13741374 req := httptest.NewRequest("DELETE", "/admin/secrets/app.example.com/attachments/redis", nil)
13751375 req = req.WithContext(ctxWithScopes("admin:secrets:write"))
@@ -1387,7 +1387,7 @@ func TestHandler_AttachmentSecretsPost_InvalidJSON(t *testing.T) {
13871387 containerSvc := inmocks.NewMockContainerService(t)
13881388 secretSvc := inmocks.NewMockSecretService(t)
13891389
1390- handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger(), nil)
1390+ handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger(), nil, nil)
13911391
13921392 body := `{not valid json`
13931393 req := httptest.NewRequest("POST", "/admin/secrets/app.example.com/attachments/redis", bytes.NewBufferString(body))
@@ -1406,7 +1406,7 @@ func TestHandler_NotFound(t *testing.T) {
14061406 containerSvc := inmocks.NewMockContainerService(t)
14071407 secretSvc := inmocks.NewMockSecretService(t)
14081408
1409- handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger(), nil)
1409+ handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger(), nil, nil)
14101410
14111411 req := httptest.NewRequest("GET", "/admin/unknown", nil)
14121412 req = req.WithContext(ctxWithScopes("admin:*:*"))
@@ -1426,7 +1426,7 @@ func TestHandler_BackupsStatus(t *testing.T) {
14261426
14271427 backupSvc.EXPECT().Status(mock.Anything).Return([]domain.BackupJob{{Domain: "app.example.com", DBName: "postgres", Status: domain.BackupStatusCompleted, FilePath: "/var/lib/gordon/backups/private.bak"}}, nil)
14281428
1429- handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger(), backupSvc)
1429+ handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger(), backupSvc, nil)
14301430
14311431 req := httptest.NewRequest("GET", "/admin/backups/status", nil)
14321432 req = req.WithContext(ctxWithScopes("admin:status:read"))
@@ -1448,7 +1448,7 @@ func TestHandler_BackupsListDomain(t *testing.T) {
14481448
14491449 backupSvc.EXPECT().ListBackups(mock.Anything, "app.example.com").Return([]domain.BackupJob{{Domain: "app.example.com", DBName: "postgres", Status: domain.BackupStatusCompleted}}, nil)
14501450
1451- handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger(), backupSvc)
1451+ handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger(), backupSvc, nil)
14521452
14531453 req := httptest.NewRequest("GET", "/admin/backups/app.example.com", nil)
14541454 req = req.WithContext(ctxWithScopes("admin:status:read"))
@@ -1469,7 +1469,7 @@ func TestHandler_BackupsRunDomain(t *testing.T) {
14691469
14701470 backupSvc.EXPECT().RunBackup(mock.Anything, "app.example.com", "postgres").Return(&domain.BackupResult{Job: domain.BackupJob{Domain: "app.example.com", DBName: "postgres", Status: domain.BackupStatusCompleted}}, nil)
14711471
1472- handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger(), backupSvc)
1472+ handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger(), backupSvc, nil)
14731473
14741474 body := bytes.NewBufferString(`{"db":"postgres"}`)
14751475 req := httptest.NewRequest("POST", "/admin/backups/app.example.com", body)
@@ -1491,7 +1491,7 @@ func TestHandler_BackupsRunDomain_ChunkedBodyIsDecoded(t *testing.T) {
14911491
14921492 backupSvc.EXPECT().RunBackup(mock.Anything, "app.example.com", "postgres").Return(&domain.BackupResult{Job: domain.BackupJob{Domain: "app.example.com", DBName: "postgres", Status: domain.BackupStatusCompleted}}, nil)
14931493
1494- handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger(), backupSvc)
1494+ handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger(), backupSvc, nil)
14951495
14961496 body := bytes.NewBufferString(`{"db":"postgres"}`)
14971497 req := httptest.NewRequest("POST", "/admin/backups/app.example.com", body)
@@ -1514,7 +1514,7 @@ func TestHandler_BackupsDetectDomain(t *testing.T) {
15141514
15151515 backupSvc.EXPECT().DetectDatabases(mock.Anything, "app.example.com").Return([]domain.DBInfo{{Type: domain.DBTypePostgreSQL, Name: "postgres", Port: 5432}}, nil)
15161516
1517- handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger(), backupSvc)
1517+ handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger(), backupSvc, nil)
15181518
15191519 req := httptest.NewRequest("GET", "/admin/backups/app.example.com/detect", nil)
15201520 req = req.WithContext(ctxWithScopes("admin:status:read"))
@@ -1556,7 +1556,7 @@ func TestHandler_ImagesGet_ReturnsMappedList(t *testing.T) {
15561556 },
15571557 }
15581558
1559- handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger(), nil, imageSvc)
1559+ handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger(), nil, nil, imageSvc)
15601560
15611561 req := httptest.NewRequest("GET", "/admin/images", nil)
15621562 req = req.WithContext(ctxWithScopes("admin:status:read"))
@@ -1617,7 +1617,7 @@ func TestHandler_ImagesPrune_AcceptsOptionalKeepLast(t *testing.T) {
16171617 },
16181618 }
16191619
1620- handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger(), nil, imageSvc)
1620+ handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger(), nil, nil, imageSvc)
16211621
16221622 req := httptest.NewRequest("POST", "/admin/images/prune", bytes.NewBufferString(tt.body))
16231623 req = req.WithContext(ctxWithScopes("admin:config:write"))
@@ -1646,7 +1646,7 @@ func TestHandler_Images_ErrorMappingForServiceFailures(t *testing.T) {
16461646 },
16471647 }
16481648
1649- handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger(), nil, imageSvc)
1649+ handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger(), nil, nil, imageSvc)
16501650
16511651 req := httptest.NewRequest("GET", "/admin/images", nil)
16521652 req = req.WithContext(ctxWithScopes("admin:status:read"))
@@ -1671,7 +1671,7 @@ func TestHandler_Images_ErrorMappingForServiceFailures(t *testing.T) {
16711671 },
16721672 }
16731673
1674- handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger(), nil, imageSvc)
1674+ handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger(), nil, nil, imageSvc)
16751675
16761676 req := httptest.NewRequest("POST", "/admin/images/prune", bytes.NewBufferString(`{"keep_last": 2}`))
16771677 req = req.WithContext(ctxWithScopes("admin:config:write"))
@@ -1692,7 +1692,7 @@ func TestHandler_ImagesPrune_ValidationAndAvailability(t *testing.T) {
16921692 containerSvc := inmocks.NewMockContainerService(t)
16931693 secretSvc := inmocks.NewMockSecretService(t)
16941694
1695- handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger(), nil, &stubImageService{})
1695+ handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger(), nil, nil, &stubImageService{})
16961696
16971697 req := httptest.NewRequest("POST", "/admin/images/prune", bytes.NewBufferString(`{"keep_last": -1}`))
16981698 req = req.WithContext(ctxWithScopes("admin:config:write"))
@@ -1710,7 +1710,7 @@ func TestHandler_ImagesPrune_ValidationAndAvailability(t *testing.T) {
17101710 containerSvc := inmocks.NewMockContainerService(t)
17111711 secretSvc := inmocks.NewMockSecretService(t)
17121712
1713- handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger(), nil)
1713+ handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger(), nil, nil)
17141714
17151715 req := httptest.NewRequest("GET", "/admin/images", nil)
17161716 req = req.WithContext(ctxWithScopes("admin:status:read"))
@@ -1781,7 +1781,7 @@ func TestHandler_ImagesPrune_ScopeDefaults(t *testing.T) {
17811781 },
17821782 }
17831783
1784- handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger(), nil, imageSvc)
1784+ handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger(), nil, nil, imageSvc)
17851785
17861786 req := httptest.NewRequest("POST", "/admin/images/prune", bytes.NewBufferString(tt.body))
17871787 req = req.WithContext(ctxWithScopes("admin:config:write"))
@@ -1806,7 +1806,7 @@ func TestHandler_Images_Authorization(t *testing.T) {
18061806 containerSvc := inmocks.NewMockContainerService(t)
18071807 secretSvc := inmocks.NewMockSecretService(t)
18081808
1809- handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger(), nil, &stubImageService{})
1809+ handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger(), nil, nil, &stubImageService{})
18101810
18111811 t.Run("images list requires status:read", func(t *testing.T) {
18121812 req := httptest.NewRequest("GET", "/admin/images", nil)
@@ -58,7 +58,7 @@ func TestHandler_RoutesGet_RequiresReadScope(t *testing.T) {
58 containerSvc := inmocks.NewMockContainerService(t)58 containerSvc := inmocks.NewMockContainerService(t)
59 secretSvc := inmocks.NewMockSecretService(t)59 secretSvc := inmocks.NewMockSecretService(t)
60 60
61- handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger(), nil)61+ handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger(), nil, nil)
62 62
63 tests := []struct {63 tests := []struct {
64 name string64 name string
@@ -121,7 +121,7 @@ func TestHandler_RoutesPost_RequiresWriteScope(t *testing.T) {
121 secretSvc := inmocks.NewMockSecretService(t)121 secretSvc := inmocks.NewMockSecretService(t)
122 registrySvc := inmocks.NewMockRegistryService(t)122 registrySvc := inmocks.NewMockRegistryService(t)
123 123
124- handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, registrySvc, nil, testLogger(), nil)124+ handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, registrySvc, nil, testLogger(), nil, nil)
125 125
126 routeJSON := `{"domain": "app.example.com", "image": "myapp:latest"}`126 routeJSON := `{"domain": "app.example.com", "image": "myapp:latest"}`
127 127
@@ -181,7 +181,7 @@ func TestHandler_RoutesPut_RequiresWriteScope(t *testing.T) {
181 secretSvc := inmocks.NewMockSecretService(t)181 secretSvc := inmocks.NewMockSecretService(t)
182 registrySvc := inmocks.NewMockRegistryService(t)182 registrySvc := inmocks.NewMockRegistryService(t)
183 183
184- handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, registrySvc, nil, testLogger(), nil)184+ handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, registrySvc, nil, testLogger(), nil, nil)
185 185
186 routeJSON := `{"image": "myapp:v2"}`186 routeJSON := `{"image": "myapp:v2"}`
187 187
@@ -227,7 +227,7 @@ func TestHandler_RoutesPost_AllowsRouteCreationWithoutRegistryManifest(t *testin
227 registrySvc := inmocks.NewMockRegistryService(t)227 registrySvc := inmocks.NewMockRegistryService(t)
228 route := domain.Route{Domain: "app.example.com", Image: "myapp:latest"}228 route := domain.Route{Domain: "app.example.com", Image: "myapp:latest"}
229 229
230- handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, registrySvc, nil, testLogger(), nil)230+ handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, registrySvc, nil, testLogger(), nil, nil)
231 231
232 configSvc.EXPECT().AddRoute(mock.Anything, route).Return(nil).Once()232 configSvc.EXPECT().AddRoute(mock.Anything, route).Return(nil).Once()
233 233
@@ -250,7 +250,7 @@ func TestHandler_RoutesPut_AllowsRouteUpdateWithoutRegistryManifest(t *testing.T
250 registrySvc := inmocks.NewMockRegistryService(t)250 registrySvc := inmocks.NewMockRegistryService(t)
251 route := domain.Route{Domain: "app.example.com", Image: "myapp:v2"}251 route := domain.Route{Domain: "app.example.com", Image: "myapp:v2"}
252 252
253- handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, registrySvc, nil, testLogger(), nil)253+ handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, registrySvc, nil, testLogger(), nil, nil)
254 254
255 configSvc.EXPECT().UpdateRoute(mock.Anything, route).Return(nil).Once()255 configSvc.EXPECT().UpdateRoute(mock.Anything, route).Return(nil).Once()
256 256
@@ -271,7 +271,7 @@ func TestHandler_AttachmentsByImageGet_OneTarget(t *testing.T) {
271 containerSvc := inmocks.NewMockContainerService(t)271 containerSvc := inmocks.NewMockContainerService(t)
272 secretSvc := inmocks.NewMockSecretService(t)272 secretSvc := inmocks.NewMockSecretService(t)
273 273
274- handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger(), nil)274+ handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger(), nil, nil)
275 275
276 configSvc.EXPECT().FindAttachmentTargetsByImage(mock.Anything, "postgres:16").Return([]string{"app.example.com"}).Once()276 configSvc.EXPECT().FindAttachmentTargetsByImage(mock.Anything, "postgres:16").Return([]string{"app.example.com"}).Once()
277 277
@@ -291,7 +291,7 @@ func TestHandler_AttachmentsByImageGet_MultipleTargets(t *testing.T) {
291 containerSvc := inmocks.NewMockContainerService(t)291 containerSvc := inmocks.NewMockContainerService(t)
292 secretSvc := inmocks.NewMockSecretService(t)292 secretSvc := inmocks.NewMockSecretService(t)
293 293
294- handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger(), nil)294+ handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger(), nil, nil)
295 295
296 configSvc.EXPECT().FindAttachmentTargetsByImage(mock.Anything, "postgres").Return([]string{"app.example.com", "workers"}).Once()296 configSvc.EXPECT().FindAttachmentTargetsByImage(mock.Anything, "postgres").Return([]string{"app.example.com", "workers"}).Once()
297 297
@@ -311,7 +311,7 @@ func TestHandler_AttachmentsByImageGet_URLDecodedImage(t *testing.T) {
311 containerSvc := inmocks.NewMockContainerService(t)311 containerSvc := inmocks.NewMockContainerService(t)
312 secretSvc := inmocks.NewMockSecretService(t)312 secretSvc := inmocks.NewMockSecretService(t)
313 313
314- handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger(), nil)314+ handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger(), nil, nil)
315 315
316 configSvc.EXPECT().FindAttachmentTargetsByImage(mock.Anything, "registry/org/image:tag").Return([]string{"app.example.com"}).Once()316 configSvc.EXPECT().FindAttachmentTargetsByImage(mock.Anything, "registry/org/image:tag").Return([]string{"app.example.com"}).Once()
317 317
@@ -331,7 +331,7 @@ func TestHandler_AttachmentsByImageGet_EmptyResultSet(t *testing.T) {
331 containerSvc := inmocks.NewMockContainerService(t)331 containerSvc := inmocks.NewMockContainerService(t)
332 secretSvc := inmocks.NewMockSecretService(t)332 secretSvc := inmocks.NewMockSecretService(t)
333 333
334- handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger(), nil)334+ handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger(), nil, nil)
335 335
336 configSvc.EXPECT().FindAttachmentTargetsByImage(mock.Anything, "missing:latest").Return([]string{}).Once()336 configSvc.EXPECT().FindAttachmentTargetsByImage(mock.Anything, "missing:latest").Return([]string{}).Once()
337 337
@@ -351,7 +351,7 @@ func TestHandler_AttachmentsByImageGet_MissingImagePath(t *testing.T) {
351 containerSvc := inmocks.NewMockContainerService(t)351 containerSvc := inmocks.NewMockContainerService(t)
352 secretSvc := inmocks.NewMockSecretService(t)352 secretSvc := inmocks.NewMockSecretService(t)
353 353
354- handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger(), nil)354+ handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger(), nil, nil)
355 355
356 req := httptest.NewRequest("GET", "/admin/attachments/by-image", nil)356 req := httptest.NewRequest("GET", "/admin/attachments/by-image", nil)
357 req = req.WithContext(ctxWithScopes("admin:config:read"))357 req = req.WithContext(ctxWithScopes("admin:config:read"))
@@ -369,7 +369,7 @@ func TestHandler_AttachmentsByImageGet_InvalidEncoding(t *testing.T) {
369 containerSvc := inmocks.NewMockContainerService(t)369 containerSvc := inmocks.NewMockContainerService(t)
370 secretSvc := inmocks.NewMockSecretService(t)370 secretSvc := inmocks.NewMockSecretService(t)
371 371
372- handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger(), nil)372+ handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger(), nil, nil)
373 373
374 req := httptest.NewRequest("GET", "/admin/attachments/by-image/placeholder", nil)374 req := httptest.NewRequest("GET", "/admin/attachments/by-image/placeholder", nil)
375 req.URL = &url.URL{Path: "/admin/attachments/by-image/%zz"}375 req.URL = &url.URL{Path: "/admin/attachments/by-image/%zz"}
@@ -388,7 +388,7 @@ func TestHandler_AttachmentsByImageGet_RequiresReadScope(t *testing.T) {
388 containerSvc := inmocks.NewMockContainerService(t)388 containerSvc := inmocks.NewMockContainerService(t)
389 secretSvc := inmocks.NewMockSecretService(t)389 secretSvc := inmocks.NewMockSecretService(t)
390 390
391- handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger(), nil)391+ handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger(), nil, nil)
392 392
393 req := httptest.NewRequest("GET", "/admin/attachments/by-image/postgres:16", nil)393 req := httptest.NewRequest("GET", "/admin/attachments/by-image/postgres:16", nil)
394 req = req.WithContext(ctxWithScopes("admin:routes:read"))394 req = req.WithContext(ctxWithScopes("admin:routes:read"))
@@ -406,7 +406,7 @@ func TestHandler_RoutesDelete_RequiresWriteScope(t *testing.T) {
406 containerSvc := inmocks.NewMockContainerService(t)406 containerSvc := inmocks.NewMockContainerService(t)
407 secretSvc := inmocks.NewMockSecretService(t)407 secretSvc := inmocks.NewMockSecretService(t)
408 408
409- handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger(), nil)409+ handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger(), nil, nil)
410 410
411 tests := []struct {411 tests := []struct {
412 name string412 name string
@@ -450,7 +450,7 @@ func TestHandler_SecretsGet_RequiresReadScope(t *testing.T) {
450 containerSvc := inmocks.NewMockContainerService(t)450 containerSvc := inmocks.NewMockContainerService(t)
451 secretSvc := inmocks.NewMockSecretService(t)451 secretSvc := inmocks.NewMockSecretService(t)
452 452
453- handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger(), nil)453+ handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger(), nil, nil)
454 454
455 tests := []struct {455 tests := []struct {
456 name string456 name string
@@ -507,7 +507,7 @@ func TestHandler_SecretsPost_RequiresWriteScope(t *testing.T) {
507 containerSvc := inmocks.NewMockContainerService(t)507 containerSvc := inmocks.NewMockContainerService(t)
508 secretSvc := inmocks.NewMockSecretService(t)508 secretSvc := inmocks.NewMockSecretService(t)
509 509
510- handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger(), nil)510+ handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger(), nil, nil)
511 511
512 secretsJSON := `{"API_KEY": "secret123"}`512 secretsJSON := `{"API_KEY": "secret123"}`
513 513
@@ -556,7 +556,7 @@ func TestHandler_SecretsDelete_RequiresWriteScope(t *testing.T) {
556 containerSvc := inmocks.NewMockContainerService(t)556 containerSvc := inmocks.NewMockContainerService(t)
557 secretSvc := inmocks.NewMockSecretService(t)557 secretSvc := inmocks.NewMockSecretService(t)
558 558
559- handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger(), nil)559+ handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger(), nil, nil)
560 560
561 tests := []struct {561 tests := []struct {
562 name string562 name string
@@ -600,7 +600,7 @@ func TestHandler_AttachmentSecretsPost(t *testing.T) {
600 containerSvc := inmocks.NewMockContainerService(t)600 containerSvc := inmocks.NewMockContainerService(t)
601 secretSvc := inmocks.NewMockSecretService(t)601 secretSvc := inmocks.NewMockSecretService(t)
602 602
603- handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger(), nil)603+ handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger(), nil, nil)
604 604
605 secretSvc.EXPECT().SetAttachment(mock.Anything, "app.example.com", "redis", mock.AnythingOfType("map[string]string")).Return(nil)605 secretSvc.EXPECT().SetAttachment(mock.Anything, "app.example.com", "redis", mock.AnythingOfType("map[string]string")).Return(nil)
606 606
@@ -626,7 +626,7 @@ func TestHandler_AttachmentSecretsDelete(t *testing.T) {
626 containerSvc := inmocks.NewMockContainerService(t)626 containerSvc := inmocks.NewMockContainerService(t)
627 secretSvc := inmocks.NewMockSecretService(t)627 secretSvc := inmocks.NewMockSecretService(t)
628 628
629- handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger(), nil)629+ handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger(), nil, nil)
630 630
631 secretSvc.EXPECT().DeleteAttachment(mock.Anything, "app.example.com", "redis", "REDIS_URL").Return(nil)631 secretSvc.EXPECT().DeleteAttachment(mock.Anything, "app.example.com", "redis", "REDIS_URL").Return(nil)
632 632
@@ -652,7 +652,7 @@ func TestHandler_Bootstrap_HappyPath(t *testing.T) {
652 secretSvc := inmocks.NewMockSecretService(t)652 secretSvc := inmocks.NewMockSecretService(t)
653 registrySvc := inmocks.NewMockRegistryService(t)653 registrySvc := inmocks.NewMockRegistryService(t)
654 654
655- handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, registrySvc, nil, testLogger(), nil)655+ handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, registrySvc, nil, testLogger(), nil, nil)
656 656
657 configSvc.EXPECT().GetRegistryDomain().Return("reg.bnema.dev").Once()657 configSvc.EXPECT().GetRegistryDomain().Return("reg.bnema.dev").Once()
658 configSvc.EXPECT().AddRoute(mock.Anything, domain.Route{Domain: "app.example.com", Image: "reg.bnema.dev/myapp"}).Return(nil).Once()658 configSvc.EXPECT().AddRoute(mock.Anything, domain.Route{Domain: "app.example.com", Image: "reg.bnema.dev/myapp"}).Return(nil).Once()
@@ -699,7 +699,7 @@ func TestHandler_Bootstrap_RequiresPermissions(t *testing.T) {
699 containerSvc := inmocks.NewMockContainerService(t)699 containerSvc := inmocks.NewMockContainerService(t)
700 secretSvc := inmocks.NewMockSecretService(t)700 secretSvc := inmocks.NewMockSecretService(t)
701 701
702- handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger(), nil)702+ handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger(), nil, nil)
703 703
704 tests := []struct {704 tests := []struct {
705 name string705 name string
@@ -741,7 +741,7 @@ func TestHandler_Status_RequiresReadScope(t *testing.T) {
741 containerSvc := inmocks.NewMockContainerService(t)741 containerSvc := inmocks.NewMockContainerService(t)
742 secretSvc := inmocks.NewMockSecretService(t)742 secretSvc := inmocks.NewMockSecretService(t)
743 743
744- handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger(), nil)744+ handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger(), nil, nil)
745 745
746 tests := []struct {746 tests := []struct {
747 name string747 name string
@@ -795,7 +795,7 @@ func TestHandler_Config_RequiresReadScope(t *testing.T) {
795 containerSvc := inmocks.NewMockContainerService(t)795 containerSvc := inmocks.NewMockContainerService(t)
796 secretSvc := inmocks.NewMockSecretService(t)796 secretSvc := inmocks.NewMockSecretService(t)
797 797
798- handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger(), nil)798+ handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger(), nil, nil)
799 799
800 tests := []struct {800 tests := []struct {
801 name string801 name string
@@ -852,7 +852,7 @@ func TestHandler_Reload_RequiresWriteScope(t *testing.T) {
852 containerSvc := inmocks.NewMockContainerService(t)852 containerSvc := inmocks.NewMockContainerService(t)
853 secretSvc := inmocks.NewMockSecretService(t)853 secretSvc := inmocks.NewMockSecretService(t)
854 854
855- handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger(), nil)855+ handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger(), nil, nil)
856 856
857 tests := []struct {857 tests := []struct {
858 name string858 name string
@@ -906,7 +906,7 @@ func TestHandler_RoutesGet_ReturnsRoutes(t *testing.T) {
906 containerSvc := inmocks.NewMockContainerService(t)906 containerSvc := inmocks.NewMockContainerService(t)
907 secretSvc := inmocks.NewMockSecretService(t)907 secretSvc := inmocks.NewMockSecretService(t)
908 908
909- handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger(), nil)909+ handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger(), nil, nil)
910 910
911 expectedRoutes := []domain.Route{911 expectedRoutes := []domain.Route{
912 {Domain: "app1.example.com", Image: "app1:latest"},912 {Domain: "app1.example.com", Image: "app1:latest"},
@@ -941,7 +941,7 @@ func TestHandler_RoutesGet_SingleRoute(t *testing.T) {
941 containerSvc := inmocks.NewMockContainerService(t)941 containerSvc := inmocks.NewMockContainerService(t)
942 secretSvc := inmocks.NewMockSecretService(t)942 secretSvc := inmocks.NewMockSecretService(t)
943 943
944- handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger(), nil)944+ handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger(), nil, nil)
945 945
946 configSvc.EXPECT().GetRoute(mock.Anything, "app.example.com").Return(&domain.Route{946 configSvc.EXPECT().GetRoute(mock.Anything, "app.example.com").Return(&domain.Route{
947 Domain: "app.example.com", Image: "app:latest",947 Domain: "app.example.com", Image: "app:latest",
@@ -972,7 +972,7 @@ func TestHandler_RoutesGet_NotFound(t *testing.T) {
972 containerSvc := inmocks.NewMockContainerService(t)972 containerSvc := inmocks.NewMockContainerService(t)
973 secretSvc := inmocks.NewMockSecretService(t)973 secretSvc := inmocks.NewMockSecretService(t)
974 974
975- handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger(), nil)975+ handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger(), nil, nil)
976 976
977 configSvc.EXPECT().GetRoute(mock.Anything, "nonexistent.example.com").Return(nil, domain.ErrRouteNotFound)977 configSvc.EXPECT().GetRoute(mock.Anything, "nonexistent.example.com").Return(nil, domain.ErrRouteNotFound)
978 978
@@ -1016,7 +1016,7 @@ func TestHandler_RoutesPost_MissingFields(t *testing.T) {
1016 secretSvc := inmocks.NewMockSecretService(t)1016 secretSvc := inmocks.NewMockSecretService(t)
1017 registrySvc := inmocks.NewMockRegistryService(t)1017 registrySvc := inmocks.NewMockRegistryService(t)
1018 1018
1019- handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, registrySvc, nil, testLogger(), nil)1019+ handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, registrySvc, nil, testLogger(), nil, nil)
1020 1020
1021 configSvc.EXPECT().AddRoute(mock.Anything, mock.Anything).Return(tt.mockErr)1021 configSvc.EXPECT().AddRoute(mock.Anything, mock.Anything).Return(tt.mockErr)
1022 1022
@@ -1037,7 +1037,7 @@ func TestHandler_RoutesPut_MissingDomain(t *testing.T) {
1037 containerSvc := inmocks.NewMockContainerService(t)1037 containerSvc := inmocks.NewMockContainerService(t)
1038 secretSvc := inmocks.NewMockSecretService(t)1038 secretSvc := inmocks.NewMockSecretService(t)
1039 1039
1040- handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger(), nil)1040+ handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger(), nil, nil)
1041 1041
1042 req := httptest.NewRequest("PUT", "/admin/routes/", bytes.NewBufferString(`{"image": "app:latest"}`))1042 req := httptest.NewRequest("PUT", "/admin/routes/", bytes.NewBufferString(`{"image": "app:latest"}`))
1043 req = req.WithContext(ctxWithScopes("admin:routes:write"))1043 req = req.WithContext(ctxWithScopes("admin:routes:write"))
@@ -1054,7 +1054,7 @@ func TestHandler_RoutesDelete_MissingDomain(t *testing.T) {
1054 containerSvc := inmocks.NewMockContainerService(t)1054 containerSvc := inmocks.NewMockContainerService(t)
1055 secretSvc := inmocks.NewMockSecretService(t)1055 secretSvc := inmocks.NewMockSecretService(t)
1056 1056
1057- handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger(), nil)1057+ handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger(), nil, nil)
1058 1058
1059 req := httptest.NewRequest("DELETE", "/admin/routes/", nil)1059 req := httptest.NewRequest("DELETE", "/admin/routes/", nil)
1060 req = req.WithContext(ctxWithScopes("admin:routes:write"))1060 req = req.WithContext(ctxWithScopes("admin:routes:write"))
@@ -1071,7 +1071,7 @@ func TestHandler_Secrets_MissingDomain(t *testing.T) {
1071 containerSvc := inmocks.NewMockContainerService(t)1071 containerSvc := inmocks.NewMockContainerService(t)
1072 secretSvc := inmocks.NewMockSecretService(t)1072 secretSvc := inmocks.NewMockSecretService(t)
1073 1073
1074- handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger(), nil)1074+ handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger(), nil, nil)
1075 1075
1076 req := httptest.NewRequest("GET", "/admin/secrets/", nil)1076 req := httptest.NewRequest("GET", "/admin/secrets/", nil)
1077 req = req.WithContext(ctxWithScopes("admin:secrets:read"))1077 req = req.WithContext(ctxWithScopes("admin:secrets:read"))
@@ -1088,7 +1088,7 @@ func TestHandler_SecretsDelete_MissingKey(t *testing.T) {
1088 containerSvc := inmocks.NewMockContainerService(t)1088 containerSvc := inmocks.NewMockContainerService(t)
1089 secretSvc := inmocks.NewMockSecretService(t)1089 secretSvc := inmocks.NewMockSecretService(t)
1090 1090
1091- handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger(), nil)1091+ handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger(), nil, nil)
1092 1092
1093 req := httptest.NewRequest("DELETE", "/admin/secrets/app.example.com", nil)1093 req := httptest.NewRequest("DELETE", "/admin/secrets/app.example.com", nil)
1094 req = req.WithContext(ctxWithScopes("admin:secrets:write"))1094 req = req.WithContext(ctxWithScopes("admin:secrets:write"))
@@ -1105,7 +1105,7 @@ func TestHandler_MethodNotAllowed(t *testing.T) {
1105 containerSvc := inmocks.NewMockContainerService(t)1105 containerSvc := inmocks.NewMockContainerService(t)
1106 secretSvc := inmocks.NewMockSecretService(t)1106 secretSvc := inmocks.NewMockSecretService(t)
1107 1107
1108- handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger(), nil)1108+ handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger(), nil, nil)
1109 1109
1110 tests := []struct {1110 tests := []struct {
1111 method string1111 method string
@@ -1138,7 +1138,7 @@ func TestHandler_Restart_InvalidDomain(t *testing.T) {
1138 containerSvc := inmocks.NewMockContainerService(t)1138 containerSvc := inmocks.NewMockContainerService(t)
1139 secretSvc := inmocks.NewMockSecretService(t)1139 secretSvc := inmocks.NewMockSecretService(t)
1140 1140
1141- handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger(), nil)1141+ handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger(), nil, nil)
1142 1142
1143 tests := []struct {1143 tests := []struct {
1144 name string1144 name string
@@ -1168,7 +1168,7 @@ func TestHandler_Restart_ContainerNotFound(t *testing.T) {
1168 containerSvc := inmocks.NewMockContainerService(t)1168 containerSvc := inmocks.NewMockContainerService(t)
1169 secretSvc := inmocks.NewMockSecretService(t)1169 secretSvc := inmocks.NewMockSecretService(t)
1170 1170
1171- handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger(), nil)1171+ handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger(), nil, nil)
1172 1172
1173 containerSvc.EXPECT().Restart(mock.Anything, "nonexistent.example.com", false).Return(domain.ErrContainerNotFound)1173 containerSvc.EXPECT().Restart(mock.Anything, "nonexistent.example.com", false).Return(domain.ErrContainerNotFound)
1174 1174
@@ -1187,7 +1187,7 @@ func TestHandler_Restart_GenericError(t *testing.T) {
1187 containerSvc := inmocks.NewMockContainerService(t)1187 containerSvc := inmocks.NewMockContainerService(t)
1188 secretSvc := inmocks.NewMockSecretService(t)1188 secretSvc := inmocks.NewMockSecretService(t)
1189 1189
1190- handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger(), nil)1190+ handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger(), nil, nil)
1191 1191
1192 containerSvc.EXPECT().Restart(mock.Anything, "test.example.com", false).Return(fmt.Errorf("docker daemon error: secret details"))1192 containerSvc.EXPECT().Restart(mock.Anything, "test.example.com", false).Return(fmt.Errorf("docker daemon error: secret details"))
1193 1193
@@ -1209,7 +1209,7 @@ func TestHandler_Restart_Success(t *testing.T) {
1209 containerSvc := inmocks.NewMockContainerService(t)1209 containerSvc := inmocks.NewMockContainerService(t)
1210 secretSvc := inmocks.NewMockSecretService(t)1210 secretSvc := inmocks.NewMockSecretService(t)
1211 1211
1212- handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger(), nil)1212+ handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger(), nil, nil)
1213 1213
1214 tests := []struct {1214 tests := []struct {
1215 name string1215 name string
@@ -1256,7 +1256,7 @@ func TestHandler_Tags_InvalidRepository(t *testing.T) {
1256 containerSvc := inmocks.NewMockContainerService(t)1256 containerSvc := inmocks.NewMockContainerService(t)
1257 secretSvc := inmocks.NewMockSecretService(t)1257 secretSvc := inmocks.NewMockSecretService(t)
1258 1258
1259- handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger(), nil)1259+ handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger(), nil, nil)
1260 1260
1261 tests := []struct {1261 tests := []struct {
1262 name string1262 name string
@@ -1287,7 +1287,7 @@ func TestHandler_Tags_Success(t *testing.T) {
1287 secretSvc := inmocks.NewMockSecretService(t)1287 secretSvc := inmocks.NewMockSecretService(t)
1288 registrySvc := inmocks.NewMockRegistryService(t)1288 registrySvc := inmocks.NewMockRegistryService(t)
1289 1289
1290- handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, registrySvc, nil, testLogger(), nil)1290+ handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, registrySvc, nil, testLogger(), nil, nil)
1291 1291
1292 registrySvc.EXPECT().ListTags(mock.Anything, "myapp").Return([]string{"latest", "v1.0"}, nil)1292 registrySvc.EXPECT().ListTags(mock.Anything, "myapp").Return([]string{"latest", "v1.0"}, nil)
1293 1293
@@ -1314,7 +1314,7 @@ func TestHandler_Tags_Error(t *testing.T) {
1314 secretSvc := inmocks.NewMockSecretService(t)1314 secretSvc := inmocks.NewMockSecretService(t)
1315 registrySvc := inmocks.NewMockRegistryService(t)1315 registrySvc := inmocks.NewMockRegistryService(t)
1316 1316
1317- handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, registrySvc, nil, testLogger(), nil)1317+ handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, registrySvc, nil, testLogger(), nil, nil)
1318 1318
1319 registrySvc.EXPECT().ListTags(mock.Anything, "myapp").Return(nil, fmt.Errorf("registry error"))1319 registrySvc.EXPECT().ListTags(mock.Anything, "myapp").Return(nil, fmt.Errorf("registry error"))
1320 1320
@@ -1334,7 +1334,7 @@ func TestHandler_AttachmentSecretsPost_RequiresWriteScope(t *testing.T) {
1334 containerSvc := inmocks.NewMockContainerService(t)1334 containerSvc := inmocks.NewMockContainerService(t)
1335 secretSvc := inmocks.NewMockSecretService(t)1335 secretSvc := inmocks.NewMockSecretService(t)
1336 1336
1337- handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger(), nil)1337+ handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger(), nil, nil)
1338 1338
1339 body := `{"REDIS_URL": "redis://localhost:6379"}`1339 body := `{"REDIS_URL": "redis://localhost:6379"}`
1340 req := httptest.NewRequest("POST", "/admin/secrets/app.example.com/attachments/redis", bytes.NewBufferString(body))1340 req := httptest.NewRequest("POST", "/admin/secrets/app.example.com/attachments/redis", bytes.NewBufferString(body))
@@ -1352,7 +1352,7 @@ func TestHandler_AttachmentSecretsDelete_RequiresWriteScope(t *testing.T) {
1352 containerSvc := inmocks.NewMockContainerService(t)1352 containerSvc := inmocks.NewMockContainerService(t)
1353 secretSvc := inmocks.NewMockSecretService(t)1353 secretSvc := inmocks.NewMockSecretService(t)
1354 1354
1355- handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger(), nil)1355+ handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger(), nil, nil)
1356 1356
1357 req := httptest.NewRequest("DELETE", "/admin/secrets/app.example.com/attachments/redis/REDIS_URL", nil)1357 req := httptest.NewRequest("DELETE", "/admin/secrets/app.example.com/attachments/redis/REDIS_URL", nil)
1358 req = req.WithContext(ctxWithScopes("admin:secrets:read"))1358 req = req.WithContext(ctxWithScopes("admin:secrets:read"))
@@ -1369,7 +1369,7 @@ func TestHandler_AttachmentSecretsDelete_RequiresKey(t *testing.T) {
1369 containerSvc := inmocks.NewMockContainerService(t)1369 containerSvc := inmocks.NewMockContainerService(t)
1370 secretSvc := inmocks.NewMockSecretService(t)1370 secretSvc := inmocks.NewMockSecretService(t)
1371 1371
1372- handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger(), nil)1372+ handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger(), nil, nil)
1373 1373
1374 req := httptest.NewRequest("DELETE", "/admin/secrets/app.example.com/attachments/redis", nil)1374 req := httptest.NewRequest("DELETE", "/admin/secrets/app.example.com/attachments/redis", nil)
1375 req = req.WithContext(ctxWithScopes("admin:secrets:write"))1375 req = req.WithContext(ctxWithScopes("admin:secrets:write"))
@@ -1387,7 +1387,7 @@ func TestHandler_AttachmentSecretsPost_InvalidJSON(t *testing.T) {
1387 containerSvc := inmocks.NewMockContainerService(t)1387 containerSvc := inmocks.NewMockContainerService(t)
1388 secretSvc := inmocks.NewMockSecretService(t)1388 secretSvc := inmocks.NewMockSecretService(t)
1389 1389
1390- handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger(), nil)1390+ handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger(), nil, nil)
1391 1391
1392 body := `{not valid json`1392 body := `{not valid json`
1393 req := httptest.NewRequest("POST", "/admin/secrets/app.example.com/attachments/redis", bytes.NewBufferString(body))1393 req := httptest.NewRequest("POST", "/admin/secrets/app.example.com/attachments/redis", bytes.NewBufferString(body))
@@ -1406,7 +1406,7 @@ func TestHandler_NotFound(t *testing.T) {
1406 containerSvc := inmocks.NewMockContainerService(t)1406 containerSvc := inmocks.NewMockContainerService(t)
1407 secretSvc := inmocks.NewMockSecretService(t)1407 secretSvc := inmocks.NewMockSecretService(t)
1408 1408
1409- handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger(), nil)1409+ handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger(), nil, nil)
1410 1410
1411 req := httptest.NewRequest("GET", "/admin/unknown", nil)1411 req := httptest.NewRequest("GET", "/admin/unknown", nil)
1412 req = req.WithContext(ctxWithScopes("admin:*:*"))1412 req = req.WithContext(ctxWithScopes("admin:*:*"))
@@ -1426,7 +1426,7 @@ func TestHandler_BackupsStatus(t *testing.T) {
1426 1426
1427 backupSvc.EXPECT().Status(mock.Anything).Return([]domain.BackupJob{{Domain: "app.example.com", DBName: "postgres", Status: domain.BackupStatusCompleted, FilePath: "/var/lib/gordon/backups/private.bak"}}, nil)1427 backupSvc.EXPECT().Status(mock.Anything).Return([]domain.BackupJob{{Domain: "app.example.com", DBName: "postgres", Status: domain.BackupStatusCompleted, FilePath: "/var/lib/gordon/backups/private.bak"}}, nil)
1428 1428
1429- handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger(), backupSvc)1429+ handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger(), backupSvc, nil)
1430 1430
1431 req := httptest.NewRequest("GET", "/admin/backups/status", nil)1431 req := httptest.NewRequest("GET", "/admin/backups/status", nil)
1432 req = req.WithContext(ctxWithScopes("admin:status:read"))1432 req = req.WithContext(ctxWithScopes("admin:status:read"))
@@ -1448,7 +1448,7 @@ func TestHandler_BackupsListDomain(t *testing.T) {
1448 1448
1449 backupSvc.EXPECT().ListBackups(mock.Anything, "app.example.com").Return([]domain.BackupJob{{Domain: "app.example.com", DBName: "postgres", Status: domain.BackupStatusCompleted}}, nil)1449 backupSvc.EXPECT().ListBackups(mock.Anything, "app.example.com").Return([]domain.BackupJob{{Domain: "app.example.com", DBName: "postgres", Status: domain.BackupStatusCompleted}}, nil)
1450 1450
1451- handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger(), backupSvc)1451+ handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger(), backupSvc, nil)
1452 1452
1453 req := httptest.NewRequest("GET", "/admin/backups/app.example.com", nil)1453 req := httptest.NewRequest("GET", "/admin/backups/app.example.com", nil)
1454 req = req.WithContext(ctxWithScopes("admin:status:read"))1454 req = req.WithContext(ctxWithScopes("admin:status:read"))
@@ -1469,7 +1469,7 @@ func TestHandler_BackupsRunDomain(t *testing.T) {
1469 1469
1470 backupSvc.EXPECT().RunBackup(mock.Anything, "app.example.com", "postgres").Return(&domain.BackupResult{Job: domain.BackupJob{Domain: "app.example.com", DBName: "postgres", Status: domain.BackupStatusCompleted}}, nil)1470 backupSvc.EXPECT().RunBackup(mock.Anything, "app.example.com", "postgres").Return(&domain.BackupResult{Job: domain.BackupJob{Domain: "app.example.com", DBName: "postgres", Status: domain.BackupStatusCompleted}}, nil)
1471 1471
1472- handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger(), backupSvc)1472+ handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger(), backupSvc, nil)
1473 1473
1474 body := bytes.NewBufferString(`{"db":"postgres"}`)1474 body := bytes.NewBufferString(`{"db":"postgres"}`)
1475 req := httptest.NewRequest("POST", "/admin/backups/app.example.com", body)1475 req := httptest.NewRequest("POST", "/admin/backups/app.example.com", body)
@@ -1491,7 +1491,7 @@ func TestHandler_BackupsRunDomain_ChunkedBodyIsDecoded(t *testing.T) {
1491 1491
1492 backupSvc.EXPECT().RunBackup(mock.Anything, "app.example.com", "postgres").Return(&domain.BackupResult{Job: domain.BackupJob{Domain: "app.example.com", DBName: "postgres", Status: domain.BackupStatusCompleted}}, nil)1492 backupSvc.EXPECT().RunBackup(mock.Anything, "app.example.com", "postgres").Return(&domain.BackupResult{Job: domain.BackupJob{Domain: "app.example.com", DBName: "postgres", Status: domain.BackupStatusCompleted}}, nil)
1493 1493
1494- handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger(), backupSvc)1494+ handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger(), backupSvc, nil)
1495 1495
1496 body := bytes.NewBufferString(`{"db":"postgres"}`)1496 body := bytes.NewBufferString(`{"db":"postgres"}`)
1497 req := httptest.NewRequest("POST", "/admin/backups/app.example.com", body)1497 req := httptest.NewRequest("POST", "/admin/backups/app.example.com", body)
@@ -1514,7 +1514,7 @@ func TestHandler_BackupsDetectDomain(t *testing.T) {
1514 1514
1515 backupSvc.EXPECT().DetectDatabases(mock.Anything, "app.example.com").Return([]domain.DBInfo{{Type: domain.DBTypePostgreSQL, Name: "postgres", Port: 5432}}, nil)1515 backupSvc.EXPECT().DetectDatabases(mock.Anything, "app.example.com").Return([]domain.DBInfo{{Type: domain.DBTypePostgreSQL, Name: "postgres", Port: 5432}}, nil)
1516 1516
1517- handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger(), backupSvc)1517+ handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger(), backupSvc, nil)
1518 1518
1519 req := httptest.NewRequest("GET", "/admin/backups/app.example.com/detect", nil)1519 req := httptest.NewRequest("GET", "/admin/backups/app.example.com/detect", nil)
1520 req = req.WithContext(ctxWithScopes("admin:status:read"))1520 req = req.WithContext(ctxWithScopes("admin:status:read"))
@@ -1556,7 +1556,7 @@ func TestHandler_ImagesGet_ReturnsMappedList(t *testing.T) {
1556 },1556 },
1557 }1557 }
1558 1558
1559- handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger(), nil, imageSvc)1559+ handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger(), nil, nil, imageSvc)
1560 1560
1561 req := httptest.NewRequest("GET", "/admin/images", nil)1561 req := httptest.NewRequest("GET", "/admin/images", nil)
1562 req = req.WithContext(ctxWithScopes("admin:status:read"))1562 req = req.WithContext(ctxWithScopes("admin:status:read"))
@@ -1617,7 +1617,7 @@ func TestHandler_ImagesPrune_AcceptsOptionalKeepLast(t *testing.T) {
1617 },1617 },
1618 }1618 }
1619 1619
1620- handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger(), nil, imageSvc)1620+ handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger(), nil, nil, imageSvc)
1621 1621
1622 req := httptest.NewRequest("POST", "/admin/images/prune", bytes.NewBufferString(tt.body))1622 req := httptest.NewRequest("POST", "/admin/images/prune", bytes.NewBufferString(tt.body))
1623 req = req.WithContext(ctxWithScopes("admin:config:write"))1623 req = req.WithContext(ctxWithScopes("admin:config:write"))
@@ -1646,7 +1646,7 @@ func TestHandler_Images_ErrorMappingForServiceFailures(t *testing.T) {
1646 },1646 },
1647 }1647 }
1648 1648
1649- handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger(), nil, imageSvc)1649+ handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger(), nil, nil, imageSvc)
1650 1650
1651 req := httptest.NewRequest("GET", "/admin/images", nil)1651 req := httptest.NewRequest("GET", "/admin/images", nil)
1652 req = req.WithContext(ctxWithScopes("admin:status:read"))1652 req = req.WithContext(ctxWithScopes("admin:status:read"))
@@ -1671,7 +1671,7 @@ func TestHandler_Images_ErrorMappingForServiceFailures(t *testing.T) {
1671 },1671 },
1672 }1672 }
1673 1673
1674- handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger(), nil, imageSvc)1674+ handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger(), nil, nil, imageSvc)
1675 1675
1676 req := httptest.NewRequest("POST", "/admin/images/prune", bytes.NewBufferString(`{"keep_last": 2}`))1676 req := httptest.NewRequest("POST", "/admin/images/prune", bytes.NewBufferString(`{"keep_last": 2}`))
1677 req = req.WithContext(ctxWithScopes("admin:config:write"))1677 req = req.WithContext(ctxWithScopes("admin:config:write"))
@@ -1692,7 +1692,7 @@ func TestHandler_ImagesPrune_ValidationAndAvailability(t *testing.T) {
1692 containerSvc := inmocks.NewMockContainerService(t)1692 containerSvc := inmocks.NewMockContainerService(t)
1693 secretSvc := inmocks.NewMockSecretService(t)1693 secretSvc := inmocks.NewMockSecretService(t)
1694 1694
1695- handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger(), nil, &stubImageService{})1695+ handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger(), nil, nil, &stubImageService{})
1696 1696
1697 req := httptest.NewRequest("POST", "/admin/images/prune", bytes.NewBufferString(`{"keep_last": -1}`))1697 req := httptest.NewRequest("POST", "/admin/images/prune", bytes.NewBufferString(`{"keep_last": -1}`))
1698 req = req.WithContext(ctxWithScopes("admin:config:write"))1698 req = req.WithContext(ctxWithScopes("admin:config:write"))
@@ -1710,7 +1710,7 @@ func TestHandler_ImagesPrune_ValidationAndAvailability(t *testing.T) {
1710 containerSvc := inmocks.NewMockContainerService(t)1710 containerSvc := inmocks.NewMockContainerService(t)
1711 secretSvc := inmocks.NewMockSecretService(t)1711 secretSvc := inmocks.NewMockSecretService(t)
1712 1712
1713- handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger(), nil)1713+ handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger(), nil, nil)
1714 1714
1715 req := httptest.NewRequest("GET", "/admin/images", nil)1715 req := httptest.NewRequest("GET", "/admin/images", nil)
1716 req = req.WithContext(ctxWithScopes("admin:status:read"))1716 req = req.WithContext(ctxWithScopes("admin:status:read"))
@@ -1781,7 +1781,7 @@ func TestHandler_ImagesPrune_ScopeDefaults(t *testing.T) {
1781 },1781 },
1782 }1782 }
1783 1783
1784- handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger(), nil, imageSvc)1784+ handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger(), nil, nil, imageSvc)
1785 1785
1786 req := httptest.NewRequest("POST", "/admin/images/prune", bytes.NewBufferString(tt.body))1786 req := httptest.NewRequest("POST", "/admin/images/prune", bytes.NewBufferString(tt.body))
1787 req = req.WithContext(ctxWithScopes("admin:config:write"))1787 req = req.WithContext(ctxWithScopes("admin:config:write"))
@@ -1806,7 +1806,7 @@ func TestHandler_Images_Authorization(t *testing.T) {
1806 containerSvc := inmocks.NewMockContainerService(t)1806 containerSvc := inmocks.NewMockContainerService(t)
1807 secretSvc := inmocks.NewMockSecretService(t)1807 secretSvc := inmocks.NewMockSecretService(t)
1808 1808
1809- handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger(), nil, &stubImageService{})1809+ handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, inmocks.NewMockRegistryService(t), nil, testLogger(), nil, nil, &stubImageService{})
1810 1810
1811 t.Run("images list requires status:read", func(t *testing.T) {1811 t.Run("images list requires status:read", func(t *testing.T) {
1812 req := httptest.NewRequest("GET", "/admin/images", nil)1812 req := httptest.NewRequest("GET", "/admin/images", nil)
A internal/adapters/in/http/admin/preview_handler.go +140 -0
@@ -0,0 +1,140 @@
1+package admin
2+
3+import (
4+ "context"
5+ "encoding/json"
6+ "net/http"
7+ "strings"
8+ "time"
9+
10+ "github.com/bnema/zerowrap"
11+
12+ "github.com/bnema/gordon/internal/domain"
13+)
14+
15+// previewService is a minimal interface for preview management used by the admin handler.
16+type previewService interface {
17+ List(ctx context.Context) ([]domain.PreviewRoute, error)
18+ Delete(ctx context.Context, name string) error
19+ Extend(ctx context.Context, name string, ttl time.Duration) error
20+}
21+
22+// previewListResponse is the JSON payload returned by GET /admin/previews.
23+type previewListResponse struct {
24+ Previews []domain.PreviewRoute `json:"previews"`
25+}
26+
27+// previewExtendRequest is the JSON body for PATCH /admin/preview/{name}.
28+type previewExtendRequest struct {
29+ TTL string `json:"ttl"` // e.g. "24h", "30m"
30+}
31+
32+// handlePreviewList handles GET /admin/previews.
33+// Returns a JSON array of all active preview environments.
34+func (h *Handler) handlePreviewList(w http.ResponseWriter, r *http.Request, _ string) {
35+ if r.Method != http.MethodGet {
36+ h.sendError(w, http.StatusMethodNotAllowed, "method not allowed")
37+ return
38+ }
39+
40+ ctx := r.Context()
41+
42+ if !HasAccess(ctx, domain.AdminResourceStatus, domain.AdminActionRead) {
43+ h.sendError(w, http.StatusForbidden, "insufficient permissions for status:read")
44+ return
45+ }
46+
47+ if h.previewSvc == nil {
48+ h.sendError(w, http.StatusServiceUnavailable, "preview service not available")
49+ return
50+ }
51+
52+ previews, err := h.previewSvc.List(ctx)
53+ if err != nil {
54+ log := zerowrap.FromCtx(ctx)
55+ log.Error().Err(err).Msg("failed to list previews")
56+ h.sendError(w, http.StatusInternalServerError, "failed to list previews")
57+ return
58+ }
59+
60+ if previews == nil {
61+ previews = []domain.PreviewRoute{}
62+ }
63+
64+ h.sendJSON(w, http.StatusOK, previewListResponse{Previews: previews})
65+}
66+
67+// handlePreviewAction handles DELETE and PATCH on /admin/preview/{name}.
68+//
69+// DELETE /admin/preview/{name} — tear down the preview
70+// PATCH /admin/preview/{name} — extend TTL (body: {"ttl":"24h"})
71+func (h *Handler) handlePreviewAction(w http.ResponseWriter, r *http.Request, path string) {
72+ ctx := r.Context()
73+ log := zerowrap.FromCtx(ctx)
74+
75+ if h.previewSvc == nil {
76+ h.sendError(w, http.StatusServiceUnavailable, "preview service not available")
77+ return
78+ }
79+
80+ name := strings.TrimPrefix(path, "/preview/")
81+ if name == "" || name == "/preview" {
82+ h.sendError(w, http.StatusBadRequest, "preview name required in path")
83+ return
84+ }
85+
86+ switch r.Method {
87+ case http.MethodDelete:
88+ if !HasAccess(ctx, domain.AdminResourceConfig, domain.AdminActionWrite) {
89+ h.sendError(w, http.StatusForbidden, "insufficient permissions for config:write")
90+ return
91+ }
92+
93+ if err := h.previewSvc.Delete(ctx, name); err != nil {
94+ log.Error().Err(err).Str("name", name).Msg("failed to delete preview")
95+ h.sendError(w, http.StatusNotFound, "preview not found")
96+ return
97+ }
98+
99+ log.Info().Str("name", name).Msg("preview deleted via admin API")
100+ h.sendJSON(w, http.StatusOK, map[string]string{"status": "deleted", "name": name})
101+
102+ case http.MethodPatch:
103+ if !HasAccess(ctx, domain.AdminResourceConfig, domain.AdminActionWrite) {
104+ h.sendError(w, http.StatusForbidden, "insufficient permissions for config:write")
105+ return
106+ }
107+
108+ r.Body = http.MaxBytesReader(w, r.Body, maxAdminRequestSize)
109+
110+ var req previewExtendRequest
111+ if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
112+ log.Warn().Err(err).Msg("invalid preview extend JSON")
113+ h.sendError(w, http.StatusBadRequest, "invalid JSON")
114+ return
115+ }
116+
117+ if req.TTL == "" {
118+ h.sendError(w, http.StatusBadRequest, "ttl is required")
119+ return
120+ }
121+
122+ ttl, err := time.ParseDuration(req.TTL)
123+ if err != nil {
124+ h.sendError(w, http.StatusBadRequest, "invalid ttl: use Go duration format, e.g. 24h or 30m")
125+ return
126+ }
127+
128+ if err := h.previewSvc.Extend(ctx, name, ttl); err != nil {
129+ log.Error().Err(err).Str("name", name).Dur("ttl", ttl).Msg("failed to extend preview TTL")
130+ h.sendError(w, http.StatusNotFound, "preview not found")
131+ return
132+ }
133+
134+ log.Info().Str("name", name).Dur("ttl", ttl).Msg("preview TTL extended via admin API")
135+ h.sendJSON(w, http.StatusOK, map[string]string{"status": "extended", "name": name, "ttl": req.TTL})
136+
137+ default:
138+ h.sendError(w, http.StatusMethodNotAllowed, "method not allowed")
139+ }
140+}
@@ -0,0 +1,140 @@
1+package admin
2+
3+import (
4+ "context"
5+ "encoding/json"
6+ "net/http"
7+ "strings"
8+ "time"
9+
10+ "github.com/bnema/zerowrap"
11+
12+ "github.com/bnema/gordon/internal/domain"
13+)
14+
15+// previewService is a minimal interface for preview management used by the admin handler.
16+type previewService interface {
17+ List(ctx context.Context) ([]domain.PreviewRoute, error)
18+ Delete(ctx context.Context, name string) error
19+ Extend(ctx context.Context, name string, ttl time.Duration) error
20+}
21+
22+// previewListResponse is the JSON payload returned by GET /admin/previews.
23+type previewListResponse struct {
24+ Previews []domain.PreviewRoute `json:"previews"`
25+}
26+
27+// previewExtendRequest is the JSON body for PATCH /admin/preview/{name}.
28+type previewExtendRequest struct {
29+ TTL string `json:"ttl"` // e.g. "24h", "30m"
30+}
31+
32+// handlePreviewList handles GET /admin/previews.
33+// Returns a JSON array of all active preview environments.
34+func (h *Handler) handlePreviewList(w http.ResponseWriter, r *http.Request, _ string) {
35+ if r.Method != http.MethodGet {
36+ h.sendError(w, http.StatusMethodNotAllowed, "method not allowed")
37+ return
38+ }
39+
40+ ctx := r.Context()
41+
42+ if !HasAccess(ctx, domain.AdminResourceStatus, domain.AdminActionRead) {
43+ h.sendError(w, http.StatusForbidden, "insufficient permissions for status:read")
44+ return
45+ }
46+
47+ if h.previewSvc == nil {
48+ h.sendError(w, http.StatusServiceUnavailable, "preview service not available")
49+ return
50+ }
51+
52+ previews, err := h.previewSvc.List(ctx)
53+ if err != nil {
54+ log := zerowrap.FromCtx(ctx)
55+ log.Error().Err(err).Msg("failed to list previews")
56+ h.sendError(w, http.StatusInternalServerError, "failed to list previews")
57+ return
58+ }
59+
60+ if previews == nil {
61+ previews = []domain.PreviewRoute{}
62+ }
63+
64+ h.sendJSON(w, http.StatusOK, previewListResponse{Previews: previews})
65+}
66+
67+// handlePreviewAction handles DELETE and PATCH on /admin/preview/{name}.
68+//
69+// DELETE /admin/preview/{name} — tear down the preview
70+// PATCH /admin/preview/{name} — extend TTL (body: {"ttl":"24h"})
71+func (h *Handler) handlePreviewAction(w http.ResponseWriter, r *http.Request, path string) {
72+ ctx := r.Context()
73+ log := zerowrap.FromCtx(ctx)
74+
75+ if h.previewSvc == nil {
76+ h.sendError(w, http.StatusServiceUnavailable, "preview service not available")
77+ return
78+ }
79+
80+ name := strings.TrimPrefix(path, "/preview/")
81+ if name == "" || name == "/preview" {
82+ h.sendError(w, http.StatusBadRequest, "preview name required in path")
83+ return
84+ }
85+
86+ switch r.Method {
87+ case http.MethodDelete:
88+ if !HasAccess(ctx, domain.AdminResourceConfig, domain.AdminActionWrite) {
89+ h.sendError(w, http.StatusForbidden, "insufficient permissions for config:write")
90+ return
91+ }
92+
93+ if err := h.previewSvc.Delete(ctx, name); err != nil {
94+ log.Error().Err(err).Str("name", name).Msg("failed to delete preview")
95+ h.sendError(w, http.StatusNotFound, "preview not found")
96+ return
97+ }
98+
99+ log.Info().Str("name", name).Msg("preview deleted via admin API")
100+ h.sendJSON(w, http.StatusOK, map[string]string{"status": "deleted", "name": name})
101+
102+ case http.MethodPatch:
103+ if !HasAccess(ctx, domain.AdminResourceConfig, domain.AdminActionWrite) {
104+ h.sendError(w, http.StatusForbidden, "insufficient permissions for config:write")
105+ return
106+ }
107+
108+ r.Body = http.MaxBytesReader(w, r.Body, maxAdminRequestSize)
109+
110+ var req previewExtendRequest
111+ if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
112+ log.Warn().Err(err).Msg("invalid preview extend JSON")
113+ h.sendError(w, http.StatusBadRequest, "invalid JSON")
114+ return
115+ }
116+
117+ if req.TTL == "" {
118+ h.sendError(w, http.StatusBadRequest, "ttl is required")
119+ return
120+ }
121+
122+ ttl, err := time.ParseDuration(req.TTL)
123+ if err != nil {
124+ h.sendError(w, http.StatusBadRequest, "invalid ttl: use Go duration format, e.g. 24h or 30m")
125+ return
126+ }
127+
128+ if err := h.previewSvc.Extend(ctx, name, ttl); err != nil {
129+ log.Error().Err(err).Str("name", name).Dur("ttl", ttl).Msg("failed to extend preview TTL")
130+ h.sendError(w, http.StatusNotFound, "preview not found")
131+ return
132+ }
133+
134+ log.Info().Str("name", name).Dur("ttl", ttl).Msg("preview TTL extended via admin API")
135+ h.sendJSON(w, http.StatusOK, map[string]string{"status": "extended", "name": name, "ttl": req.TTL})
136+
137+ default:
138+ h.sendError(w, http.StatusMethodNotAllowed, "method not allowed")
139+ }
140+}
M internal/app/run.go +1 -0
@@ -553,6 +553,7 @@ func createServices(ctx context.Context, v *viper.Viper, cfg Config, log zerowra
553553 svc.eventBus,
554554 log,
555555 svc.backupSvc,
556+ nil, // previewSvc: wired in Task 13
556557 svc.imageSvc,
557558 ).WithVolumeService(svc.volumeSvc)
558559
@@ -553,6 +553,7 @@ func createServices(ctx context.Context, v *viper.Viper, cfg Config, log zerowra
553 svc.eventBus,553 svc.eventBus,
554 log,554 log,
555 svc.backupSvc,555 svc.backupSvc,
556+ nil, // previewSvc: wired in Task 13
556 svc.imageSvc,557 svc.imageSvc,
557 ).WithVolumeService(svc.volumeSvc)558 ).WithVolumeService(svc.volumeSvc)
558 559

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