detached 322f9c0

feat(config): add FindRoutesByImage across all layers

commit
322f9c0fc94113006c35f9438e4868829b6ae786
author
bnema · · 2026-02-09 20:22 UTC
parent
aa8d1ec
signature
unsigned
9 files · +183 -0
M internal/adapters/dto/admin_route.go +6 -0
@@ -42,6 +42,12 @@ type AttachmentsResponse struct {
4242 Attachments []Attachment `json:"attachments"`
4343 }
4444
45+// RoutesByImageResponse represents routes found for a given image.
46+type RoutesByImageResponse struct {
47+ Image string `json:"image"`
48+ Routes []Route `json:"routes"`
49+}
50+
4551 // RouteDeleteResponse represents a route removal response.
4652 type RouteDeleteResponse struct {
4753 Status string `json:"status"`
@@ -42,6 +42,12 @@ type AttachmentsResponse struct {
42 Attachments []Attachment `json:"attachments"`42 Attachments []Attachment `json:"attachments"`
43 }43 }
44 44
45+// RoutesByImageResponse represents routes found for a given image.
46+type RoutesByImageResponse struct {
47+ Image string `json:"image"`
48+ Routes []Route `json:"routes"`
49+}
50+
45 // RouteDeleteResponse represents a route removal response.51 // RouteDeleteResponse represents a route removal response.
46 type RouteDeleteResponse struct {52 type RouteDeleteResponse struct {
47 Status string `json:"status"`53 Status string `json:"status"`
M internal/adapters/in/cli/controlplane.go +1 -0
@@ -16,6 +16,7 @@ type ControlPlane interface {
1616 ListRoutesWithDetails(ctx context.Context) ([]remote.RouteInfo, error)
1717 GetHealth(ctx context.Context) (map[string]*remote.RouteHealth, error)
1818 GetRoute(ctx context.Context, routeDomain string) (*domain.Route, error)
19+ FindRoutesByImage(ctx context.Context, imageName string) ([]domain.Route, error)
1920 AddRoute(ctx context.Context, route domain.Route) error
2021 UpdateRoute(ctx context.Context, route domain.Route) error
2122 RemoveRoute(ctx context.Context, routeDomain string) error
@@ -16,6 +16,7 @@ type ControlPlane interface {
16 ListRoutesWithDetails(ctx context.Context) ([]remote.RouteInfo, error)16 ListRoutesWithDetails(ctx context.Context) ([]remote.RouteInfo, error)
17 GetHealth(ctx context.Context) (map[string]*remote.RouteHealth, error)17 GetHealth(ctx context.Context) (map[string]*remote.RouteHealth, error)
18 GetRoute(ctx context.Context, routeDomain string) (*domain.Route, error)18 GetRoute(ctx context.Context, routeDomain string) (*domain.Route, error)
19+ FindRoutesByImage(ctx context.Context, imageName string) ([]domain.Route, error)
19 AddRoute(ctx context.Context, route domain.Route) error20 AddRoute(ctx context.Context, route domain.Route) error
20 UpdateRoute(ctx context.Context, route domain.Route) error21 UpdateRoute(ctx context.Context, route domain.Route) error
21 RemoveRoute(ctx context.Context, routeDomain string) error22 RemoveRoute(ctx context.Context, routeDomain string) error
M internal/adapters/in/cli/controlplane_local.go +7 -0
@@ -84,6 +84,13 @@ func (l *localControlPlane) GetRoute(ctx context.Context, routeDomain string) (*
8484 return l.configSvc.GetRoute(ctx, routeDomain)
8585 }
8686
87+func (l *localControlPlane) FindRoutesByImage(ctx context.Context, imageName string) ([]domain.Route, error) {
88+ if l.configSvc == nil {
89+ return nil, fmt.Errorf("local config service unavailable")
90+ }
91+ return l.configSvc.FindRoutesByImage(ctx, imageName), nil
92+}
93+
8794 func (l *localControlPlane) AddRoute(ctx context.Context, route domain.Route) error {
8895 if l.configSvc == nil {
8996 return fmt.Errorf("local config service unavailable")
@@ -84,6 +84,13 @@ func (l *localControlPlane) GetRoute(ctx context.Context, routeDomain string) (*
84 return l.configSvc.GetRoute(ctx, routeDomain)84 return l.configSvc.GetRoute(ctx, routeDomain)
85 }85 }
86 86
87+func (l *localControlPlane) FindRoutesByImage(ctx context.Context, imageName string) ([]domain.Route, error) {
88+ if l.configSvc == nil {
89+ return nil, fmt.Errorf("local config service unavailable")
90+ }
91+ return l.configSvc.FindRoutesByImage(ctx, imageName), nil
92+}
93+
87 func (l *localControlPlane) AddRoute(ctx context.Context, route domain.Route) error {94 func (l *localControlPlane) AddRoute(ctx context.Context, route domain.Route) error {
88 if l.configSvc == nil {95 if l.configSvc == nil {
89 return fmt.Errorf("local config service unavailable")96 return fmt.Errorf("local config service unavailable")
M internal/adapters/in/cli/controlplane_remote.go +4 -0
@@ -28,6 +28,10 @@ func (r *remoteControlPlane) GetRoute(ctx context.Context, routeDomain string) (
2828 return r.client.GetRoute(ctx, routeDomain)
2929 }
3030
31+func (r *remoteControlPlane) FindRoutesByImage(ctx context.Context, imageName string) ([]domain.Route, error) {
32+ return r.client.FindRoutesByImage(ctx, imageName)
33+}
34+
3135 func (r *remoteControlPlane) AddRoute(ctx context.Context, route domain.Route) error {
3236 return r.client.AddRoute(ctx, route)
3337 }
@@ -28,6 +28,10 @@ func (r *remoteControlPlane) GetRoute(ctx context.Context, routeDomain string) (
28 return r.client.GetRoute(ctx, routeDomain)28 return r.client.GetRoute(ctx, routeDomain)
29 }29 }
30 30
31+func (r *remoteControlPlane) FindRoutesByImage(ctx context.Context, imageName string) ([]domain.Route, error) {
32+ return r.client.FindRoutesByImage(ctx, imageName)
33+}
34+
31 func (r *remoteControlPlane) AddRoute(ctx context.Context, route domain.Route) error {35 func (r *remoteControlPlane) AddRoute(ctx context.Context, route domain.Route) error {
32 return r.client.AddRoute(ctx, route)36 return r.client.AddRoute(ctx, route)
33 }37 }
M internal/adapters/in/cli/remote/client.go +18 -0
@@ -322,6 +322,24 @@ func (c *Client) GetRoute(ctx context.Context, routeDomain string) (*domain.Rout
322322 return &route, nil
323323 }
324324
325+// FindRoutesByImage returns all routes associated with the given image name.
326+func (c *Client) FindRoutesByImage(ctx context.Context, imageName string) ([]domain.Route, error) {
327+ resp, err := c.request(ctx, http.MethodGet, "/routes/by-image/"+url.PathEscape(imageName), nil)
328+ if err != nil {
329+ return nil, err
330+ }
331+
332+ var result struct {
333+ Image string `json:"image"`
334+ Routes []domain.Route `json:"routes"`
335+ }
336+ if err := parseResponse(resp, &result); err != nil {
337+ return nil, err
338+ }
339+
340+ return result.Routes, nil
341+}
342+
325343 // AddRoute adds a new route.
326344 func (c *Client) AddRoute(ctx context.Context, route domain.Route) error {
327345 resp, err := c.request(ctx, http.MethodPost, "/routes", route)
@@ -322,6 +322,24 @@ func (c *Client) GetRoute(ctx context.Context, routeDomain string) (*domain.Rout
322 return &route, nil322 return &route, nil
323 }323 }
324 324
325+// FindRoutesByImage returns all routes associated with the given image name.
326+func (c *Client) FindRoutesByImage(ctx context.Context, imageName string) ([]domain.Route, error) {
327+ resp, err := c.request(ctx, http.MethodGet, "/routes/by-image/"+url.PathEscape(imageName), nil)
328+ if err != nil {
329+ return nil, err
330+ }
331+
332+ var result struct {
333+ Image string `json:"image"`
334+ Routes []domain.Route `json:"routes"`
335+ }
336+ if err := parseResponse(resp, &result); err != nil {
337+ return nil, err
338+ }
339+
340+ return result.Routes, nil
341+}
342+
325 // AddRoute adds a new route.343 // AddRoute adds a new route.
326 func (c *Client) AddRoute(ctx context.Context, route domain.Route) error {344 func (c *Client) AddRoute(ctx context.Context, route domain.Route) error {
327 resp, err := c.request(ctx, http.MethodPost, "/routes", route)345 resp, err := c.request(ctx, http.MethodPost, "/routes", route)
M internal/adapters/in/http/admin/handler.go +39 -0
@@ -210,6 +210,7 @@ func (h *Handler) matchRoute(path string) (routeHandler, bool) {
210210 }{
211211 {"/backups", h.handleBackups},
212212 {"/attachments", h.handleAttachmentsConfig},
213+ {"/routes/by-image", h.handleRoutesByImage},
213214 {"/routes", h.handleRoutes},
214215 {"/secrets", h.handleSecrets},
215216 {"/deploy", h.handleDeploy},
@@ -453,6 +454,44 @@ func (h *Handler) handleRoutesDelete(w http.ResponseWriter, r *http.Request, rou
453454 h.sendJSON(w, http.StatusOK, dto.RouteDeleteResponse{Status: "removed"})
454455 }
455456
457+// handleRoutesByImage handles GET /admin/routes/by-image/{image} endpoint.
458+// Returns all routes associated with the given image name.
459+func (h *Handler) handleRoutesByImage(w http.ResponseWriter, r *http.Request, path string) {
460+ if r.Method != http.MethodGet {
461+ h.sendError(w, http.StatusMethodNotAllowed, "method not allowed")
462+ return
463+ }
464+
465+ ctx := r.Context()
466+
467+ if !HasAccess(ctx, domain.AdminResourceRoutes, domain.AdminActionRead) {
468+ h.sendError(w, http.StatusForbidden, "insufficient permissions for routes:read")
469+ return
470+ }
471+
472+ imageName := strings.TrimPrefix(path, "/routes/by-image/")
473+ if imageName == "" || imageName == "/routes/by-image" {
474+ h.sendError(w, http.StatusBadRequest, "image name required in path")
475+ return
476+ }
477+
478+ routes := h.configSvc.FindRoutesByImage(ctx, imageName)
479+ if len(routes) == 0 {
480+ h.sendError(w, http.StatusNotFound, fmt.Sprintf("no routes found for image '%s'", imageName))
481+ return
482+ }
483+
484+ response := make([]routeResponse, 0, len(routes))
485+ for _, route := range routes {
486+ response = append(response, toRouteResponse(route))
487+ }
488+
489+ h.sendJSON(w, http.StatusOK, dto.RoutesByImageResponse{
490+ Image: imageName,
491+ Routes: response,
492+ })
493+}
494+
456495 func (h *Handler) handleNetworks(w http.ResponseWriter, r *http.Request) {
457496 if r.Method != http.MethodGet {
458497 h.sendError(w, http.StatusMethodNotAllowed, "method not allowed")
@@ -210,6 +210,7 @@ func (h *Handler) matchRoute(path string) (routeHandler, bool) {
210 }{210 }{
211 {"/backups", h.handleBackups},211 {"/backups", h.handleBackups},
212 {"/attachments", h.handleAttachmentsConfig},212 {"/attachments", h.handleAttachmentsConfig},
213+ {"/routes/by-image", h.handleRoutesByImage},
213 {"/routes", h.handleRoutes},214 {"/routes", h.handleRoutes},
214 {"/secrets", h.handleSecrets},215 {"/secrets", h.handleSecrets},
215 {"/deploy", h.handleDeploy},216 {"/deploy", h.handleDeploy},
@@ -453,6 +454,44 @@ func (h *Handler) handleRoutesDelete(w http.ResponseWriter, r *http.Request, rou
453 h.sendJSON(w, http.StatusOK, dto.RouteDeleteResponse{Status: "removed"})454 h.sendJSON(w, http.StatusOK, dto.RouteDeleteResponse{Status: "removed"})
454 }455 }
455 456
457+// handleRoutesByImage handles GET /admin/routes/by-image/{image} endpoint.
458+// Returns all routes associated with the given image name.
459+func (h *Handler) handleRoutesByImage(w http.ResponseWriter, r *http.Request, path string) {
460+ if r.Method != http.MethodGet {
461+ h.sendError(w, http.StatusMethodNotAllowed, "method not allowed")
462+ return
463+ }
464+
465+ ctx := r.Context()
466+
467+ if !HasAccess(ctx, domain.AdminResourceRoutes, domain.AdminActionRead) {
468+ h.sendError(w, http.StatusForbidden, "insufficient permissions for routes:read")
469+ return
470+ }
471+
472+ imageName := strings.TrimPrefix(path, "/routes/by-image/")
473+ if imageName == "" || imageName == "/routes/by-image" {
474+ h.sendError(w, http.StatusBadRequest, "image name required in path")
475+ return
476+ }
477+
478+ routes := h.configSvc.FindRoutesByImage(ctx, imageName)
479+ if len(routes) == 0 {
480+ h.sendError(w, http.StatusNotFound, fmt.Sprintf("no routes found for image '%s'", imageName))
481+ return
482+ }
483+
484+ response := make([]routeResponse, 0, len(routes))
485+ for _, route := range routes {
486+ response = append(response, toRouteResponse(route))
487+ }
488+
489+ h.sendJSON(w, http.StatusOK, dto.RoutesByImageResponse{
490+ Image: imageName,
491+ Routes: response,
492+ })
493+}
494+
456 func (h *Handler) handleNetworks(w http.ResponseWriter, r *http.Request) {495 func (h *Handler) handleNetworks(w http.ResponseWriter, r *http.Request) {
457 if r.Method != http.MethodGet {496 if r.Method != http.MethodGet {
458 h.sendError(w, http.StatusMethodNotAllowed, "method not allowed")497 h.sendError(w, http.StatusMethodNotAllowed, "method not allowed")
M internal/boundaries/in/config.go +4 -0
@@ -21,6 +21,10 @@ type ConfigService interface {
2121 // GetRoute returns a single route by domain.
2222 GetRoute(ctx context.Context, domain string) (*domain.Route, error)
2323
24+ // FindRoutesByImage returns all routes that match the given image name.
25+ // Image names are normalized by stripping the registry domain prefix before comparison.
26+ FindRoutesByImage(ctx context.Context, imageName string) []domain.Route
27+
2428 // AddRoute adds a new route to the configuration.
2529 AddRoute(ctx context.Context, route domain.Route) error
2630
@@ -21,6 +21,10 @@ type ConfigService interface {
21 // GetRoute returns a single route by domain.21 // GetRoute returns a single route by domain.
22 GetRoute(ctx context.Context, domain string) (*domain.Route, error)22 GetRoute(ctx context.Context, domain string) (*domain.Route, error)
23 23
24+ // FindRoutesByImage returns all routes that match the given image name.
25+ // Image names are normalized by stripping the registry domain prefix before comparison.
26+ FindRoutesByImage(ctx context.Context, imageName string) []domain.Route
27+
24 // AddRoute adds a new route to the configuration.28 // AddRoute adds a new route to the configuration.
25 AddRoute(ctx context.Context, route domain.Route) error29 AddRoute(ctx context.Context, route domain.Route) error
26 30
M internal/boundaries/in/mocks/mock_config_service.go +59 -0
@@ -158,6 +158,65 @@ func (_c *MockConfigService_AddRoute_Call) RunAndReturn(run func(ctx context.Con
158158 return _c
159159 }
160160
161+// FindRoutesByImage provides a mock function for the type MockConfigService
162+func (_mock *MockConfigService) FindRoutesByImage(ctx context.Context, imageName string) []domain.Route {
163+ ret := _mock.Called(ctx, imageName)
164+
165+ if len(ret) == 0 {
166+ panic("no return value specified for FindRoutesByImage")
167+ }
168+
169+ var r0 []domain.Route
170+ if returnFunc, ok := ret.Get(0).(func(context.Context, string) []domain.Route); ok {
171+ r0 = returnFunc(ctx, imageName)
172+ } else {
173+ if ret.Get(0) != nil {
174+ r0 = ret.Get(0).([]domain.Route)
175+ }
176+ }
177+ return r0
178+}
179+
180+// MockConfigService_FindRoutesByImage_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'FindRoutesByImage'
181+type MockConfigService_FindRoutesByImage_Call struct {
182+ *mock.Call
183+}
184+
185+// FindRoutesByImage is a helper method to define mock.On call
186+// - ctx context.Context
187+// - imageName string
188+func (_e *MockConfigService_Expecter) FindRoutesByImage(ctx interface{}, imageName interface{}) *MockConfigService_FindRoutesByImage_Call {
189+ return &MockConfigService_FindRoutesByImage_Call{Call: _e.mock.On("FindRoutesByImage", ctx, imageName)}
190+}
191+
192+func (_c *MockConfigService_FindRoutesByImage_Call) Run(run func(ctx context.Context, imageName string)) *MockConfigService_FindRoutesByImage_Call {
193+ _c.Call.Run(func(args mock.Arguments) {
194+ var arg0 context.Context
195+ if args[0] != nil {
196+ arg0 = args[0].(context.Context)
197+ }
198+ var arg1 string
199+ if args[1] != nil {
200+ arg1 = args[1].(string)
201+ }
202+ run(
203+ arg0,
204+ arg1,
205+ )
206+ })
207+ return _c
208+}
209+
210+func (_c *MockConfigService_FindRoutesByImage_Call) Return(routes []domain.Route) *MockConfigService_FindRoutesByImage_Call {
211+ _c.Call.Return(routes)
212+ return _c
213+}
214+
215+func (_c *MockConfigService_FindRoutesByImage_Call) RunAndReturn(run func(ctx context.Context, imageName string) []domain.Route) *MockConfigService_FindRoutesByImage_Call {
216+ _c.Call.Return(run)
217+ return _c
218+}
219+
161220 // GetAllAttachments provides a mock function for the type MockConfigService
162221 func (_mock *MockConfigService) GetAllAttachments(ctx context.Context) map[string][]string {
163222 ret := _mock.Called(ctx)
@@ -158,6 +158,65 @@ func (_c *MockConfigService_AddRoute_Call) RunAndReturn(run func(ctx context.Con
158 return _c158 return _c
159 }159 }
160 160
161+// FindRoutesByImage provides a mock function for the type MockConfigService
162+func (_mock *MockConfigService) FindRoutesByImage(ctx context.Context, imageName string) []domain.Route {
163+ ret := _mock.Called(ctx, imageName)
164+
165+ if len(ret) == 0 {
166+ panic("no return value specified for FindRoutesByImage")
167+ }
168+
169+ var r0 []domain.Route
170+ if returnFunc, ok := ret.Get(0).(func(context.Context, string) []domain.Route); ok {
171+ r0 = returnFunc(ctx, imageName)
172+ } else {
173+ if ret.Get(0) != nil {
174+ r0 = ret.Get(0).([]domain.Route)
175+ }
176+ }
177+ return r0
178+}
179+
180+// MockConfigService_FindRoutesByImage_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'FindRoutesByImage'
181+type MockConfigService_FindRoutesByImage_Call struct {
182+ *mock.Call
183+}
184+
185+// FindRoutesByImage is a helper method to define mock.On call
186+// - ctx context.Context
187+// - imageName string
188+func (_e *MockConfigService_Expecter) FindRoutesByImage(ctx interface{}, imageName interface{}) *MockConfigService_FindRoutesByImage_Call {
189+ return &MockConfigService_FindRoutesByImage_Call{Call: _e.mock.On("FindRoutesByImage", ctx, imageName)}
190+}
191+
192+func (_c *MockConfigService_FindRoutesByImage_Call) Run(run func(ctx context.Context, imageName string)) *MockConfigService_FindRoutesByImage_Call {
193+ _c.Call.Run(func(args mock.Arguments) {
194+ var arg0 context.Context
195+ if args[0] != nil {
196+ arg0 = args[0].(context.Context)
197+ }
198+ var arg1 string
199+ if args[1] != nil {
200+ arg1 = args[1].(string)
201+ }
202+ run(
203+ arg0,
204+ arg1,
205+ )
206+ })
207+ return _c
208+}
209+
210+func (_c *MockConfigService_FindRoutesByImage_Call) Return(routes []domain.Route) *MockConfigService_FindRoutesByImage_Call {
211+ _c.Call.Return(routes)
212+ return _c
213+}
214+
215+func (_c *MockConfigService_FindRoutesByImage_Call) RunAndReturn(run func(ctx context.Context, imageName string) []domain.Route) *MockConfigService_FindRoutesByImage_Call {
216+ _c.Call.Return(run)
217+ return _c
218+}
219+
161 // GetAllAttachments provides a mock function for the type MockConfigService220 // GetAllAttachments provides a mock function for the type MockConfigService
162 func (_mock *MockConfigService) GetAllAttachments(ctx context.Context) map[string][]string {221 func (_mock *MockConfigService) GetAllAttachments(ctx context.Context) map[string][]string {
163 ret := _mock.Called(ctx)222 ret := _mock.Called(ctx)
M internal/usecase/config/service.go +45 -0
@@ -226,6 +226,51 @@ func (s *Service) GetRoute(_ context.Context, domainName string) (*domain.Route,
226226 return route, nil
227227 }
228228
229+// FindRoutesByImage returns all routes whose image matches the given image name.
230+// Image names are normalized by stripping the registry domain prefix before comparison.
231+func (s *Service) FindRoutesByImage(_ context.Context, imageName string) []domain.Route {
232+ s.mu.RLock()
233+ defer s.mu.RUnlock()
234+
235+ registryDomain := s.config.RegistryDomain
236+ normalizedImage := NormalizeRegistryImage(imageName, registryDomain)
237+
238+ var routes []domain.Route
239+ for domainName, image := range s.config.Routes {
240+ normalizedRouteImage := NormalizeRegistryImage(image, registryDomain)
241+ if strings.EqualFold(normalizedRouteImage, normalizedImage) {
242+ route := domain.Route{
243+ Image: image,
244+ HTTPS: true,
245+ }
246+ if strings.HasPrefix(domainName, "http://") {
247+ route.Domain = strings.TrimPrefix(domainName, "http://")
248+ route.HTTPS = false
249+ } else {
250+ route.Domain = domainName
251+ }
252+ routes = append(routes, route)
253+ }
254+ }
255+
256+ return routes
257+}
258+
259+// NormalizeRegistryImage strips the registry domain prefix from an image name for comparison.
260+func NormalizeRegistryImage(imageName, registryDomain string) string {
261+ registryDomain = strings.TrimSuffix(registryDomain, "/")
262+ if registryDomain == "" {
263+ return imageName
264+ }
265+
266+ prefix := registryDomain + "/"
267+ if strings.HasPrefix(imageName, prefix) {
268+ return strings.TrimPrefix(imageName, prefix)
269+ }
270+
271+ return imageName
272+}
273+
229274 // AddRoute adds a new route to the configuration and persists it.
230275 func (s *Service) AddRoute(ctx context.Context, route domain.Route) error {
231276 ctx = zerowrap.CtxWithFields(ctx, map[string]any{
@@ -226,6 +226,51 @@ func (s *Service) GetRoute(_ context.Context, domainName string) (*domain.Route,
226 return route, nil226 return route, nil
227 }227 }
228 228
229+// FindRoutesByImage returns all routes whose image matches the given image name.
230+// Image names are normalized by stripping the registry domain prefix before comparison.
231+func (s *Service) FindRoutesByImage(_ context.Context, imageName string) []domain.Route {
232+ s.mu.RLock()
233+ defer s.mu.RUnlock()
234+
235+ registryDomain := s.config.RegistryDomain
236+ normalizedImage := NormalizeRegistryImage(imageName, registryDomain)
237+
238+ var routes []domain.Route
239+ for domainName, image := range s.config.Routes {
240+ normalizedRouteImage := NormalizeRegistryImage(image, registryDomain)
241+ if strings.EqualFold(normalizedRouteImage, normalizedImage) {
242+ route := domain.Route{
243+ Image: image,
244+ HTTPS: true,
245+ }
246+ if strings.HasPrefix(domainName, "http://") {
247+ route.Domain = strings.TrimPrefix(domainName, "http://")
248+ route.HTTPS = false
249+ } else {
250+ route.Domain = domainName
251+ }
252+ routes = append(routes, route)
253+ }
254+ }
255+
256+ return routes
257+}
258+
259+// NormalizeRegistryImage strips the registry domain prefix from an image name for comparison.
260+func NormalizeRegistryImage(imageName, registryDomain string) string {
261+ registryDomain = strings.TrimSuffix(registryDomain, "/")
262+ if registryDomain == "" {
263+ return imageName
264+ }
265+
266+ prefix := registryDomain + "/"
267+ if strings.HasPrefix(imageName, prefix) {
268+ return strings.TrimPrefix(imageName, prefix)
269+ }
270+
271+ return imageName
272+}
273+
229 // AddRoute adds a new route to the configuration and persists it.274 // AddRoute adds a new route to the configuration and persists it.
230 func (s *Service) AddRoute(ctx context.Context, route domain.Route) error {275 func (s *Service) AddRoute(ctx context.Context, route domain.Route) error {
231 ctx = zerowrap.CtxWithFields(ctx, map[string]any{276 ctx = zerowrap.CtxWithFields(ctx, map[string]any{

Keyboard shortcuts

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

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

Branches 8

Tags 106

↑↓ moveenter openesc close