detached ad3801f

feat(app): wire preview service, dispatcher, and TTL ticker in run.go

commit
ad3801f1c2b20eec66910f39b50a6c12a3ea9b83
author
brice · · 2026-03-19 05:10 UTC
committer
brice · · 2026-03-19 05:13 UTC
parent
458a175
signature
unsigned
1 file · +40 -3
M internal/app/run.go +40 -3
@@ -64,6 +64,8 @@ import (
6464
6565 // Use cases
6666 "github.com/bnema/gordon/internal/usecase/auth"
67+ "github.com/bnema/gordon/internal/usecase/auto"
68+ "github.com/bnema/gordon/internal/usecase/auto/preview"
6769 "github.com/bnema/gordon/internal/usecase/backup"
6870 "github.com/bnema/gordon/internal/usecase/config"
6971 "github.com/bnema/gordon/internal/usecase/container"
@@ -192,6 +194,8 @@ type services struct {
192194 adminHandler *admin.Handler
193195 internalRegUser string
194196 internalRegPass string
197+ previewStore *filesystem.PreviewStore
198+ previewService *preview.Service
195199 envDir string
196200 maxBlobChunkSize int64
197201 }
@@ -541,6 +545,27 @@ func createServices(ctx context.Context, v *viper.Viper, cfg Config, log zerowra
541545 // Create log service for accessing logs via admin API
542546 svc.logSvc = logs.NewService(resolveLogFilePath(cfg), cfg.Logging.File.Enabled, svc.containerSvc, svc.runtime, log)
543547
548+ // Preview store and service
549+ previewStorePath := filepath.Join(resolveDataDir(cfg.Server.DataDir), "previews.json")
550+ svc.previewStore = filesystem.NewPreviewStore(previewStorePath)
551+ previewConfig := svc.configSvc.GetPreviewConfig()
552+ svc.previewService = preview.NewService(svc.previewStore, previewConfig.TTL)
553+ if err := svc.previewService.Load(ctx); err != nil {
554+ log.Warn().Err(err).Msg("failed to load previews")
555+ }
556+
557+ // Start TTL ticker to expire and clean up preview environments
558+ svc.previewService.StartTicker(ctx, 60*time.Second, func(ctx context.Context, p domain.PreviewRoute) {
559+ for _, containerName := range p.Containers {
560+ _ = svc.runtime.StopContainer(ctx, containerName)
561+ _ = svc.runtime.RemoveContainer(ctx, containerName, true)
562+ }
563+ for _, volName := range p.Volumes {
564+ _ = svc.runtime.RemoveVolume(ctx, volName, true)
565+ }
566+ svc.proxySvc.InvalidateTarget(ctx, p.Domain)
567+ })
568+
544569 // Create admin handler for admin API
545570 svc.adminHandler = admin.NewHandler(
546571 svc.configSvc,
@@ -553,7 +578,7 @@ func createServices(ctx context.Context, v *viper.Viper, cfg Config, log zerowra
553578 svc.eventBus,
554579 log,
555580 svc.backupSvc,
556- nil, // previewSvc: wired in Task 13
581+ svc.previewService,
557582 svc.imageSvc,
558583 ).WithVolumeService(svc.volumeSvc)
559584
@@ -1350,8 +1375,20 @@ func registerEventHandlers(ctx context.Context, svc *services, cfg Config) (func
13501375 // Auto-route handler for creating routes from image labels
13511376 autoRouteHandler := container.NewAutoRouteHandler(ctx, svc.configSvc, svc.containerSvc, svc.blobStorage, cfg.Server.RegistryDomain).
13521377 WithEnvExtractor(svc.runtime, svc.envDir)
1353- if err := svc.eventBus.Subscribe(autoRouteHandler); err != nil {
1354- return nil, fmt.Errorf("failed to subscribe auto-route handler: %w", err)
1378+
1379+ // Preview handler for creating preview environments from tagged images
1380+ autoPreviewHandler := preview.NewAutoPreviewHandler(
1381+ ctx,
1382+ svc.configSvc,
1383+ svc.blobStorage,
1384+ svc.previewService,
1385+ cfg.Server.RegistryDomain,
1386+ )
1387+
1388+ // Dispatcher routes image push events to either auto-route or preview handler
1389+ dispatcher := auto.NewImagePushDispatcher(svc.configSvc, autoRouteHandler, autoPreviewHandler)
1390+ if err := svc.eventBus.Subscribe(dispatcher); err != nil {
1391+ return nil, fmt.Errorf("subscribe image push dispatcher: %w", err)
13551392 }
13561393
13571394 configReloadHandler := container.NewConfigReloadHandler(ctx, svc.containerSvc, svc.configSvc)
@@ -64,6 +64,8 @@ import (
64 64
65 // Use cases65 // Use cases
66 "github.com/bnema/gordon/internal/usecase/auth"66 "github.com/bnema/gordon/internal/usecase/auth"
67+ "github.com/bnema/gordon/internal/usecase/auto"
68+ "github.com/bnema/gordon/internal/usecase/auto/preview"
67 "github.com/bnema/gordon/internal/usecase/backup"69 "github.com/bnema/gordon/internal/usecase/backup"
68 "github.com/bnema/gordon/internal/usecase/config"70 "github.com/bnema/gordon/internal/usecase/config"
69 "github.com/bnema/gordon/internal/usecase/container"71 "github.com/bnema/gordon/internal/usecase/container"
@@ -192,6 +194,8 @@ type services struct {
192 adminHandler *admin.Handler194 adminHandler *admin.Handler
193 internalRegUser string195 internalRegUser string
194 internalRegPass string196 internalRegPass string
197+ previewStore *filesystem.PreviewStore
198+ previewService *preview.Service
195 envDir string199 envDir string
196 maxBlobChunkSize int64200 maxBlobChunkSize int64
197 }201 }
@@ -541,6 +545,27 @@ func createServices(ctx context.Context, v *viper.Viper, cfg Config, log zerowra
541 // Create log service for accessing logs via admin API545 // Create log service for accessing logs via admin API
542 svc.logSvc = logs.NewService(resolveLogFilePath(cfg), cfg.Logging.File.Enabled, svc.containerSvc, svc.runtime, log)546 svc.logSvc = logs.NewService(resolveLogFilePath(cfg), cfg.Logging.File.Enabled, svc.containerSvc, svc.runtime, log)
543 547
548+ // Preview store and service
549+ previewStorePath := filepath.Join(resolveDataDir(cfg.Server.DataDir), "previews.json")
550+ svc.previewStore = filesystem.NewPreviewStore(previewStorePath)
551+ previewConfig := svc.configSvc.GetPreviewConfig()
552+ svc.previewService = preview.NewService(svc.previewStore, previewConfig.TTL)
553+ if err := svc.previewService.Load(ctx); err != nil {
554+ log.Warn().Err(err).Msg("failed to load previews")
555+ }
556+
557+ // Start TTL ticker to expire and clean up preview environments
558+ svc.previewService.StartTicker(ctx, 60*time.Second, func(ctx context.Context, p domain.PreviewRoute) {
559+ for _, containerName := range p.Containers {
560+ _ = svc.runtime.StopContainer(ctx, containerName)
561+ _ = svc.runtime.RemoveContainer(ctx, containerName, true)
562+ }
563+ for _, volName := range p.Volumes {
564+ _ = svc.runtime.RemoveVolume(ctx, volName, true)
565+ }
566+ svc.proxySvc.InvalidateTarget(ctx, p.Domain)
567+ })
568+
544 // Create admin handler for admin API569 // Create admin handler for admin API
545 svc.adminHandler = admin.NewHandler(570 svc.adminHandler = admin.NewHandler(
546 svc.configSvc,571 svc.configSvc,
@@ -553,7 +578,7 @@ func createServices(ctx context.Context, v *viper.Viper, cfg Config, log zerowra
553 svc.eventBus,578 svc.eventBus,
554 log,579 log,
555 svc.backupSvc,580 svc.backupSvc,
556- nil, // previewSvc: wired in Task 13581+ svc.previewService,
557 svc.imageSvc,582 svc.imageSvc,
558 ).WithVolumeService(svc.volumeSvc)583 ).WithVolumeService(svc.volumeSvc)
559 584
@@ -1350,8 +1375,20 @@ func registerEventHandlers(ctx context.Context, svc *services, cfg Config) (func
1350 // Auto-route handler for creating routes from image labels1375 // Auto-route handler for creating routes from image labels
1351 autoRouteHandler := container.NewAutoRouteHandler(ctx, svc.configSvc, svc.containerSvc, svc.blobStorage, cfg.Server.RegistryDomain).1376 autoRouteHandler := container.NewAutoRouteHandler(ctx, svc.configSvc, svc.containerSvc, svc.blobStorage, cfg.Server.RegistryDomain).
1352 WithEnvExtractor(svc.runtime, svc.envDir)1377 WithEnvExtractor(svc.runtime, svc.envDir)
1353- if err := svc.eventBus.Subscribe(autoRouteHandler); err != nil {1378+
1354- return nil, fmt.Errorf("failed to subscribe auto-route handler: %w", err)1379+ // Preview handler for creating preview environments from tagged images
1380+ autoPreviewHandler := preview.NewAutoPreviewHandler(
1381+ ctx,
1382+ svc.configSvc,
1383+ svc.blobStorage,
1384+ svc.previewService,
1385+ cfg.Server.RegistryDomain,
1386+ )
1387+
1388+ // Dispatcher routes image push events to either auto-route or preview handler
1389+ dispatcher := auto.NewImagePushDispatcher(svc.configSvc, autoRouteHandler, autoPreviewHandler)
1390+ if err := svc.eventBus.Subscribe(dispatcher); err != nil {
1391+ return nil, fmt.Errorf("subscribe image push dispatcher: %w", err)
1355 }1392 }
1356 1393
1357 configReloadHandler := container.NewConfigReloadHandler(ctx, svc.containerSvc, svc.configSvc)1394 configReloadHandler := container.NewConfigReloadHandler(ctx, svc.containerSvc, svc.configSvc)

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