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 { | |||
| 42 | 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 | 51 | // RouteDeleteResponse represents a route removal response. | |
| 46 | 52 | type RouteDeleteResponse struct { | |
| 47 | 53 | 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 { | |||
| 16 | 16 | ListRoutesWithDetails(ctx context.Context) ([]remote.RouteInfo, error) | |
| 17 | 17 | GetHealth(ctx context.Context) (map[string]*remote.RouteHealth, error) | |
| 18 | 18 | GetRoute(ctx context.Context, routeDomain string) (*domain.Route, error) | |
| 19 | + | FindRoutesByImage(ctx context.Context, imageName string) ([]domain.Route, error) | |
| 19 | 20 | AddRoute(ctx context.Context, route domain.Route) error | |
| 20 | 21 | UpdateRoute(ctx context.Context, route domain.Route) error | |
| 21 | 22 | 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) error | 20 | AddRoute(ctx context.Context, route domain.Route) error | ||
| 20 | UpdateRoute(ctx context.Context, route domain.Route) error | 21 | UpdateRoute(ctx context.Context, route domain.Route) error | ||
| 21 | RemoveRoute(ctx context.Context, routeDomain string) error | 22 | 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) (* | |||
| 84 | 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 | 94 | func (l *localControlPlane) AddRoute(ctx context.Context, route domain.Route) error { | |
| 88 | 95 | if l.configSvc == nil { | |
| 89 | 96 | 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) ( | |||
| 28 | 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 | 35 | func (r *remoteControlPlane) AddRoute(ctx context.Context, route domain.Route) error { | |
| 32 | 36 | return r.client.AddRoute(ctx, route) | |
| 33 | 37 | } | |
| @@ -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 | |||
| 322 | 322 | 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 | 343 | // AddRoute adds a new route. | |
| 326 | 344 | func (c *Client) AddRoute(ctx context.Context, route domain.Route) error { | |
| 327 | 345 | 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, nil | 322 | 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) { | |||
| 210 | 210 | }{ | |
| 211 | 211 | {"/backups", h.handleBackups}, | |
| 212 | 212 | {"/attachments", h.handleAttachmentsConfig}, | |
| 213 | + | {"/routes/by-image", h.handleRoutesByImage}, | |
| 213 | 214 | {"/routes", h.handleRoutes}, | |
| 214 | 215 | {"/secrets", h.handleSecrets}, | |
| 215 | 216 | {"/deploy", h.handleDeploy}, | |
| @@ -453,6 +454,44 @@ func (h *Handler) handleRoutesDelete(w http.ResponseWriter, r *http.Request, rou | |||
| 453 | 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 | 495 | func (h *Handler) handleNetworks(w http.ResponseWriter, r *http.Request) { | |
| 457 | 496 | if r.Method != http.MethodGet { | |
| 458 | 497 | 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 { | |||
| 21 | 21 | // GetRoute returns a single route by domain. | |
| 22 | 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 | 28 | // AddRoute adds a new route to the configuration. | |
| 25 | 29 | AddRoute(ctx context.Context, route domain.Route) error | |
| 26 | 30 | ||
| @@ -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) error | 29 | 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 | |||
| 158 | 158 | 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 | 220 | // GetAllAttachments provides a mock function for the type MockConfigService | |
| 162 | 221 | func (_mock *MockConfigService) GetAllAttachments(ctx context.Context) map[string][]string { | |
| 163 | 222 | ret := _mock.Called(ctx) | |
| @@ -158,6 +158,65 @@ func (_c *MockConfigService_AddRoute_Call) RunAndReturn(run func(ctx context.Con | |||||
| 158 | return _c | 158 | 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 MockConfigService | 220 | // 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, | |||
| 226 | 226 | 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 | 274 | // AddRoute adds a new route to the configuration and persists it. | |
| 230 | 275 | func (s *Service) AddRoute(ctx context.Context, route domain.Route) error { | |
| 231 | 276 | ctx = zerowrap.CtxWithFields(ctx, map[string]any{ | |
| @@ -226,6 +226,51 @@ func (s *Service) GetRoute(_ context.Context, domainName string) (*domain.Route, | |||||
| 226 | return route, nil | 226 | 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{ | ||