Merge pull request #29 from bnema/feat/routes-list-network-attachments
feat: expose route network and attachments in API
14 files · +915 -32
A internal/adapters/dto/route.go +20 -0
| @@ -0,0 +1,20 @@ | |||
| 1 | + | // Package dto provides shared data transfer objects for API responses. | |
| 2 | + | package dto | |
| 3 | + | ||
| 4 | + | // RouteInfo represents route details in API responses. | |
| 5 | + | type RouteInfo struct { | |
| 6 | + | Domain string `json:"domain"` | |
| 7 | + | Image string `json:"image"` | |
| 8 | + | ContainerID string `json:"container_id"` | |
| 9 | + | ContainerStatus string `json:"container_status"` | |
| 10 | + | Network string `json:"network"` | |
| 11 | + | Attachments []Attachment `json:"attachments"` | |
| 12 | + | } | |
| 13 | + | ||
| 14 | + | // Attachment represents an attached service in API responses. | |
| 15 | + | type Attachment struct { | |
| 16 | + | Name string `json:"name"` | |
| 17 | + | Image string `json:"image"` | |
| 18 | + | ContainerID string `json:"container_id"` | |
| 19 | + | Status string `json:"status"` | |
| 20 | + | } | |
| @@ -0,0 +1,20 @@ | |||||
| 1 | + | // Package dto provides shared data transfer objects for API responses. | |||
| 2 | + | package dto | |||
| 3 | + | ||||
| 4 | + | // RouteInfo represents route details in API responses. | |||
| 5 | + | type RouteInfo struct { | |||
| 6 | + | Domain string `json:"domain"` | |||
| 7 | + | Image string `json:"image"` | |||
| 8 | + | ContainerID string `json:"container_id"` | |||
| 9 | + | ContainerStatus string `json:"container_status"` | |||
| 10 | + | Network string `json:"network"` | |||
| 11 | + | Attachments []Attachment `json:"attachments"` | |||
| 12 | + | } | |||
| 13 | + | ||||
| 14 | + | // Attachment represents an attached service in API responses. | |||
| 15 | + | type Attachment struct { | |||
| 16 | + | Name string `json:"name"` | |||
| 17 | + | Image string `json:"image"` | |||
| 18 | + | ContainerID string `json:"container_id"` | |||
| 19 | + | Status string `json:"status"` | |||
| 20 | + | } | |||
M internal/adapters/in/cli/remote/client.go +60 -0
| @@ -12,6 +12,7 @@ import ( | |||
| 12 | 12 | "strings" | |
| 13 | 13 | "time" | |
| 14 | 14 | ||
| 15 | + | "gordon/internal/adapters/dto" | |
| 15 | 16 | "gordon/internal/domain" | |
| 16 | 17 | ) | |
| 17 | 18 | ||
| @@ -119,6 +120,10 @@ func parseResponse(resp *http.Response, target any) error { | |||
| 119 | 120 | ||
| 120 | 121 | // Routes API | |
| 121 | 122 | ||
| 123 | + | // Type aliases for API types using shared DTO package. | |
| 124 | + | type RouteInfo = dto.RouteInfo | |
| 125 | + | type Attachment = dto.Attachment | |
| 126 | + | ||
| 122 | 127 | // ListRoutes returns all configured routes. | |
| 123 | 128 | func (c *Client) ListRoutes(ctx context.Context) ([]domain.Route, error) { | |
| 124 | 129 | resp, err := c.request(ctx, http.MethodGet, "/routes", nil) | |
| @@ -136,6 +141,61 @@ func (c *Client) ListRoutes(ctx context.Context) ([]domain.Route, error) { | |||
| 136 | 141 | return result.Routes, nil | |
| 137 | 142 | } | |
| 138 | 143 | ||
| 144 | + | // ListRoutesWithDetails returns routes with network and attachment info. | |
| 145 | + | func (c *Client) ListRoutesWithDetails(ctx context.Context) ([]RouteInfo, error) { | |
| 146 | + | resp, err := c.request(ctx, http.MethodGet, "/routes?detailed=true", nil) | |
| 147 | + | if err != nil { | |
| 148 | + | return nil, err | |
| 149 | + | } | |
| 150 | + | ||
| 151 | + | var result struct { | |
| 152 | + | Routes []RouteInfo `json:"routes"` | |
| 153 | + | } | |
| 154 | + | if err := parseResponse(resp, &result); err != nil { | |
| 155 | + | return nil, err | |
| 156 | + | } | |
| 157 | + | ||
| 158 | + | return result.Routes, nil | |
| 159 | + | } | |
| 160 | + | ||
| 161 | + | // ListNetworks returns Gordon-managed networks. | |
| 162 | + | func (c *Client) ListNetworks(ctx context.Context) ([]*domain.NetworkInfo, error) { | |
| 163 | + | resp, err := c.request(ctx, http.MethodGet, "/networks", nil) | |
| 164 | + | if err != nil { | |
| 165 | + | return nil, err | |
| 166 | + | } | |
| 167 | + | ||
| 168 | + | var result struct { | |
| 169 | + | Networks []*domain.NetworkInfo `json:"networks"` | |
| 170 | + | } | |
| 171 | + | if err := parseResponse(resp, &result); err != nil { | |
| 172 | + | return nil, err | |
| 173 | + | } | |
| 174 | + | ||
| 175 | + | return result.Networks, nil | |
| 176 | + | } | |
| 177 | + | ||
| 178 | + | // ListAttachments returns attachments for a domain. | |
| 179 | + | func (c *Client) ListAttachments(ctx context.Context, routeDomain string) ([]Attachment, error) { | |
| 180 | + | if routeDomain == "" { | |
| 181 | + | return nil, fmt.Errorf("domain is required") | |
| 182 | + | } | |
| 183 | + | path := "/routes/" + url.PathEscape(routeDomain) + "/attachments" | |
| 184 | + | resp, err := c.request(ctx, http.MethodGet, path, nil) | |
| 185 | + | if err != nil { | |
| 186 | + | return nil, err | |
| 187 | + | } | |
| 188 | + | ||
| 189 | + | var result struct { | |
| 190 | + | Attachments []Attachment `json:"attachments"` | |
| 191 | + | } | |
| 192 | + | if err := parseResponse(resp, &result); err != nil { | |
| 193 | + | return nil, err | |
| 194 | + | } | |
| 195 | + | ||
| 196 | + | return result.Attachments, nil | |
| 197 | + | } | |
| 198 | + | ||
| 139 | 199 | // GetRoute returns a specific route by domain. | |
| 140 | 200 | func (c *Client) GetRoute(ctx context.Context, routeDomain string) (*domain.Route, error) { | |
| 141 | 201 | resp, err := c.request(ctx, http.MethodGet, "/routes/"+url.PathEscape(routeDomain), nil) | |
| @@ -12,6 +12,7 @@ import ( | |||||
| 12 | "strings" | 12 | "strings" | ||
| 13 | "time" | 13 | "time" | ||
| 14 | 14 | ||||
| 15 | + | "gordon/internal/adapters/dto" | |||
| 15 | "gordon/internal/domain" | 16 | "gordon/internal/domain" | ||
| 16 | ) | 17 | ) | ||
| 17 | 18 | ||||
| @@ -119,6 +120,10 @@ func parseResponse(resp *http.Response, target any) error { | |||||
| 119 | 120 | ||||
| 120 | // Routes API | 121 | // Routes API | ||
| 121 | 122 | ||||
| 123 | + | // Type aliases for API types using shared DTO package. | |||
| 124 | + | type RouteInfo = dto.RouteInfo | |||
| 125 | + | type Attachment = dto.Attachment | |||
| 126 | + | ||||
| 122 | // ListRoutes returns all configured routes. | 127 | // ListRoutes returns all configured routes. | ||
| 123 | func (c *Client) ListRoutes(ctx context.Context) ([]domain.Route, error) { | 128 | func (c *Client) ListRoutes(ctx context.Context) ([]domain.Route, error) { | ||
| 124 | resp, err := c.request(ctx, http.MethodGet, "/routes", nil) | 129 | resp, err := c.request(ctx, http.MethodGet, "/routes", nil) | ||
| @@ -136,6 +141,61 @@ func (c *Client) ListRoutes(ctx context.Context) ([]domain.Route, error) { | |||||
| 136 | return result.Routes, nil | 141 | return result.Routes, nil | ||
| 137 | } | 142 | } | ||
| 138 | 143 | ||||
| 144 | + | // ListRoutesWithDetails returns routes with network and attachment info. | |||
| 145 | + | func (c *Client) ListRoutesWithDetails(ctx context.Context) ([]RouteInfo, error) { | |||
| 146 | + | resp, err := c.request(ctx, http.MethodGet, "/routes?detailed=true", nil) | |||
| 147 | + | if err != nil { | |||
| 148 | + | return nil, err | |||
| 149 | + | } | |||
| 150 | + | ||||
| 151 | + | var result struct { | |||
| 152 | + | Routes []RouteInfo `json:"routes"` | |||
| 153 | + | } | |||
| 154 | + | if err := parseResponse(resp, &result); err != nil { | |||
| 155 | + | return nil, err | |||
| 156 | + | } | |||
| 157 | + | ||||
| 158 | + | return result.Routes, nil | |||
| 159 | + | } | |||
| 160 | + | ||||
| 161 | + | // ListNetworks returns Gordon-managed networks. | |||
| 162 | + | func (c *Client) ListNetworks(ctx context.Context) ([]*domain.NetworkInfo, error) { | |||
| 163 | + | resp, err := c.request(ctx, http.MethodGet, "/networks", nil) | |||
| 164 | + | if err != nil { | |||
| 165 | + | return nil, err | |||
| 166 | + | } | |||
| 167 | + | ||||
| 168 | + | var result struct { | |||
| 169 | + | Networks []*domain.NetworkInfo `json:"networks"` | |||
| 170 | + | } | |||
| 171 | + | if err := parseResponse(resp, &result); err != nil { | |||
| 172 | + | return nil, err | |||
| 173 | + | } | |||
| 174 | + | ||||
| 175 | + | return result.Networks, nil | |||
| 176 | + | } | |||
| 177 | + | ||||
| 178 | + | // ListAttachments returns attachments for a domain. | |||
| 179 | + | func (c *Client) ListAttachments(ctx context.Context, routeDomain string) ([]Attachment, error) { | |||
| 180 | + | if routeDomain == "" { | |||
| 181 | + | return nil, fmt.Errorf("domain is required") | |||
| 182 | + | } | |||
| 183 | + | path := "/routes/" + url.PathEscape(routeDomain) + "/attachments" | |||
| 184 | + | resp, err := c.request(ctx, http.MethodGet, path, nil) | |||
| 185 | + | if err != nil { | |||
| 186 | + | return nil, err | |||
| 187 | + | } | |||
| 188 | + | ||||
| 189 | + | var result struct { | |||
| 190 | + | Attachments []Attachment `json:"attachments"` | |||
| 191 | + | } | |||
| 192 | + | if err := parseResponse(resp, &result); err != nil { | |||
| 193 | + | return nil, err | |||
| 194 | + | } | |||
| 195 | + | ||||
| 196 | + | return result.Attachments, nil | |||
| 197 | + | } | |||
| 198 | + | ||||
| 139 | // GetRoute returns a specific route by domain. | 199 | // GetRoute returns a specific route by domain. | ||
| 140 | func (c *Client) GetRoute(ctx context.Context, routeDomain string) (*domain.Route, error) { | 200 | func (c *Client) GetRoute(ctx context.Context, routeDomain string) (*domain.Route, error) { | ||
| 141 | resp, err := c.request(ctx, http.MethodGet, "/routes/"+url.PathEscape(routeDomain), nil) | 201 | resp, err := c.request(ctx, http.MethodGet, "/routes/"+url.PathEscape(routeDomain), nil) | ||
M internal/adapters/in/cli/routes.go +50 -15
| @@ -29,25 +29,40 @@ func truncateImage(image string, maxLen int) string { | |||
| 29 | 29 | if len(digest) > 12 { | |
| 30 | 30 | digest = digest[:12] | |
| 31 | 31 | } | |
| 32 | - | short := fmt.Sprintf("%s@sha256:%s…", name, digest) | |
| 32 | + | short := fmt.Sprintf("%s@sha256:%s", name, digest) | |
| 33 | 33 | if len(short) > maxLen { | |
| 34 | - | // Truncate name if still too long | |
| 35 | - | available := maxLen - len("@sha256:") - 12 - 1 // 1 for ellipsis | |
| 36 | - | if available > 3 { | |
| 37 | - | name = name[:available-1] + "…" | |
| 34 | + | // Truncate from the end to maintain valid reference format | |
| 35 | + | if maxLen <= 3 { | |
| 36 | + | return short[:maxLen] | |
| 38 | 37 | } | |
| 39 | - | short = fmt.Sprintf("%s@sha256:%s…", name, digest) | |
| 38 | + | return short[:maxLen-3] + "..." | |
| 40 | 39 | } | |
| 41 | 40 | return short | |
| 42 | 41 | } | |
| 43 | 42 | ||
| 44 | 43 | // Regular tag: truncate if needed | |
| 45 | 44 | if len(image) > maxLen { | |
| 46 | - | return image[:maxLen-1] + "…" | |
| 45 | + | if maxLen <= 3 { | |
| 46 | + | return image[:maxLen] | |
| 47 | + | } | |
| 48 | + | return image[:maxLen-3] + "..." | |
| 47 | 49 | } | |
| 48 | 50 | return image | |
| 49 | 51 | } | |
| 50 | 52 | ||
| 53 | + | func truncateNetwork(network string, maxLen int) string { | |
| 54 | + | if network == "" || network == "-" { | |
| 55 | + | return "-" | |
| 56 | + | } | |
| 57 | + | if len(network) > maxLen { | |
| 58 | + | if maxLen <= 3 { | |
| 59 | + | return network[:maxLen] | |
| 60 | + | } | |
| 61 | + | return network[:maxLen-3] + "..." | |
| 62 | + | } | |
| 63 | + | return network | |
| 64 | + | } | |
| 65 | + | ||
| 51 | 66 | // newRoutesCmd creates the routes command group. | |
| 52 | 67 | func newRoutesCmd() *cobra.Command { | |
| 53 | 68 | cmd := &cobra.Command{ | |
| @@ -107,7 +122,7 @@ func newRoutesListCmd() *cobra.Command { | |||
| 107 | 122 | ||
| 108 | 123 | // runRoutesListRemote lists routes from a remote Gordon instance. | |
| 109 | 124 | func runRoutesListRemote(ctx context.Context, client *remote.Client) error { | |
| 110 | - | routes, err := client.ListRoutes(ctx) | |
| 125 | + | routes, err := client.ListRoutesWithDetails(ctx) | |
| 111 | 126 | if err != nil { | |
| 112 | 127 | return fmt.Errorf("failed to list routes: %w", err) | |
| 113 | 128 | } | |
| @@ -123,16 +138,20 @@ func runRoutesListRemote(ctx context.Context, client *remote.Client) error { | |||
| 123 | 138 | health = make(map[string]*remote.RouteHealth) | |
| 124 | 139 | } | |
| 125 | 140 | ||
| 126 | - | // Build table rows | |
| 127 | 141 | const imageColWidth = 35 | |
| 128 | - | rows := make([][]string, len(routes)) | |
| 129 | - | for i, route := range routes { | |
| 142 | + | const networkColWidth = 22 | |
| 143 | + | rows := make([][]string, 0, len(routes)) | |
| 144 | + | for _, route := range routes { | |
| 130 | 145 | routeHealth := health[route.Domain] | |
| 131 | 146 | ||
| 132 | 147 | // Container status column | |
| 133 | - | containerStatus := "unknown" | |
| 134 | - | if routeHealth != nil { | |
| 135 | - | containerStatus = routeHealth.ContainerStatus | |
| 148 | + | containerStatus := route.ContainerStatus | |
| 149 | + | if containerStatus == "" { | |
| 150 | + | if routeHealth != nil { | |
| 151 | + | containerStatus = routeHealth.ContainerStatus | |
| 152 | + | } else { | |
| 153 | + | containerStatus = "unknown" | |
| 154 | + | } | |
| 136 | 155 | } | |
| 137 | 156 | containerBadge := components.ContainerStatusBadge(containerStatus) | |
| 138 | 157 | ||
| @@ -140,7 +159,22 @@ func runRoutesListRemote(ctx context.Context, client *remote.Client) error { | |||
| 140 | 159 | httpStatus := formatHTTPStatus(routeHealth) | |
| 141 | 160 | ||
| 142 | 161 | displayImage := truncateImage(route.Image, imageColWidth) | |
| 143 | - | rows[i] = []string{route.Domain, displayImage, containerBadge, httpStatus} | |
| 162 | + | displayNetwork := truncateNetwork(route.Network, networkColWidth) | |
| 163 | + | rows = append(rows, []string{route.Domain, displayImage, displayNetwork, containerBadge, httpStatus}) | |
| 164 | + | ||
| 165 | + | for i, attachment := range route.Attachments { | |
| 166 | + | prefix := "|-" | |
| 167 | + | if i == len(route.Attachments)-1 { | |
| 168 | + | prefix = "`-" | |
| 169 | + | } | |
| 170 | + | attachmentName := prefix + " " + attachment.Name | |
| 171 | + | if len(attachmentName) > 25 { | |
| 172 | + | attachmentName = attachmentName[:22] + "..." | |
| 173 | + | } | |
| 174 | + | attachmentStatus := components.ContainerStatusBadge(attachment.Status) | |
| 175 | + | attachmentImage := truncateImage(attachment.Image, imageColWidth) | |
| 176 | + | rows = append(rows, []string{attachmentName, attachmentImage, "-", attachmentStatus, "-"}) | |
| 177 | + | } | |
| 144 | 178 | } | |
| 145 | 179 | ||
| 146 | 180 | // Render table | |
| @@ -148,6 +182,7 @@ func runRoutesListRemote(ctx context.Context, client *remote.Client) error { | |||
| 148 | 182 | components.WithColumns([]components.TableColumn{ | |
| 149 | 183 | {Title: "Domain", Width: 25}, | |
| 150 | 184 | {Title: "Image", Width: imageColWidth}, | |
| 185 | + | {Title: "Network", Width: networkColWidth}, | |
| 151 | 186 | {Title: "Container", Width: 12}, | |
| 152 | 187 | {Title: "HTTP", Width: 14}, | |
| 153 | 188 | }), | |
| @@ -29,25 +29,40 @@ func truncateImage(image string, maxLen int) string { | |||||
| 29 | if len(digest) > 12 { | 29 | if len(digest) > 12 { | ||
| 30 | digest = digest[:12] | 30 | digest = digest[:12] | ||
| 31 | } | 31 | } | ||
| 32 | - | short := fmt.Sprintf("%s@sha256:%s…", name, digest) | 32 | + | short := fmt.Sprintf("%s@sha256:%s", name, digest) |
| 33 | if len(short) > maxLen { | 33 | if len(short) > maxLen { | ||
| 34 | - | // Truncate name if still too long | 34 | + | // Truncate from the end to maintain valid reference format |
| 35 | - | available := maxLen - len("@sha256:") - 12 - 1 // 1 for ellipsis | 35 | + | if maxLen <= 3 { |
| 36 | - | if available > 3 { | 36 | + | return short[:maxLen] |
| 37 | - | name = name[:available-1] + "…" | |||
| 38 | } | 37 | } | ||
| 39 | - | short = fmt.Sprintf("%s@sha256:%s…", name, digest) | 38 | + | return short[:maxLen-3] + "..." |
| 40 | } | 39 | } | ||
| 41 | return short | 40 | return short | ||
| 42 | } | 41 | } | ||
| 43 | 42 | ||||
| 44 | // Regular tag: truncate if needed | 43 | // Regular tag: truncate if needed | ||
| 45 | if len(image) > maxLen { | 44 | if len(image) > maxLen { | ||
| 46 | - | return image[:maxLen-1] + "…" | 45 | + | if maxLen <= 3 { |
| 46 | + | return image[:maxLen] | |||
| 47 | + | } | |||
| 48 | + | return image[:maxLen-3] + "..." | |||
| 47 | } | 49 | } | ||
| 48 | return image | 50 | return image | ||
| 49 | } | 51 | } | ||
| 50 | 52 | ||||
| 53 | + | func truncateNetwork(network string, maxLen int) string { | |||
| 54 | + | if network == "" || network == "-" { | |||
| 55 | + | return "-" | |||
| 56 | + | } | |||
| 57 | + | if len(network) > maxLen { | |||
| 58 | + | if maxLen <= 3 { | |||
| 59 | + | return network[:maxLen] | |||
| 60 | + | } | |||
| 61 | + | return network[:maxLen-3] + "..." | |||
| 62 | + | } | |||
| 63 | + | return network | |||
| 64 | + | } | |||
| 65 | + | ||||
| 51 | // newRoutesCmd creates the routes command group. | 66 | // newRoutesCmd creates the routes command group. | ||
| 52 | func newRoutesCmd() *cobra.Command { | 67 | func newRoutesCmd() *cobra.Command { | ||
| 53 | cmd := &cobra.Command{ | 68 | cmd := &cobra.Command{ | ||
| @@ -107,7 +122,7 @@ func newRoutesListCmd() *cobra.Command { | |||||
| 107 | 122 | ||||
| 108 | // runRoutesListRemote lists routes from a remote Gordon instance. | 123 | // runRoutesListRemote lists routes from a remote Gordon instance. | ||
| 109 | func runRoutesListRemote(ctx context.Context, client *remote.Client) error { | 124 | func runRoutesListRemote(ctx context.Context, client *remote.Client) error { | ||
| 110 | - | routes, err := client.ListRoutes(ctx) | 125 | + | routes, err := client.ListRoutesWithDetails(ctx) |
| 111 | if err != nil { | 126 | if err != nil { | ||
| 112 | return fmt.Errorf("failed to list routes: %w", err) | 127 | return fmt.Errorf("failed to list routes: %w", err) | ||
| 113 | } | 128 | } | ||
| @@ -123,16 +138,20 @@ func runRoutesListRemote(ctx context.Context, client *remote.Client) error { | |||||
| 123 | health = make(map[string]*remote.RouteHealth) | 138 | health = make(map[string]*remote.RouteHealth) | ||
| 124 | } | 139 | } | ||
| 125 | 140 | ||||
| 126 | - | // Build table rows | |||
| 127 | const imageColWidth = 35 | 141 | const imageColWidth = 35 | ||
| 128 | - | rows := make([][]string, len(routes)) | 142 | + | const networkColWidth = 22 |
| 129 | - | for i, route := range routes { | 143 | + | rows := make([][]string, 0, len(routes)) |
| 144 | + | for _, route := range routes { | |||
| 130 | routeHealth := health[route.Domain] | 145 | routeHealth := health[route.Domain] | ||
| 131 | 146 | ||||
| 132 | // Container status column | 147 | // Container status column | ||
| 133 | - | containerStatus := "unknown" | 148 | + | containerStatus := route.ContainerStatus |
| 134 | - | if routeHealth != nil { | 149 | + | if containerStatus == "" { |
| 135 | - | containerStatus = routeHealth.ContainerStatus | 150 | + | if routeHealth != nil { |
| 151 | + | containerStatus = routeHealth.ContainerStatus | |||
| 152 | + | } else { | |||
| 153 | + | containerStatus = "unknown" | |||
| 154 | + | } | |||
| 136 | } | 155 | } | ||
| 137 | containerBadge := components.ContainerStatusBadge(containerStatus) | 156 | containerBadge := components.ContainerStatusBadge(containerStatus) | ||
| 138 | 157 | ||||
| @@ -140,7 +159,22 @@ func runRoutesListRemote(ctx context.Context, client *remote.Client) error { | |||||
| 140 | httpStatus := formatHTTPStatus(routeHealth) | 159 | httpStatus := formatHTTPStatus(routeHealth) | ||
| 141 | 160 | ||||
| 142 | displayImage := truncateImage(route.Image, imageColWidth) | 161 | displayImage := truncateImage(route.Image, imageColWidth) | ||
| 143 | - | rows[i] = []string{route.Domain, displayImage, containerBadge, httpStatus} | 162 | + | displayNetwork := truncateNetwork(route.Network, networkColWidth) |
| 163 | + | rows = append(rows, []string{route.Domain, displayImage, displayNetwork, containerBadge, httpStatus}) | |||
| 164 | + | ||||
| 165 | + | for i, attachment := range route.Attachments { | |||
| 166 | + | prefix := "|-" | |||
| 167 | + | if i == len(route.Attachments)-1 { | |||
| 168 | + | prefix = "`-" | |||
| 169 | + | } | |||
| 170 | + | attachmentName := prefix + " " + attachment.Name | |||
| 171 | + | if len(attachmentName) > 25 { | |||
| 172 | + | attachmentName = attachmentName[:22] + "..." | |||
| 173 | + | } | |||
| 174 | + | attachmentStatus := components.ContainerStatusBadge(attachment.Status) | |||
| 175 | + | attachmentImage := truncateImage(attachment.Image, imageColWidth) | |||
| 176 | + | rows = append(rows, []string{attachmentName, attachmentImage, "-", attachmentStatus, "-"}) | |||
| 177 | + | } | |||
| 144 | } | 178 | } | ||
| 145 | 179 | ||||
| 146 | // Render table | 180 | // Render table | ||
| @@ -148,6 +182,7 @@ func runRoutesListRemote(ctx context.Context, client *remote.Client) error { | |||||
| 148 | components.WithColumns([]components.TableColumn{ | 182 | components.WithColumns([]components.TableColumn{ | ||
| 149 | {Title: "Domain", Width: 25}, | 183 | {Title: "Domain", Width: 25}, | ||
| 150 | {Title: "Image", Width: imageColWidth}, | 184 | {Title: "Image", Width: imageColWidth}, | ||
| 185 | + | {Title: "Network", Width: networkColWidth}, | |||
| 151 | {Title: "Container", Width: 12}, | 186 | {Title: "Container", Width: 12}, | ||
| 152 | {Title: "HTTP", Width: 14}, | 187 | {Title: "HTTP", Width: 14}, | ||
| 153 | }), | 188 | }), | ||
M internal/adapters/in/http/admin/handler.go +88 -8
| @@ -11,6 +11,7 @@ import ( | |||
| 11 | 11 | ||
| 12 | 12 | "github.com/bnema/zerowrap" | |
| 13 | 13 | ||
| 14 | + | "gordon/internal/adapters/dto" | |
| 14 | 15 | "gordon/internal/boundaries/in" | |
| 15 | 16 | "gordon/internal/boundaries/out" | |
| 16 | 17 | "gordon/internal/domain" | |
| @@ -34,6 +35,36 @@ type Handler struct { | |||
| 34 | 35 | log zerowrap.Logger | |
| 35 | 36 | } | |
| 36 | 37 | ||
| 38 | + | // Type aliases for API responses using shared DTO types. | |
| 39 | + | type routeInfoResponse = dto.RouteInfo | |
| 40 | + | type attachmentResponse = dto.Attachment | |
| 41 | + | ||
| 42 | + | // toAttachmentResponse converts a domain.Attachment to a dto.Attachment. | |
| 43 | + | func toAttachmentResponse(a domain.Attachment) dto.Attachment { | |
| 44 | + | return dto.Attachment{ | |
| 45 | + | Name: a.Name, | |
| 46 | + | Image: a.Image, | |
| 47 | + | ContainerID: a.ContainerID, | |
| 48 | + | Status: a.Status, | |
| 49 | + | } | |
| 50 | + | } | |
| 51 | + | ||
| 52 | + | // toRouteInfoResponse converts a domain.RouteInfo to a dto.RouteInfo. | |
| 53 | + | func toRouteInfoResponse(r domain.RouteInfo) dto.RouteInfo { | |
| 54 | + | attachments := make([]dto.Attachment, 0, len(r.Attachments)) | |
| 55 | + | for _, a := range r.Attachments { | |
| 56 | + | attachments = append(attachments, toAttachmentResponse(a)) | |
| 57 | + | } | |
| 58 | + | return dto.RouteInfo{ | |
| 59 | + | Domain: r.Domain, | |
| 60 | + | Image: r.Image, | |
| 61 | + | ContainerID: r.ContainerID, | |
| 62 | + | ContainerStatus: r.ContainerStatus, | |
| 63 | + | Network: r.Network, | |
| 64 | + | Attachments: attachments, | |
| 65 | + | } | |
| 66 | + | } | |
| 67 | + | ||
| 37 | 68 | // NewHandler creates a new admin HTTP handler. | |
| 38 | 69 | func NewHandler( | |
| 39 | 70 | configSvc in.ConfigService, | |
| @@ -83,6 +114,8 @@ func (h *Handler) handleAdminRoutes(w http.ResponseWriter, r *http.Request) { | |||
| 83 | 114 | switch { | |
| 84 | 115 | case path == "/routes" || strings.HasPrefix(path, "/routes/"): | |
| 85 | 116 | h.handleRoutes(w, r, path) | |
| 117 | + | case path == "/networks": | |
| 118 | + | h.handleNetworks(w, r) | |
| 86 | 119 | case path == "/secrets" || strings.HasPrefix(path, "/secrets/"): | |
| 87 | 120 | h.handleSecrets(w, r, path) | |
| 88 | 121 | case path == "/deploy" || strings.HasPrefix(path, "/deploy/"): | |
| @@ -134,7 +167,7 @@ func (h *Handler) handleRoutes(w http.ResponseWriter, r *http.Request, path stri | |||
| 134 | 167 | case http.MethodDelete: | |
| 135 | 168 | h.handleRoutesDelete(w, r, routeDomain) | |
| 136 | 169 | default: | |
| 137 | - | http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) | |
| 170 | + | h.sendError(w, http.StatusMethodNotAllowed, "method not allowed") | |
| 138 | 171 | } | |
| 139 | 172 | } | |
| 140 | 173 | ||
| @@ -148,11 +181,36 @@ func (h *Handler) handleRoutesGet(w http.ResponseWriter, r *http.Request, routeD | |||
| 148 | 181 | } | |
| 149 | 182 | ||
| 150 | 183 | if routeDomain == "" { | |
| 184 | + | if r.URL.Query().Get("detailed") == "true" { | |
| 185 | + | routes := h.containerSvc.ListRoutesWithDetails(ctx) | |
| 186 | + | response := make([]routeInfoResponse, 0, len(routes)) | |
| 187 | + | for _, route := range routes { | |
| 188 | + | response = append(response, toRouteInfoResponse(route)) | |
| 189 | + | } | |
| 190 | + | h.sendJSON(w, http.StatusOK, map[string]any{"routes": response}) | |
| 191 | + | return | |
| 192 | + | } | |
| 193 | + | ||
| 151 | 194 | routes := h.configSvc.GetRoutes(ctx) | |
| 152 | 195 | h.sendJSON(w, http.StatusOK, map[string]any{"routes": routes}) | |
| 153 | 196 | return | |
| 154 | 197 | } | |
| 155 | 198 | ||
| 199 | + | if strings.HasSuffix(routeDomain, "/attachments") { | |
| 200 | + | parentDomain := strings.TrimSuffix(routeDomain, "/attachments") | |
| 201 | + | if parentDomain == "" { | |
| 202 | + | h.sendError(w, http.StatusBadRequest, "domain required in path") | |
| 203 | + | return | |
| 204 | + | } | |
| 205 | + | attachments := h.containerSvc.ListAttachments(ctx, parentDomain) | |
| 206 | + | response := make([]attachmentResponse, 0, len(attachments)) | |
| 207 | + | for _, attachment := range attachments { | |
| 208 | + | response = append(response, toAttachmentResponse(attachment)) | |
| 209 | + | } | |
| 210 | + | h.sendJSON(w, http.StatusOK, map[string]any{"attachments": response}) | |
| 211 | + | return | |
| 212 | + | } | |
| 213 | + | ||
| 156 | 214 | route, err := h.configSvc.GetRoute(ctx, routeDomain) | |
| 157 | 215 | if err != nil { | |
| 158 | 216 | h.sendError(w, http.StatusNotFound, "route not found") | |
| @@ -265,6 +323,28 @@ func (h *Handler) handleRoutesDelete(w http.ResponseWriter, r *http.Request, rou | |||
| 265 | 323 | h.sendJSON(w, http.StatusOK, map[string]string{"status": "removed"}) | |
| 266 | 324 | } | |
| 267 | 325 | ||
| 326 | + | func (h *Handler) handleNetworks(w http.ResponseWriter, r *http.Request) { | |
| 327 | + | if r.Method != http.MethodGet { | |
| 328 | + | h.sendError(w, http.StatusMethodNotAllowed, "method not allowed") | |
| 329 | + | return | |
| 330 | + | } | |
| 331 | + | ||
| 332 | + | ctx := r.Context() | |
| 333 | + | ||
| 334 | + | if !HasAccess(ctx, domain.AdminResourceStatus, domain.AdminActionRead) { | |
| 335 | + | h.sendError(w, http.StatusForbidden, "insufficient permissions for status:read") | |
| 336 | + | return | |
| 337 | + | } | |
| 338 | + | ||
| 339 | + | networks, err := h.containerSvc.ListNetworks(ctx) | |
| 340 | + | if err != nil { | |
| 341 | + | h.sendError(w, http.StatusInternalServerError, "failed to list networks") | |
| 342 | + | return | |
| 343 | + | } | |
| 344 | + | ||
| 345 | + | h.sendJSON(w, http.StatusOK, map[string]any{"networks": networks}) | |
| 346 | + | } | |
| 347 | + | ||
| 268 | 348 | // handleSecrets handles /admin/secrets endpoints. | |
| 269 | 349 | func (h *Handler) handleSecrets(w http.ResponseWriter, r *http.Request, path string) { | |
| 270 | 350 | ctx := r.Context() | |
| @@ -347,7 +427,7 @@ func (h *Handler) handleSecrets(w http.ResponseWriter, r *http.Request, path str | |||
| 347 | 427 | h.sendJSON(w, http.StatusOK, map[string]string{"status": "deleted"}) | |
| 348 | 428 | ||
| 349 | 429 | default: | |
| 350 | - | http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) | |
| 430 | + | h.sendError(w, http.StatusMethodNotAllowed, "method not allowed") | |
| 351 | 431 | } | |
| 352 | 432 | } | |
| 353 | 433 | ||
| @@ -355,7 +435,7 @@ func (h *Handler) handleSecrets(w http.ResponseWriter, r *http.Request, path str | |||
| 355 | 435 | // Returns detailed health status for all routes including HTTP probe results. | |
| 356 | 436 | func (h *Handler) handleHealth(w http.ResponseWriter, r *http.Request) { | |
| 357 | 437 | if r.Method != http.MethodGet { | |
| 358 | - | http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) | |
| 438 | + | h.sendError(w, http.StatusMethodNotAllowed, "method not allowed") | |
| 359 | 439 | return | |
| 360 | 440 | } | |
| 361 | 441 | ||
| @@ -387,7 +467,7 @@ func (h *Handler) handleHealth(w http.ResponseWriter, r *http.Request) { | |||
| 387 | 467 | // handleStatus handles /admin/status endpoint. | |
| 388 | 468 | func (h *Handler) handleStatus(w http.ResponseWriter, r *http.Request) { | |
| 389 | 469 | if r.Method != http.MethodGet { | |
| 390 | - | http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) | |
| 470 | + | h.sendError(w, http.StatusMethodNotAllowed, "method not allowed") | |
| 391 | 471 | return | |
| 392 | 472 | } | |
| 393 | 473 | ||
| @@ -429,7 +509,7 @@ func (h *Handler) handleStatus(w http.ResponseWriter, r *http.Request) { | |||
| 429 | 509 | // This reloads configuration from file into memory and triggers container sync. | |
| 430 | 510 | func (h *Handler) handleReload(w http.ResponseWriter, r *http.Request) { | |
| 431 | 511 | if r.Method != http.MethodPost { | |
| 432 | - | http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) | |
| 512 | + | h.sendError(w, http.StatusMethodNotAllowed, "method not allowed") | |
| 433 | 513 | return | |
| 434 | 514 | } | |
| 435 | 515 | ||
| @@ -464,7 +544,7 @@ func (h *Handler) handleReload(w http.ResponseWriter, r *http.Request) { | |||
| 464 | 544 | // handleConfig handles /admin/config endpoint. | |
| 465 | 545 | func (h *Handler) handleConfig(w http.ResponseWriter, r *http.Request) { | |
| 466 | 546 | if r.Method != http.MethodGet { | |
| 467 | - | http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) | |
| 547 | + | h.sendError(w, http.StatusMethodNotAllowed, "method not allowed") | |
| 468 | 548 | return | |
| 469 | 549 | } | |
| 470 | 550 | ||
| @@ -501,7 +581,7 @@ func (h *Handler) handleConfig(w http.ResponseWriter, r *http.Request) { | |||
| 501 | 581 | // POST triggers a deployment for the specified domain. | |
| 502 | 582 | func (h *Handler) handleDeploy(w http.ResponseWriter, r *http.Request, path string) { | |
| 503 | 583 | if r.Method != http.MethodPost { | |
| 504 | - | http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) | |
| 584 | + | h.sendError(w, http.StatusMethodNotAllowed, "method not allowed") | |
| 505 | 585 | return | |
| 506 | 586 | } | |
| 507 | 587 | ||
| @@ -549,7 +629,7 @@ func (h *Handler) handleDeploy(w http.ResponseWriter, r *http.Request, path stri | |||
| 549 | 629 | // GET /admin/logs/:domain - Container logs for a specific domain | |
| 550 | 630 | func (h *Handler) handleLogs(w http.ResponseWriter, r *http.Request, path string) { | |
| 551 | 631 | if r.Method != http.MethodGet { | |
| 552 | - | http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) | |
| 632 | + | h.sendError(w, http.StatusMethodNotAllowed, "method not allowed") | |
| 553 | 633 | return | |
| 554 | 634 | } | |
| 555 | 635 | ||
| @@ -11,6 +11,7 @@ import ( | |||||
| 11 | 11 | ||||
| 12 | "github.com/bnema/zerowrap" | 12 | "github.com/bnema/zerowrap" | ||
| 13 | 13 | ||||
| 14 | + | "gordon/internal/adapters/dto" | |||
| 14 | "gordon/internal/boundaries/in" | 15 | "gordon/internal/boundaries/in" | ||
| 15 | "gordon/internal/boundaries/out" | 16 | "gordon/internal/boundaries/out" | ||
| 16 | "gordon/internal/domain" | 17 | "gordon/internal/domain" | ||
| @@ -34,6 +35,36 @@ type Handler struct { | |||||
| 34 | log zerowrap.Logger | 35 | log zerowrap.Logger | ||
| 35 | } | 36 | } | ||
| 36 | 37 | ||||
| 38 | + | // Type aliases for API responses using shared DTO types. | |||
| 39 | + | type routeInfoResponse = dto.RouteInfo | |||
| 40 | + | type attachmentResponse = dto.Attachment | |||
| 41 | + | ||||
| 42 | + | // toAttachmentResponse converts a domain.Attachment to a dto.Attachment. | |||
| 43 | + | func toAttachmentResponse(a domain.Attachment) dto.Attachment { | |||
| 44 | + | return dto.Attachment{ | |||
| 45 | + | Name: a.Name, | |||
| 46 | + | Image: a.Image, | |||
| 47 | + | ContainerID: a.ContainerID, | |||
| 48 | + | Status: a.Status, | |||
| 49 | + | } | |||
| 50 | + | } | |||
| 51 | + | ||||
| 52 | + | // toRouteInfoResponse converts a domain.RouteInfo to a dto.RouteInfo. | |||
| 53 | + | func toRouteInfoResponse(r domain.RouteInfo) dto.RouteInfo { | |||
| 54 | + | attachments := make([]dto.Attachment, 0, len(r.Attachments)) | |||
| 55 | + | for _, a := range r.Attachments { | |||
| 56 | + | attachments = append(attachments, toAttachmentResponse(a)) | |||
| 57 | + | } | |||
| 58 | + | return dto.RouteInfo{ | |||
| 59 | + | Domain: r.Domain, | |||
| 60 | + | Image: r.Image, | |||
| 61 | + | ContainerID: r.ContainerID, | |||
| 62 | + | ContainerStatus: r.ContainerStatus, | |||
| 63 | + | Network: r.Network, | |||
| 64 | + | Attachments: attachments, | |||
| 65 | + | } | |||
| 66 | + | } | |||
| 67 | + | ||||
| 37 | // NewHandler creates a new admin HTTP handler. | 68 | // NewHandler creates a new admin HTTP handler. | ||
| 38 | func NewHandler( | 69 | func NewHandler( | ||
| 39 | configSvc in.ConfigService, | 70 | configSvc in.ConfigService, | ||
| @@ -83,6 +114,8 @@ func (h *Handler) handleAdminRoutes(w http.ResponseWriter, r *http.Request) { | |||||
| 83 | switch { | 114 | switch { | ||
| 84 | case path == "/routes" || strings.HasPrefix(path, "/routes/"): | 115 | case path == "/routes" || strings.HasPrefix(path, "/routes/"): | ||
| 85 | h.handleRoutes(w, r, path) | 116 | h.handleRoutes(w, r, path) | ||
| 117 | + | case path == "/networks": | |||
| 118 | + | h.handleNetworks(w, r) | |||
| 86 | case path == "/secrets" || strings.HasPrefix(path, "/secrets/"): | 119 | case path == "/secrets" || strings.HasPrefix(path, "/secrets/"): | ||
| 87 | h.handleSecrets(w, r, path) | 120 | h.handleSecrets(w, r, path) | ||
| 88 | case path == "/deploy" || strings.HasPrefix(path, "/deploy/"): | 121 | case path == "/deploy" || strings.HasPrefix(path, "/deploy/"): | ||
| @@ -134,7 +167,7 @@ func (h *Handler) handleRoutes(w http.ResponseWriter, r *http.Request, path stri | |||||
| 134 | case http.MethodDelete: | 167 | case http.MethodDelete: | ||
| 135 | h.handleRoutesDelete(w, r, routeDomain) | 168 | h.handleRoutesDelete(w, r, routeDomain) | ||
| 136 | default: | 169 | default: | ||
| 137 | - | http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) | 170 | + | h.sendError(w, http.StatusMethodNotAllowed, "method not allowed") |
| 138 | } | 171 | } | ||
| 139 | } | 172 | } | ||
| 140 | 173 | ||||
| @@ -148,11 +181,36 @@ func (h *Handler) handleRoutesGet(w http.ResponseWriter, r *http.Request, routeD | |||||
| 148 | } | 181 | } | ||
| 149 | 182 | ||||
| 150 | if routeDomain == "" { | 183 | if routeDomain == "" { | ||
| 184 | + | if r.URL.Query().Get("detailed") == "true" { | |||
| 185 | + | routes := h.containerSvc.ListRoutesWithDetails(ctx) | |||
| 186 | + | response := make([]routeInfoResponse, 0, len(routes)) | |||
| 187 | + | for _, route := range routes { | |||
| 188 | + | response = append(response, toRouteInfoResponse(route)) | |||
| 189 | + | } | |||
| 190 | + | h.sendJSON(w, http.StatusOK, map[string]any{"routes": response}) | |||
| 191 | + | return | |||
| 192 | + | } | |||
| 193 | + | ||||
| 151 | routes := h.configSvc.GetRoutes(ctx) | 194 | routes := h.configSvc.GetRoutes(ctx) | ||
| 152 | h.sendJSON(w, http.StatusOK, map[string]any{"routes": routes}) | 195 | h.sendJSON(w, http.StatusOK, map[string]any{"routes": routes}) | ||
| 153 | return | 196 | return | ||
| 154 | } | 197 | } | ||
| 155 | 198 | ||||
| 199 | + | if strings.HasSuffix(routeDomain, "/attachments") { | |||
| 200 | + | parentDomain := strings.TrimSuffix(routeDomain, "/attachments") | |||
| 201 | + | if parentDomain == "" { | |||
| 202 | + | h.sendError(w, http.StatusBadRequest, "domain required in path") | |||
| 203 | + | return | |||
| 204 | + | } | |||
| 205 | + | attachments := h.containerSvc.ListAttachments(ctx, parentDomain) | |||
| 206 | + | response := make([]attachmentResponse, 0, len(attachments)) | |||
| 207 | + | for _, attachment := range attachments { | |||
| 208 | + | response = append(response, toAttachmentResponse(attachment)) | |||
| 209 | + | } | |||
| 210 | + | h.sendJSON(w, http.StatusOK, map[string]any{"attachments": response}) | |||
| 211 | + | return | |||
| 212 | + | } | |||
| 213 | + | ||||
| 156 | route, err := h.configSvc.GetRoute(ctx, routeDomain) | 214 | route, err := h.configSvc.GetRoute(ctx, routeDomain) | ||
| 157 | if err != nil { | 215 | if err != nil { | ||
| 158 | h.sendError(w, http.StatusNotFound, "route not found") | 216 | h.sendError(w, http.StatusNotFound, "route not found") | ||
| @@ -265,6 +323,28 @@ func (h *Handler) handleRoutesDelete(w http.ResponseWriter, r *http.Request, rou | |||||
| 265 | h.sendJSON(w, http.StatusOK, map[string]string{"status": "removed"}) | 323 | h.sendJSON(w, http.StatusOK, map[string]string{"status": "removed"}) | ||
| 266 | } | 324 | } | ||
| 267 | 325 | ||||
| 326 | + | func (h *Handler) handleNetworks(w http.ResponseWriter, r *http.Request) { | |||
| 327 | + | if r.Method != http.MethodGet { | |||
| 328 | + | h.sendError(w, http.StatusMethodNotAllowed, "method not allowed") | |||
| 329 | + | return | |||
| 330 | + | } | |||
| 331 | + | ||||
| 332 | + | ctx := r.Context() | |||
| 333 | + | ||||
| 334 | + | if !HasAccess(ctx, domain.AdminResourceStatus, domain.AdminActionRead) { | |||
| 335 | + | h.sendError(w, http.StatusForbidden, "insufficient permissions for status:read") | |||
| 336 | + | return | |||
| 337 | + | } | |||
| 338 | + | ||||
| 339 | + | networks, err := h.containerSvc.ListNetworks(ctx) | |||
| 340 | + | if err != nil { | |||
| 341 | + | h.sendError(w, http.StatusInternalServerError, "failed to list networks") | |||
| 342 | + | return | |||
| 343 | + | } | |||
| 344 | + | ||||
| 345 | + | h.sendJSON(w, http.StatusOK, map[string]any{"networks": networks}) | |||
| 346 | + | } | |||
| 347 | + | ||||
| 268 | // handleSecrets handles /admin/secrets endpoints. | 348 | // handleSecrets handles /admin/secrets endpoints. | ||
| 269 | func (h *Handler) handleSecrets(w http.ResponseWriter, r *http.Request, path string) { | 349 | func (h *Handler) handleSecrets(w http.ResponseWriter, r *http.Request, path string) { | ||
| 270 | ctx := r.Context() | 350 | ctx := r.Context() | ||
| @@ -347,7 +427,7 @@ func (h *Handler) handleSecrets(w http.ResponseWriter, r *http.Request, path str | |||||
| 347 | h.sendJSON(w, http.StatusOK, map[string]string{"status": "deleted"}) | 427 | h.sendJSON(w, http.StatusOK, map[string]string{"status": "deleted"}) | ||
| 348 | 428 | ||||
| 349 | default: | 429 | default: | ||
| 350 | - | http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) | 430 | + | h.sendError(w, http.StatusMethodNotAllowed, "method not allowed") |
| 351 | } | 431 | } | ||
| 352 | } | 432 | } | ||
| 353 | 433 | ||||
| @@ -355,7 +435,7 @@ func (h *Handler) handleSecrets(w http.ResponseWriter, r *http.Request, path str | |||||
| 355 | // Returns detailed health status for all routes including HTTP probe results. | 435 | // Returns detailed health status for all routes including HTTP probe results. | ||
| 356 | func (h *Handler) handleHealth(w http.ResponseWriter, r *http.Request) { | 436 | func (h *Handler) handleHealth(w http.ResponseWriter, r *http.Request) { | ||
| 357 | if r.Method != http.MethodGet { | 437 | if r.Method != http.MethodGet { | ||
| 358 | - | http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) | 438 | + | h.sendError(w, http.StatusMethodNotAllowed, "method not allowed") |
| 359 | return | 439 | return | ||
| 360 | } | 440 | } | ||
| 361 | 441 | ||||
| @@ -387,7 +467,7 @@ func (h *Handler) handleHealth(w http.ResponseWriter, r *http.Request) { | |||||
| 387 | // handleStatus handles /admin/status endpoint. | 467 | // handleStatus handles /admin/status endpoint. | ||
| 388 | func (h *Handler) handleStatus(w http.ResponseWriter, r *http.Request) { | 468 | func (h *Handler) handleStatus(w http.ResponseWriter, r *http.Request) { | ||
| 389 | if r.Method != http.MethodGet { | 469 | if r.Method != http.MethodGet { | ||
| 390 | - | http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) | 470 | + | h.sendError(w, http.StatusMethodNotAllowed, "method not allowed") |
| 391 | return | 471 | return | ||
| 392 | } | 472 | } | ||
| 393 | 473 | ||||
| @@ -429,7 +509,7 @@ func (h *Handler) handleStatus(w http.ResponseWriter, r *http.Request) { | |||||
| 429 | // This reloads configuration from file into memory and triggers container sync. | 509 | // This reloads configuration from file into memory and triggers container sync. | ||
| 430 | func (h *Handler) handleReload(w http.ResponseWriter, r *http.Request) { | 510 | func (h *Handler) handleReload(w http.ResponseWriter, r *http.Request) { | ||
| 431 | if r.Method != http.MethodPost { | 511 | if r.Method != http.MethodPost { | ||
| 432 | - | http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) | 512 | + | h.sendError(w, http.StatusMethodNotAllowed, "method not allowed") |
| 433 | return | 513 | return | ||
| 434 | } | 514 | } | ||
| 435 | 515 | ||||
| @@ -464,7 +544,7 @@ func (h *Handler) handleReload(w http.ResponseWriter, r *http.Request) { | |||||
| 464 | // handleConfig handles /admin/config endpoint. | 544 | // handleConfig handles /admin/config endpoint. | ||
| 465 | func (h *Handler) handleConfig(w http.ResponseWriter, r *http.Request) { | 545 | func (h *Handler) handleConfig(w http.ResponseWriter, r *http.Request) { | ||
| 466 | if r.Method != http.MethodGet { | 546 | if r.Method != http.MethodGet { | ||
| 467 | - | http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) | 547 | + | h.sendError(w, http.StatusMethodNotAllowed, "method not allowed") |
| 468 | return | 548 | return | ||
| 469 | } | 549 | } | ||
| 470 | 550 | ||||
| @@ -501,7 +581,7 @@ func (h *Handler) handleConfig(w http.ResponseWriter, r *http.Request) { | |||||
| 501 | // POST triggers a deployment for the specified domain. | 581 | // POST triggers a deployment for the specified domain. | ||
| 502 | func (h *Handler) handleDeploy(w http.ResponseWriter, r *http.Request, path string) { | 582 | func (h *Handler) handleDeploy(w http.ResponseWriter, r *http.Request, path string) { | ||
| 503 | if r.Method != http.MethodPost { | 583 | if r.Method != http.MethodPost { | ||
| 504 | - | http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) | 584 | + | h.sendError(w, http.StatusMethodNotAllowed, "method not allowed") |
| 505 | return | 585 | return | ||
| 506 | } | 586 | } | ||
| 507 | 587 | ||||
| @@ -549,7 +629,7 @@ func (h *Handler) handleDeploy(w http.ResponseWriter, r *http.Request, path stri | |||||
| 549 | // GET /admin/logs/:domain - Container logs for a specific domain | 629 | // GET /admin/logs/:domain - Container logs for a specific domain | ||
| 550 | func (h *Handler) handleLogs(w http.ResponseWriter, r *http.Request, path string) { | 630 | func (h *Handler) handleLogs(w http.ResponseWriter, r *http.Request, path string) { | ||
| 551 | if r.Method != http.MethodGet { | 631 | if r.Method != http.MethodGet { | ||
| 552 | - | http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) | 632 | + | h.sendError(w, http.StatusMethodNotAllowed, "method not allowed") |
| 553 | return | 633 | return | ||
| 554 | } | 634 | } | ||
| 555 | 635 | ||||
A internal/adapters/out/docker/network_test.go +118 -0
| @@ -0,0 +1,118 @@ | |||
| 1 | + | package docker | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "context" | |
| 5 | + | "net/http" | |
| 6 | + | "net/http/httptest" | |
| 7 | + | "strings" | |
| 8 | + | "testing" | |
| 9 | + | ||
| 10 | + | "github.com/docker/docker/client" | |
| 11 | + | "github.com/stretchr/testify/assert" | |
| 12 | + | ) | |
| 13 | + | ||
| 14 | + | func TestRuntime_GetContainerNetwork(t *testing.T) { | |
| 15 | + | server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | |
| 16 | + | assert.Equal(t, http.MethodGet, r.Method) | |
| 17 | + | assert.Equal(t, "/v1.41/containers/abc123/json", r.URL.Path) | |
| 18 | + | w.Header().Set("Content-Type", "application/json") | |
| 19 | + | _, _ = w.Write([]byte(`{ | |
| 20 | + | "NetworkSettings": { | |
| 21 | + | "Networks": { | |
| 22 | + | "bridge": {"IPAddress": "172.17.0.2"}, | |
| 23 | + | "gordon-app": {"IPAddress": "172.18.0.2"} | |
| 24 | + | } | |
| 25 | + | } | |
| 26 | + | }`)) | |
| 27 | + | })) | |
| 28 | + | defer server.Close() | |
| 29 | + | ||
| 30 | + | host := strings.TrimPrefix(server.URL, "http://") | |
| 31 | + | cli, err := client.NewClientWithOpts(client.WithHost("tcp://"+host), client.WithVersion("1.41"), client.WithHTTPClient(server.Client())) | |
| 32 | + | if err != nil { | |
| 33 | + | t.Fatalf("failed to create docker client: %v", err) | |
| 34 | + | } | |
| 35 | + | ||
| 36 | + | runtime := NewRuntimeWithClient(cli) | |
| 37 | + | network, err := runtime.GetContainerNetwork(context.Background(), "abc123") | |
| 38 | + | ||
| 39 | + | assert.NoError(t, err) | |
| 40 | + | assert.Equal(t, "gordon-app", network) | |
| 41 | + | } | |
| 42 | + | ||
| 43 | + | func TestRuntime_GetContainerNetwork_Fallback(t *testing.T) { | |
| 44 | + | server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | |
| 45 | + | assert.Equal(t, http.MethodGet, r.Method) | |
| 46 | + | assert.Equal(t, "/v1.41/containers/abc123/json", r.URL.Path) | |
| 47 | + | w.Header().Set("Content-Type", "application/json") | |
| 48 | + | _, _ = w.Write([]byte(`{ | |
| 49 | + | "NetworkSettings": { | |
| 50 | + | "Networks": { | |
| 51 | + | "bridge": {"IPAddress": "172.17.0.2"}, | |
| 52 | + | "custom": {"IPAddress": "172.18.0.2"} | |
| 53 | + | } | |
| 54 | + | } | |
| 55 | + | }`)) | |
| 56 | + | })) | |
| 57 | + | defer server.Close() | |
| 58 | + | ||
| 59 | + | host := strings.TrimPrefix(server.URL, "http://") | |
| 60 | + | cli, err := client.NewClientWithOpts(client.WithHost("tcp://"+host), client.WithVersion("1.41"), client.WithHTTPClient(server.Client())) | |
| 61 | + | if err != nil { | |
| 62 | + | t.Fatalf("failed to create docker client: %v", err) | |
| 63 | + | } | |
| 64 | + | ||
| 65 | + | runtime := NewRuntimeWithClient(cli) | |
| 66 | + | network, err := runtime.GetContainerNetwork(context.Background(), "abc123") | |
| 67 | + | ||
| 68 | + | assert.NoError(t, err) | |
| 69 | + | assert.Equal(t, "bridge", network) | |
| 70 | + | } | |
| 71 | + | ||
| 72 | + | func TestRuntime_GetContainerNetwork_EmptyNetworks(t *testing.T) { | |
| 73 | + | server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | |
| 74 | + | assert.Equal(t, http.MethodGet, r.Method) | |
| 75 | + | assert.Equal(t, "/v1.41/containers/abc123/json", r.URL.Path) | |
| 76 | + | w.Header().Set("Content-Type", "application/json") | |
| 77 | + | _, _ = w.Write([]byte(`{ | |
| 78 | + | "NetworkSettings": { | |
| 79 | + | "Networks": {} | |
| 80 | + | } | |
| 81 | + | }`)) | |
| 82 | + | })) | |
| 83 | + | defer server.Close() | |
| 84 | + | ||
| 85 | + | host := strings.TrimPrefix(server.URL, "http://") | |
| 86 | + | cli, err := client.NewClientWithOpts(client.WithHost("tcp://"+host), client.WithVersion("1.41"), client.WithHTTPClient(server.Client())) | |
| 87 | + | if err != nil { | |
| 88 | + | t.Fatalf("failed to create docker client: %v", err) | |
| 89 | + | } | |
| 90 | + | ||
| 91 | + | runtime := NewRuntimeWithClient(cli) | |
| 92 | + | network, err := runtime.GetContainerNetwork(context.Background(), "abc123") | |
| 93 | + | ||
| 94 | + | assert.NoError(t, err) | |
| 95 | + | assert.Equal(t, "bridge", network) | |
| 96 | + | } | |
| 97 | + | ||
| 98 | + | func TestRuntime_GetContainerNetwork_NilNetworkSettings(t *testing.T) { | |
| 99 | + | server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | |
| 100 | + | assert.Equal(t, http.MethodGet, r.Method) | |
| 101 | + | assert.Equal(t, "/v1.41/containers/abc123/json", r.URL.Path) | |
| 102 | + | w.Header().Set("Content-Type", "application/json") | |
| 103 | + | _, _ = w.Write([]byte(`{}`)) | |
| 104 | + | })) | |
| 105 | + | defer server.Close() | |
| 106 | + | ||
| 107 | + | host := strings.TrimPrefix(server.URL, "http://") | |
| 108 | + | cli, err := client.NewClientWithOpts(client.WithHost("tcp://"+host), client.WithVersion("1.41"), client.WithHTTPClient(server.Client())) | |
| 109 | + | if err != nil { | |
| 110 | + | t.Fatalf("failed to create docker client: %v", err) | |
| 111 | + | } | |
| 112 | + | ||
| 113 | + | runtime := NewRuntimeWithClient(cli) | |
| 114 | + | network, err := runtime.GetContainerNetwork(context.Background(), "abc123") | |
| 115 | + | ||
| 116 | + | assert.NoError(t, err) | |
| 117 | + | assert.Equal(t, "bridge", network) | |
| 118 | + | } | |
| @@ -0,0 +1,118 @@ | |||||
| 1 | + | package docker | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "context" | |||
| 5 | + | "net/http" | |||
| 6 | + | "net/http/httptest" | |||
| 7 | + | "strings" | |||
| 8 | + | "testing" | |||
| 9 | + | ||||
| 10 | + | "github.com/docker/docker/client" | |||
| 11 | + | "github.com/stretchr/testify/assert" | |||
| 12 | + | ) | |||
| 13 | + | ||||
| 14 | + | func TestRuntime_GetContainerNetwork(t *testing.T) { | |||
| 15 | + | server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | |||
| 16 | + | assert.Equal(t, http.MethodGet, r.Method) | |||
| 17 | + | assert.Equal(t, "/v1.41/containers/abc123/json", r.URL.Path) | |||
| 18 | + | w.Header().Set("Content-Type", "application/json") | |||
| 19 | + | _, _ = w.Write([]byte(`{ | |||
| 20 | + | "NetworkSettings": { | |||
| 21 | + | "Networks": { | |||
| 22 | + | "bridge": {"IPAddress": "172.17.0.2"}, | |||
| 23 | + | "gordon-app": {"IPAddress": "172.18.0.2"} | |||
| 24 | + | } | |||
| 25 | + | } | |||
| 26 | + | }`)) | |||
| 27 | + | })) | |||
| 28 | + | defer server.Close() | |||
| 29 | + | ||||
| 30 | + | host := strings.TrimPrefix(server.URL, "http://") | |||
| 31 | + | cli, err := client.NewClientWithOpts(client.WithHost("tcp://"+host), client.WithVersion("1.41"), client.WithHTTPClient(server.Client())) | |||
| 32 | + | if err != nil { | |||
| 33 | + | t.Fatalf("failed to create docker client: %v", err) | |||
| 34 | + | } | |||
| 35 | + | ||||
| 36 | + | runtime := NewRuntimeWithClient(cli) | |||
| 37 | + | network, err := runtime.GetContainerNetwork(context.Background(), "abc123") | |||
| 38 | + | ||||
| 39 | + | assert.NoError(t, err) | |||
| 40 | + | assert.Equal(t, "gordon-app", network) | |||
| 41 | + | } | |||
| 42 | + | ||||
| 43 | + | func TestRuntime_GetContainerNetwork_Fallback(t *testing.T) { | |||
| 44 | + | server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | |||
| 45 | + | assert.Equal(t, http.MethodGet, r.Method) | |||
| 46 | + | assert.Equal(t, "/v1.41/containers/abc123/json", r.URL.Path) | |||
| 47 | + | w.Header().Set("Content-Type", "application/json") | |||
| 48 | + | _, _ = w.Write([]byte(`{ | |||
| 49 | + | "NetworkSettings": { | |||
| 50 | + | "Networks": { | |||
| 51 | + | "bridge": {"IPAddress": "172.17.0.2"}, | |||
| 52 | + | "custom": {"IPAddress": "172.18.0.2"} | |||
| 53 | + | } | |||
| 54 | + | } | |||
| 55 | + | }`)) | |||
| 56 | + | })) | |||
| 57 | + | defer server.Close() | |||
| 58 | + | ||||
| 59 | + | host := strings.TrimPrefix(server.URL, "http://") | |||
| 60 | + | cli, err := client.NewClientWithOpts(client.WithHost("tcp://"+host), client.WithVersion("1.41"), client.WithHTTPClient(server.Client())) | |||
| 61 | + | if err != nil { | |||
| 62 | + | t.Fatalf("failed to create docker client: %v", err) | |||
| 63 | + | } | |||
| 64 | + | ||||
| 65 | + | runtime := NewRuntimeWithClient(cli) | |||
| 66 | + | network, err := runtime.GetContainerNetwork(context.Background(), "abc123") | |||
| 67 | + | ||||
| 68 | + | assert.NoError(t, err) | |||
| 69 | + | assert.Equal(t, "bridge", network) | |||
| 70 | + | } | |||
| 71 | + | ||||
| 72 | + | func TestRuntime_GetContainerNetwork_EmptyNetworks(t *testing.T) { | |||
| 73 | + | server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | |||
| 74 | + | assert.Equal(t, http.MethodGet, r.Method) | |||
| 75 | + | assert.Equal(t, "/v1.41/containers/abc123/json", r.URL.Path) | |||
| 76 | + | w.Header().Set("Content-Type", "application/json") | |||
| 77 | + | _, _ = w.Write([]byte(`{ | |||
| 78 | + | "NetworkSettings": { | |||
| 79 | + | "Networks": {} | |||
| 80 | + | } | |||
| 81 | + | }`)) | |||
| 82 | + | })) | |||
| 83 | + | defer server.Close() | |||
| 84 | + | ||||
| 85 | + | host := strings.TrimPrefix(server.URL, "http://") | |||
| 86 | + | cli, err := client.NewClientWithOpts(client.WithHost("tcp://"+host), client.WithVersion("1.41"), client.WithHTTPClient(server.Client())) | |||
| 87 | + | if err != nil { | |||
| 88 | + | t.Fatalf("failed to create docker client: %v", err) | |||
| 89 | + | } | |||
| 90 | + | ||||
| 91 | + | runtime := NewRuntimeWithClient(cli) | |||
| 92 | + | network, err := runtime.GetContainerNetwork(context.Background(), "abc123") | |||
| 93 | + | ||||
| 94 | + | assert.NoError(t, err) | |||
| 95 | + | assert.Equal(t, "bridge", network) | |||
| 96 | + | } | |||
| 97 | + | ||||
| 98 | + | func TestRuntime_GetContainerNetwork_NilNetworkSettings(t *testing.T) { | |||
| 99 | + | server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | |||
| 100 | + | assert.Equal(t, http.MethodGet, r.Method) | |||
| 101 | + | assert.Equal(t, "/v1.41/containers/abc123/json", r.URL.Path) | |||
| 102 | + | w.Header().Set("Content-Type", "application/json") | |||
| 103 | + | _, _ = w.Write([]byte(`{}`)) | |||
| 104 | + | })) | |||
| 105 | + | defer server.Close() | |||
| 106 | + | ||||
| 107 | + | host := strings.TrimPrefix(server.URL, "http://") | |||
| 108 | + | cli, err := client.NewClientWithOpts(client.WithHost("tcp://"+host), client.WithVersion("1.41"), client.WithHTTPClient(server.Client())) | |||
| 109 | + | if err != nil { | |||
| 110 | + | t.Fatalf("failed to create docker client: %v", err) | |||
| 111 | + | } | |||
| 112 | + | ||||
| 113 | + | runtime := NewRuntimeWithClient(cli) | |||
| 114 | + | network, err := runtime.GetContainerNetwork(context.Background(), "abc123") | |||
| 115 | + | ||||
| 116 | + | assert.NoError(t, err) | |||
| 117 | + | assert.Equal(t, "bridge", network) | |||
| 118 | + | } | |||
M internal/adapters/out/docker/runtime.go +39 -0
| @@ -746,6 +746,45 @@ func (r *Runtime) GetContainerNetworkInfo(ctx context.Context, containerID strin | |||
| 746 | 746 | return containerIP, selectedPort, nil | |
| 747 | 747 | } | |
| 748 | 748 | ||
| 749 | + | // GetContainerNetwork returns the network name a container is connected to. | |
| 750 | + | func (r *Runtime) GetContainerNetwork(ctx context.Context, containerID string) (string, error) { | |
| 751 | + | ctx = zerowrap.CtxWithFields(ctx, map[string]any{ | |
| 752 | + | zerowrap.FieldLayer: "adapter", | |
| 753 | + | zerowrap.FieldAdapter: "docker", | |
| 754 | + | zerowrap.FieldAction: "GetContainerNetwork", | |
| 755 | + | zerowrap.FieldEntityID: containerID, | |
| 756 | + | }) | |
| 757 | + | log := zerowrap.FromCtx(ctx) | |
| 758 | + | ||
| 759 | + | resp, err := r.client.ContainerInspect(ctx, containerID) | |
| 760 | + | if err != nil { | |
| 761 | + | return "", log.WrapErr(err, "failed to inspect container") | |
| 762 | + | } | |
| 763 | + | ||
| 764 | + | networks := resp.NetworkSettings | |
| 765 | + | if networks == nil || networks.Networks == nil || len(networks.Networks) == 0 { | |
| 766 | + | return "bridge", nil | |
| 767 | + | } | |
| 768 | + | ||
| 769 | + | selected := "" | |
| 770 | + | for name := range networks.Networks { | |
| 771 | + | if strings.HasPrefix(name, "gordon-") { | |
| 772 | + | selected = name | |
| 773 | + | break | |
| 774 | + | } | |
| 775 | + | if selected == "" { | |
| 776 | + | selected = name | |
| 777 | + | } | |
| 778 | + | } | |
| 779 | + | ||
| 780 | + | if selected == "" { | |
| 781 | + | return "bridge", nil | |
| 782 | + | } | |
| 783 | + | ||
| 784 | + | log.Debug().Str("network", selected).Msg("container network detected") | |
| 785 | + | return selected, nil | |
| 786 | + | } | |
| 787 | + | ||
| 749 | 788 | // InspectImageVolumes gets the volume mount points declared in the image. | |
| 750 | 789 | func (r *Runtime) InspectImageVolumes(ctx context.Context, imageRef string) ([]string, error) { | |
| 751 | 790 | ctx = zerowrap.CtxWithFields(ctx, map[string]any{ | |
| @@ -746,6 +746,45 @@ func (r *Runtime) GetContainerNetworkInfo(ctx context.Context, containerID strin | |||||
| 746 | return containerIP, selectedPort, nil | 746 | return containerIP, selectedPort, nil | ||
| 747 | } | 747 | } | ||
| 748 | 748 | ||||
| 749 | + | // GetContainerNetwork returns the network name a container is connected to. | |||
| 750 | + | func (r *Runtime) GetContainerNetwork(ctx context.Context, containerID string) (string, error) { | |||
| 751 | + | ctx = zerowrap.CtxWithFields(ctx, map[string]any{ | |||
| 752 | + | zerowrap.FieldLayer: "adapter", | |||
| 753 | + | zerowrap.FieldAdapter: "docker", | |||
| 754 | + | zerowrap.FieldAction: "GetContainerNetwork", | |||
| 755 | + | zerowrap.FieldEntityID: containerID, | |||
| 756 | + | }) | |||
| 757 | + | log := zerowrap.FromCtx(ctx) | |||
| 758 | + | ||||
| 759 | + | resp, err := r.client.ContainerInspect(ctx, containerID) | |||
| 760 | + | if err != nil { | |||
| 761 | + | return "", log.WrapErr(err, "failed to inspect container") | |||
| 762 | + | } | |||
| 763 | + | ||||
| 764 | + | networks := resp.NetworkSettings | |||
| 765 | + | if networks == nil || networks.Networks == nil || len(networks.Networks) == 0 { | |||
| 766 | + | return "bridge", nil | |||
| 767 | + | } | |||
| 768 | + | ||||
| 769 | + | selected := "" | |||
| 770 | + | for name := range networks.Networks { | |||
| 771 | + | if strings.HasPrefix(name, "gordon-") { | |||
| 772 | + | selected = name | |||
| 773 | + | break | |||
| 774 | + | } | |||
| 775 | + | if selected == "" { | |||
| 776 | + | selected = name | |||
| 777 | + | } | |||
| 778 | + | } | |||
| 779 | + | ||||
| 780 | + | if selected == "" { | |||
| 781 | + | return "bridge", nil | |||
| 782 | + | } | |||
| 783 | + | ||||
| 784 | + | log.Debug().Str("network", selected).Msg("container network detected") | |||
| 785 | + | return selected, nil | |||
| 786 | + | } | |||
| 787 | + | ||||
| 749 | // InspectImageVolumes gets the volume mount points declared in the image. | 788 | // InspectImageVolumes gets the volume mount points declared in the image. | ||
| 750 | func (r *Runtime) InspectImageVolumes(ctx context.Context, imageRef string) ([]string, error) { | 789 | func (r *Runtime) InspectImageVolumes(ctx context.Context, imageRef string) ([]string, error) { | ||
| 751 | ctx = zerowrap.CtxWithFields(ctx, map[string]any{ | 790 | ctx = zerowrap.CtxWithFields(ctx, map[string]any{ | ||
M internal/boundaries/in/container.go +9 -0
| @@ -26,6 +26,15 @@ type ContainerService interface { | |||
| 26 | 26 | // List returns all managed containers. | |
| 27 | 27 | List(ctx context.Context) map[string]*domain.Container | |
| 28 | 28 | ||
| 29 | + | // ListRoutesWithDetails returns routes with network and attachment info. | |
| 30 | + | ListRoutesWithDetails(ctx context.Context) []domain.RouteInfo | |
| 31 | + | ||
| 32 | + | // ListAttachments returns attachments for a domain. | |
| 33 | + | ListAttachments(ctx context.Context, domain string) []domain.Attachment | |
| 34 | + | ||
| 35 | + | // ListNetworks returns Gordon-managed networks. | |
| 36 | + | ListNetworks(ctx context.Context) ([]*domain.NetworkInfo, error) | |
| 37 | + | ||
| 29 | 38 | // HealthCheck performs health checks on all containers. | |
| 30 | 39 | HealthCheck(ctx context.Context) map[string]bool | |
| 31 | 40 | ||
| @@ -26,6 +26,15 @@ type ContainerService interface { | |||||
| 26 | // List returns all managed containers. | 26 | // List returns all managed containers. | ||
| 27 | List(ctx context.Context) map[string]*domain.Container | 27 | List(ctx context.Context) map[string]*domain.Container | ||
| 28 | 28 | ||||
| 29 | + | // ListRoutesWithDetails returns routes with network and attachment info. | |||
| 30 | + | ListRoutesWithDetails(ctx context.Context) []domain.RouteInfo | |||
| 31 | + | ||||
| 32 | + | // ListAttachments returns attachments for a domain. | |||
| 33 | + | ListAttachments(ctx context.Context, domain string) []domain.Attachment | |||
| 34 | + | ||||
| 35 | + | // ListNetworks returns Gordon-managed networks. | |||
| 36 | + | ListNetworks(ctx context.Context) ([]*domain.NetworkInfo, error) | |||
| 37 | + | ||||
| 29 | // HealthCheck performs health checks on all containers. | 38 | // HealthCheck performs health checks on all containers. | ||
| 30 | HealthCheck(ctx context.Context) map[string]bool | 39 | HealthCheck(ctx context.Context) map[string]bool | ||
| 31 | 40 | ||||
M internal/boundaries/in/mocks/mock_container_service.go +174 -0
| @@ -337,6 +337,180 @@ func (_c *MockContainerService_List_Call) RunAndReturn(run func(ctx context.Cont | |||
| 337 | 337 | return _c | |
| 338 | 338 | } | |
| 339 | 339 | ||
| 340 | + | // ListAttachments provides a mock function for the type MockContainerService | |
| 341 | + | func (_mock *MockContainerService) ListAttachments(ctx context.Context, domain1 string) []domain.Attachment { | |
| 342 | + | ret := _mock.Called(ctx, domain1) | |
| 343 | + | ||
| 344 | + | if len(ret) == 0 { | |
| 345 | + | panic("no return value specified for ListAttachments") | |
| 346 | + | } | |
| 347 | + | ||
| 348 | + | var r0 []domain.Attachment | |
| 349 | + | if returnFunc, ok := ret.Get(0).(func(context.Context, string) []domain.Attachment); ok { | |
| 350 | + | r0 = returnFunc(ctx, domain1) | |
| 351 | + | } else { | |
| 352 | + | if ret.Get(0) != nil { | |
| 353 | + | r0 = ret.Get(0).([]domain.Attachment) | |
| 354 | + | } | |
| 355 | + | } | |
| 356 | + | return r0 | |
| 357 | + | } | |
| 358 | + | ||
| 359 | + | // MockContainerService_ListAttachments_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ListAttachments' | |
| 360 | + | type MockContainerService_ListAttachments_Call struct { | |
| 361 | + | *mock.Call | |
| 362 | + | } | |
| 363 | + | ||
| 364 | + | // ListAttachments is a helper method to define mock.On call | |
| 365 | + | // - ctx context.Context | |
| 366 | + | // - domain1 string | |
| 367 | + | func (_e *MockContainerService_Expecter) ListAttachments(ctx interface{}, domain1 interface{}) *MockContainerService_ListAttachments_Call { | |
| 368 | + | return &MockContainerService_ListAttachments_Call{Call: _e.mock.On("ListAttachments", ctx, domain1)} | |
| 369 | + | } | |
| 370 | + | ||
| 371 | + | func (_c *MockContainerService_ListAttachments_Call) Run(run func(ctx context.Context, domain1 string)) *MockContainerService_ListAttachments_Call { | |
| 372 | + | _c.Call.Run(func(args mock.Arguments) { | |
| 373 | + | var arg0 context.Context | |
| 374 | + | if args[0] != nil { | |
| 375 | + | arg0 = args[0].(context.Context) | |
| 376 | + | } | |
| 377 | + | var arg1 string | |
| 378 | + | if args[1] != nil { | |
| 379 | + | arg1 = args[1].(string) | |
| 380 | + | } | |
| 381 | + | run( | |
| 382 | + | arg0, | |
| 383 | + | arg1, | |
| 384 | + | ) | |
| 385 | + | }) | |
| 386 | + | return _c | |
| 387 | + | } | |
| 388 | + | ||
| 389 | + | func (_c *MockContainerService_ListAttachments_Call) Return(attachments []domain.Attachment) *MockContainerService_ListAttachments_Call { | |
| 390 | + | _c.Call.Return(attachments) | |
| 391 | + | return _c | |
| 392 | + | } | |
| 393 | + | ||
| 394 | + | func (_c *MockContainerService_ListAttachments_Call) RunAndReturn(run func(ctx context.Context, domain1 string) []domain.Attachment) *MockContainerService_ListAttachments_Call { | |
| 395 | + | _c.Call.Return(run) | |
| 396 | + | return _c | |
| 397 | + | } | |
| 398 | + | ||
| 399 | + | // ListNetworks provides a mock function for the type MockContainerService | |
| 400 | + | func (_mock *MockContainerService) ListNetworks(ctx context.Context) ([]*domain.NetworkInfo, error) { | |
| 401 | + | ret := _mock.Called(ctx) | |
| 402 | + | ||
| 403 | + | if len(ret) == 0 { | |
| 404 | + | panic("no return value specified for ListNetworks") | |
| 405 | + | } | |
| 406 | + | ||
| 407 | + | var r0 []*domain.NetworkInfo | |
| 408 | + | var r1 error | |
| 409 | + | if returnFunc, ok := ret.Get(0).(func(context.Context) ([]*domain.NetworkInfo, error)); ok { | |
| 410 | + | return returnFunc(ctx) | |
| 411 | + | } | |
| 412 | + | if returnFunc, ok := ret.Get(0).(func(context.Context) []*domain.NetworkInfo); ok { | |
| 413 | + | r0 = returnFunc(ctx) | |
| 414 | + | } else { | |
| 415 | + | if ret.Get(0) != nil { | |
| 416 | + | r0 = ret.Get(0).([]*domain.NetworkInfo) | |
| 417 | + | } | |
| 418 | + | } | |
| 419 | + | if returnFunc, ok := ret.Get(1).(func(context.Context) error); ok { | |
| 420 | + | r1 = returnFunc(ctx) | |
| 421 | + | } else { | |
| 422 | + | r1 = ret.Error(1) | |
| 423 | + | } | |
| 424 | + | return r0, r1 | |
| 425 | + | } | |
| 426 | + | ||
| 427 | + | // MockContainerService_ListNetworks_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ListNetworks' | |
| 428 | + | type MockContainerService_ListNetworks_Call struct { | |
| 429 | + | *mock.Call | |
| 430 | + | } | |
| 431 | + | ||
| 432 | + | // ListNetworks is a helper method to define mock.On call | |
| 433 | + | // - ctx context.Context | |
| 434 | + | func (_e *MockContainerService_Expecter) ListNetworks(ctx interface{}) *MockContainerService_ListNetworks_Call { | |
| 435 | + | return &MockContainerService_ListNetworks_Call{Call: _e.mock.On("ListNetworks", ctx)} | |
| 436 | + | } | |
| 437 | + | ||
| 438 | + | func (_c *MockContainerService_ListNetworks_Call) Run(run func(ctx context.Context)) *MockContainerService_ListNetworks_Call { | |
| 439 | + | _c.Call.Run(func(args mock.Arguments) { | |
| 440 | + | var arg0 context.Context | |
| 441 | + | if args[0] != nil { | |
| 442 | + | arg0 = args[0].(context.Context) | |
| 443 | + | } | |
| 444 | + | run( | |
| 445 | + | arg0, | |
| 446 | + | ) | |
| 447 | + | }) | |
| 448 | + | return _c | |
| 449 | + | } | |
| 450 | + | ||
| 451 | + | func (_c *MockContainerService_ListNetworks_Call) Return(networkInfos []*domain.NetworkInfo, err error) *MockContainerService_ListNetworks_Call { | |
| 452 | + | _c.Call.Return(networkInfos, err) | |
| 453 | + | return _c | |
| 454 | + | } | |
| 455 | + | ||
| 456 | + | func (_c *MockContainerService_ListNetworks_Call) RunAndReturn(run func(ctx context.Context) ([]*domain.NetworkInfo, error)) *MockContainerService_ListNetworks_Call { | |
| 457 | + | _c.Call.Return(run) | |
| 458 | + | return _c | |
| 459 | + | } | |
| 460 | + | ||
| 461 | + | // ListRoutesWithDetails provides a mock function for the type MockContainerService | |
| 462 | + | func (_mock *MockContainerService) ListRoutesWithDetails(ctx context.Context) []domain.RouteInfo { | |
| 463 | + | ret := _mock.Called(ctx) | |
| 464 | + | ||
| 465 | + | if len(ret) == 0 { | |
| 466 | + | panic("no return value specified for ListRoutesWithDetails") | |
| 467 | + | } | |
| 468 | + | ||
| 469 | + | var r0 []domain.RouteInfo | |
| 470 | + | if returnFunc, ok := ret.Get(0).(func(context.Context) []domain.RouteInfo); ok { | |
| 471 | + | r0 = returnFunc(ctx) | |
| 472 | + | } else { | |
| 473 | + | if ret.Get(0) != nil { | |
| 474 | + | r0 = ret.Get(0).([]domain.RouteInfo) | |
| 475 | + | } | |
| 476 | + | } | |
| 477 | + | return r0 | |
| 478 | + | } | |
| 479 | + | ||
| 480 | + | // MockContainerService_ListRoutesWithDetails_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ListRoutesWithDetails' | |
| 481 | + | type MockContainerService_ListRoutesWithDetails_Call struct { | |
| 482 | + | *mock.Call | |
| 483 | + | } | |
| 484 | + | ||
| 485 | + | // ListRoutesWithDetails is a helper method to define mock.On call | |
| 486 | + | // - ctx context.Context | |
| 487 | + | func (_e *MockContainerService_Expecter) ListRoutesWithDetails(ctx interface{}) *MockContainerService_ListRoutesWithDetails_Call { | |
| 488 | + | return &MockContainerService_ListRoutesWithDetails_Call{Call: _e.mock.On("ListRoutesWithDetails", ctx)} | |
| 489 | + | } | |
| 490 | + | ||
| 491 | + | func (_c *MockContainerService_ListRoutesWithDetails_Call) Run(run func(ctx context.Context)) *MockContainerService_ListRoutesWithDetails_Call { | |
| 492 | + | _c.Call.Run(func(args mock.Arguments) { | |
| 493 | + | var arg0 context.Context | |
| 494 | + | if args[0] != nil { | |
| 495 | + | arg0 = args[0].(context.Context) | |
| 496 | + | } | |
| 497 | + | run( | |
| 498 | + | arg0, | |
| 499 | + | ) | |
| 500 | + | }) | |
| 501 | + | return _c | |
| 502 | + | } | |
| 503 | + | ||
| 504 | + | func (_c *MockContainerService_ListRoutesWithDetails_Call) Return(routeInfos []domain.RouteInfo) *MockContainerService_ListRoutesWithDetails_Call { | |
| 505 | + | _c.Call.Return(routeInfos) | |
| 506 | + | return _c | |
| 507 | + | } | |
| 508 | + | ||
| 509 | + | func (_c *MockContainerService_ListRoutesWithDetails_Call) RunAndReturn(run func(ctx context.Context) []domain.RouteInfo) *MockContainerService_ListRoutesWithDetails_Call { | |
| 510 | + | _c.Call.Return(run) | |
| 511 | + | return _c | |
| 512 | + | } | |
| 513 | + | ||
| 340 | 514 | // Remove provides a mock function for the type MockContainerService | |
| 341 | 515 | func (_mock *MockContainerService) Remove(ctx context.Context, containerID string, force bool) error { | |
| 342 | 516 | ret := _mock.Called(ctx, containerID, force) | |
| @@ -337,6 +337,180 @@ func (_c *MockContainerService_List_Call) RunAndReturn(run func(ctx context.Cont | |||||
| 337 | return _c | 337 | return _c | ||
| 338 | } | 338 | } | ||
| 339 | 339 | ||||
| 340 | + | // ListAttachments provides a mock function for the type MockContainerService | |||
| 341 | + | func (_mock *MockContainerService) ListAttachments(ctx context.Context, domain1 string) []domain.Attachment { | |||
| 342 | + | ret := _mock.Called(ctx, domain1) | |||
| 343 | + | ||||
| 344 | + | if len(ret) == 0 { | |||
| 345 | + | panic("no return value specified for ListAttachments") | |||
| 346 | + | } | |||
| 347 | + | ||||
| 348 | + | var r0 []domain.Attachment | |||
| 349 | + | if returnFunc, ok := ret.Get(0).(func(context.Context, string) []domain.Attachment); ok { | |||
| 350 | + | r0 = returnFunc(ctx, domain1) | |||
| 351 | + | } else { | |||
| 352 | + | if ret.Get(0) != nil { | |||
| 353 | + | r0 = ret.Get(0).([]domain.Attachment) | |||
| 354 | + | } | |||
| 355 | + | } | |||
| 356 | + | return r0 | |||
| 357 | + | } | |||
| 358 | + | ||||
| 359 | + | // MockContainerService_ListAttachments_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ListAttachments' | |||
| 360 | + | type MockContainerService_ListAttachments_Call struct { | |||
| 361 | + | *mock.Call | |||
| 362 | + | } | |||
| 363 | + | ||||
| 364 | + | // ListAttachments is a helper method to define mock.On call | |||
| 365 | + | // - ctx context.Context | |||
| 366 | + | // - domain1 string | |||
| 367 | + | func (_e *MockContainerService_Expecter) ListAttachments(ctx interface{}, domain1 interface{}) *MockContainerService_ListAttachments_Call { | |||
| 368 | + | return &MockContainerService_ListAttachments_Call{Call: _e.mock.On("ListAttachments", ctx, domain1)} | |||
| 369 | + | } | |||
| 370 | + | ||||
| 371 | + | func (_c *MockContainerService_ListAttachments_Call) Run(run func(ctx context.Context, domain1 string)) *MockContainerService_ListAttachments_Call { | |||
| 372 | + | _c.Call.Run(func(args mock.Arguments) { | |||
| 373 | + | var arg0 context.Context | |||
| 374 | + | if args[0] != nil { | |||
| 375 | + | arg0 = args[0].(context.Context) | |||
| 376 | + | } | |||
| 377 | + | var arg1 string | |||
| 378 | + | if args[1] != nil { | |||
| 379 | + | arg1 = args[1].(string) | |||
| 380 | + | } | |||
| 381 | + | run( | |||
| 382 | + | arg0, | |||
| 383 | + | arg1, | |||
| 384 | + | ) | |||
| 385 | + | }) | |||
| 386 | + | return _c | |||
| 387 | + | } | |||
| 388 | + | ||||
| 389 | + | func (_c *MockContainerService_ListAttachments_Call) Return(attachments []domain.Attachment) *MockContainerService_ListAttachments_Call { | |||
| 390 | + | _c.Call.Return(attachments) | |||
| 391 | + | return _c | |||
| 392 | + | } | |||
| 393 | + | ||||
| 394 | + | func (_c *MockContainerService_ListAttachments_Call) RunAndReturn(run func(ctx context.Context, domain1 string) []domain.Attachment) *MockContainerService_ListAttachments_Call { | |||
| 395 | + | _c.Call.Return(run) | |||
| 396 | + | return _c | |||
| 397 | + | } | |||
| 398 | + | ||||
| 399 | + | // ListNetworks provides a mock function for the type MockContainerService | |||
| 400 | + | func (_mock *MockContainerService) ListNetworks(ctx context.Context) ([]*domain.NetworkInfo, error) { | |||
| 401 | + | ret := _mock.Called(ctx) | |||
| 402 | + | ||||
| 403 | + | if len(ret) == 0 { | |||
| 404 | + | panic("no return value specified for ListNetworks") | |||
| 405 | + | } | |||
| 406 | + | ||||
| 407 | + | var r0 []*domain.NetworkInfo | |||
| 408 | + | var r1 error | |||
| 409 | + | if returnFunc, ok := ret.Get(0).(func(context.Context) ([]*domain.NetworkInfo, error)); ok { | |||
| 410 | + | return returnFunc(ctx) | |||
| 411 | + | } | |||
| 412 | + | if returnFunc, ok := ret.Get(0).(func(context.Context) []*domain.NetworkInfo); ok { | |||
| 413 | + | r0 = returnFunc(ctx) | |||
| 414 | + | } else { | |||
| 415 | + | if ret.Get(0) != nil { | |||
| 416 | + | r0 = ret.Get(0).([]*domain.NetworkInfo) | |||
| 417 | + | } | |||
| 418 | + | } | |||
| 419 | + | if returnFunc, ok := ret.Get(1).(func(context.Context) error); ok { | |||
| 420 | + | r1 = returnFunc(ctx) | |||
| 421 | + | } else { | |||
| 422 | + | r1 = ret.Error(1) | |||
| 423 | + | } | |||
| 424 | + | return r0, r1 | |||
| 425 | + | } | |||
| 426 | + | ||||
| 427 | + | // MockContainerService_ListNetworks_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ListNetworks' | |||
| 428 | + | type MockContainerService_ListNetworks_Call struct { | |||
| 429 | + | *mock.Call | |||
| 430 | + | } | |||
| 431 | + | ||||
| 432 | + | // ListNetworks is a helper method to define mock.On call | |||
| 433 | + | // - ctx context.Context | |||
| 434 | + | func (_e *MockContainerService_Expecter) ListNetworks(ctx interface{}) *MockContainerService_ListNetworks_Call { | |||
| 435 | + | return &MockContainerService_ListNetworks_Call{Call: _e.mock.On("ListNetworks", ctx)} | |||
| 436 | + | } | |||
| 437 | + | ||||
| 438 | + | func (_c *MockContainerService_ListNetworks_Call) Run(run func(ctx context.Context)) *MockContainerService_ListNetworks_Call { | |||
| 439 | + | _c.Call.Run(func(args mock.Arguments) { | |||
| 440 | + | var arg0 context.Context | |||
| 441 | + | if args[0] != nil { | |||
| 442 | + | arg0 = args[0].(context.Context) | |||
| 443 | + | } | |||
| 444 | + | run( | |||
| 445 | + | arg0, | |||
| 446 | + | ) | |||
| 447 | + | }) | |||
| 448 | + | return _c | |||
| 449 | + | } | |||
| 450 | + | ||||
| 451 | + | func (_c *MockContainerService_ListNetworks_Call) Return(networkInfos []*domain.NetworkInfo, err error) *MockContainerService_ListNetworks_Call { | |||
| 452 | + | _c.Call.Return(networkInfos, err) | |||
| 453 | + | return _c | |||
| 454 | + | } | |||
| 455 | + | ||||
| 456 | + | func (_c *MockContainerService_ListNetworks_Call) RunAndReturn(run func(ctx context.Context) ([]*domain.NetworkInfo, error)) *MockContainerService_ListNetworks_Call { | |||
| 457 | + | _c.Call.Return(run) | |||
| 458 | + | return _c | |||
| 459 | + | } | |||
| 460 | + | ||||
| 461 | + | // ListRoutesWithDetails provides a mock function for the type MockContainerService | |||
| 462 | + | func (_mock *MockContainerService) ListRoutesWithDetails(ctx context.Context) []domain.RouteInfo { | |||
| 463 | + | ret := _mock.Called(ctx) | |||
| 464 | + | ||||
| 465 | + | if len(ret) == 0 { | |||
| 466 | + | panic("no return value specified for ListRoutesWithDetails") | |||
| 467 | + | } | |||
| 468 | + | ||||
| 469 | + | var r0 []domain.RouteInfo | |||
| 470 | + | if returnFunc, ok := ret.Get(0).(func(context.Context) []domain.RouteInfo); ok { | |||
| 471 | + | r0 = returnFunc(ctx) | |||
| 472 | + | } else { | |||
| 473 | + | if ret.Get(0) != nil { | |||
| 474 | + | r0 = ret.Get(0).([]domain.RouteInfo) | |||
| 475 | + | } | |||
| 476 | + | } | |||
| 477 | + | return r0 | |||
| 478 | + | } | |||
| 479 | + | ||||
| 480 | + | // MockContainerService_ListRoutesWithDetails_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ListRoutesWithDetails' | |||
| 481 | + | type MockContainerService_ListRoutesWithDetails_Call struct { | |||
| 482 | + | *mock.Call | |||
| 483 | + | } | |||
| 484 | + | ||||
| 485 | + | // ListRoutesWithDetails is a helper method to define mock.On call | |||
| 486 | + | // - ctx context.Context | |||
| 487 | + | func (_e *MockContainerService_Expecter) ListRoutesWithDetails(ctx interface{}) *MockContainerService_ListRoutesWithDetails_Call { | |||
| 488 | + | return &MockContainerService_ListRoutesWithDetails_Call{Call: _e.mock.On("ListRoutesWithDetails", ctx)} | |||
| 489 | + | } | |||
| 490 | + | ||||
| 491 | + | func (_c *MockContainerService_ListRoutesWithDetails_Call) Run(run func(ctx context.Context)) *MockContainerService_ListRoutesWithDetails_Call { | |||
| 492 | + | _c.Call.Run(func(args mock.Arguments) { | |||
| 493 | + | var arg0 context.Context | |||
| 494 | + | if args[0] != nil { | |||
| 495 | + | arg0 = args[0].(context.Context) | |||
| 496 | + | } | |||
| 497 | + | run( | |||
| 498 | + | arg0, | |||
| 499 | + | ) | |||
| 500 | + | }) | |||
| 501 | + | return _c | |||
| 502 | + | } | |||
| 503 | + | ||||
| 504 | + | func (_c *MockContainerService_ListRoutesWithDetails_Call) Return(routeInfos []domain.RouteInfo) *MockContainerService_ListRoutesWithDetails_Call { | |||
| 505 | + | _c.Call.Return(routeInfos) | |||
| 506 | + | return _c | |||
| 507 | + | } | |||
| 508 | + | ||||
| 509 | + | func (_c *MockContainerService_ListRoutesWithDetails_Call) RunAndReturn(run func(ctx context.Context) []domain.RouteInfo) *MockContainerService_ListRoutesWithDetails_Call { | |||
| 510 | + | _c.Call.Return(run) | |||
| 511 | + | return _c | |||
| 512 | + | } | |||
| 513 | + | ||||
| 340 | // Remove provides a mock function for the type MockContainerService | 514 | // Remove provides a mock function for the type MockContainerService | ||
| 341 | func (_mock *MockContainerService) Remove(ctx context.Context, containerID string, force bool) error { | 515 | func (_mock *MockContainerService) Remove(ctx context.Context, containerID string, force bool) error { | ||
| 342 | ret := _mock.Called(ctx, containerID, force) | 516 | ret := _mock.Called(ctx, containerID, force) | ||
M internal/boundaries/out/mocks/mock_container_runtime.go +66 -0
| @@ -495,6 +495,72 @@ func (_c *MockContainerRuntime_GetContainerLogs_Call) RunAndReturn(run func(ctx | |||
| 495 | 495 | return _c | |
| 496 | 496 | } | |
| 497 | 497 | ||
| 498 | + | // GetContainerNetwork provides a mock function for the type MockContainerRuntime | |
| 499 | + | func (_mock *MockContainerRuntime) GetContainerNetwork(ctx context.Context, containerID string) (string, error) { | |
| 500 | + | ret := _mock.Called(ctx, containerID) | |
| 501 | + | ||
| 502 | + | if len(ret) == 0 { | |
| 503 | + | panic("no return value specified for GetContainerNetwork") | |
| 504 | + | } | |
| 505 | + | ||
| 506 | + | var r0 string | |
| 507 | + | var r1 error | |
| 508 | + | if returnFunc, ok := ret.Get(0).(func(context.Context, string) (string, error)); ok { | |
| 509 | + | return returnFunc(ctx, containerID) | |
| 510 | + | } | |
| 511 | + | if returnFunc, ok := ret.Get(0).(func(context.Context, string) string); ok { | |
| 512 | + | r0 = returnFunc(ctx, containerID) | |
| 513 | + | } else { | |
| 514 | + | r0 = ret.Get(0).(string) | |
| 515 | + | } | |
| 516 | + | if returnFunc, ok := ret.Get(1).(func(context.Context, string) error); ok { | |
| 517 | + | r1 = returnFunc(ctx, containerID) | |
| 518 | + | } else { | |
| 519 | + | r1 = ret.Error(1) | |
| 520 | + | } | |
| 521 | + | return r0, r1 | |
| 522 | + | } | |
| 523 | + | ||
| 524 | + | // MockContainerRuntime_GetContainerNetwork_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetContainerNetwork' | |
| 525 | + | type MockContainerRuntime_GetContainerNetwork_Call struct { | |
| 526 | + | *mock.Call | |
| 527 | + | } | |
| 528 | + | ||
| 529 | + | // GetContainerNetwork is a helper method to define mock.On call | |
| 530 | + | // - ctx context.Context | |
| 531 | + | // - containerID string | |
| 532 | + | func (_e *MockContainerRuntime_Expecter) GetContainerNetwork(ctx interface{}, containerID interface{}) *MockContainerRuntime_GetContainerNetwork_Call { | |
| 533 | + | return &MockContainerRuntime_GetContainerNetwork_Call{Call: _e.mock.On("GetContainerNetwork", ctx, containerID)} | |
| 534 | + | } | |
| 535 | + | ||
| 536 | + | func (_c *MockContainerRuntime_GetContainerNetwork_Call) Run(run func(ctx context.Context, containerID string)) *MockContainerRuntime_GetContainerNetwork_Call { | |
| 537 | + | _c.Call.Run(func(args mock.Arguments) { | |
| 538 | + | var arg0 context.Context | |
| 539 | + | if args[0] != nil { | |
| 540 | + | arg0 = args[0].(context.Context) | |
| 541 | + | } | |
| 542 | + | var arg1 string | |
| 543 | + | if args[1] != nil { | |
| 544 | + | arg1 = args[1].(string) | |
| 545 | + | } | |
| 546 | + | run( | |
| 547 | + | arg0, | |
| 548 | + | arg1, | |
| 549 | + | ) | |
| 550 | + | }) | |
| 551 | + | return _c | |
| 552 | + | } | |
| 553 | + | ||
| 554 | + | func (_c *MockContainerRuntime_GetContainerNetwork_Call) Return(s string, err error) *MockContainerRuntime_GetContainerNetwork_Call { | |
| 555 | + | _c.Call.Return(s, err) | |
| 556 | + | return _c | |
| 557 | + | } | |
| 558 | + | ||
| 559 | + | func (_c *MockContainerRuntime_GetContainerNetwork_Call) RunAndReturn(run func(ctx context.Context, containerID string) (string, error)) *MockContainerRuntime_GetContainerNetwork_Call { | |
| 560 | + | _c.Call.Return(run) | |
| 561 | + | return _c | |
| 562 | + | } | |
| 563 | + | ||
| 498 | 564 | // GetContainerNetworkInfo provides a mock function for the type MockContainerRuntime | |
| 499 | 565 | func (_mock *MockContainerRuntime) GetContainerNetworkInfo(ctx context.Context, containerID string) (string, int, error) { | |
| 500 | 566 | ret := _mock.Called(ctx, containerID) | |
| @@ -495,6 +495,72 @@ func (_c *MockContainerRuntime_GetContainerLogs_Call) RunAndReturn(run func(ctx | |||||
| 495 | return _c | 495 | return _c | ||
| 496 | } | 496 | } | ||
| 497 | 497 | ||||
| 498 | + | // GetContainerNetwork provides a mock function for the type MockContainerRuntime | |||
| 499 | + | func (_mock *MockContainerRuntime) GetContainerNetwork(ctx context.Context, containerID string) (string, error) { | |||
| 500 | + | ret := _mock.Called(ctx, containerID) | |||
| 501 | + | ||||
| 502 | + | if len(ret) == 0 { | |||
| 503 | + | panic("no return value specified for GetContainerNetwork") | |||
| 504 | + | } | |||
| 505 | + | ||||
| 506 | + | var r0 string | |||
| 507 | + | var r1 error | |||
| 508 | + | if returnFunc, ok := ret.Get(0).(func(context.Context, string) (string, error)); ok { | |||
| 509 | + | return returnFunc(ctx, containerID) | |||
| 510 | + | } | |||
| 511 | + | if returnFunc, ok := ret.Get(0).(func(context.Context, string) string); ok { | |||
| 512 | + | r0 = returnFunc(ctx, containerID) | |||
| 513 | + | } else { | |||
| 514 | + | r0 = ret.Get(0).(string) | |||
| 515 | + | } | |||
| 516 | + | if returnFunc, ok := ret.Get(1).(func(context.Context, string) error); ok { | |||
| 517 | + | r1 = returnFunc(ctx, containerID) | |||
| 518 | + | } else { | |||
| 519 | + | r1 = ret.Error(1) | |||
| 520 | + | } | |||
| 521 | + | return r0, r1 | |||
| 522 | + | } | |||
| 523 | + | ||||
| 524 | + | // MockContainerRuntime_GetContainerNetwork_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetContainerNetwork' | |||
| 525 | + | type MockContainerRuntime_GetContainerNetwork_Call struct { | |||
| 526 | + | *mock.Call | |||
| 527 | + | } | |||
| 528 | + | ||||
| 529 | + | // GetContainerNetwork is a helper method to define mock.On call | |||
| 530 | + | // - ctx context.Context | |||
| 531 | + | // - containerID string | |||
| 532 | + | func (_e *MockContainerRuntime_Expecter) GetContainerNetwork(ctx interface{}, containerID interface{}) *MockContainerRuntime_GetContainerNetwork_Call { | |||
| 533 | + | return &MockContainerRuntime_GetContainerNetwork_Call{Call: _e.mock.On("GetContainerNetwork", ctx, containerID)} | |||
| 534 | + | } | |||
| 535 | + | ||||
| 536 | + | func (_c *MockContainerRuntime_GetContainerNetwork_Call) Run(run func(ctx context.Context, containerID string)) *MockContainerRuntime_GetContainerNetwork_Call { | |||
| 537 | + | _c.Call.Run(func(args mock.Arguments) { | |||
| 538 | + | var arg0 context.Context | |||
| 539 | + | if args[0] != nil { | |||
| 540 | + | arg0 = args[0].(context.Context) | |||
| 541 | + | } | |||
| 542 | + | var arg1 string | |||
| 543 | + | if args[1] != nil { | |||
| 544 | + | arg1 = args[1].(string) | |||
| 545 | + | } | |||
| 546 | + | run( | |||
| 547 | + | arg0, | |||
| 548 | + | arg1, | |||
| 549 | + | ) | |||
| 550 | + | }) | |||
| 551 | + | return _c | |||
| 552 | + | } | |||
| 553 | + | ||||
| 554 | + | func (_c *MockContainerRuntime_GetContainerNetwork_Call) Return(s string, err error) *MockContainerRuntime_GetContainerNetwork_Call { | |||
| 555 | + | _c.Call.Return(s, err) | |||
| 556 | + | return _c | |||
| 557 | + | } | |||
| 558 | + | ||||
| 559 | + | func (_c *MockContainerRuntime_GetContainerNetwork_Call) RunAndReturn(run func(ctx context.Context, containerID string) (string, error)) *MockContainerRuntime_GetContainerNetwork_Call { | |||
| 560 | + | _c.Call.Return(run) | |||
| 561 | + | return _c | |||
| 562 | + | } | |||
| 563 | + | ||||
| 498 | // GetContainerNetworkInfo provides a mock function for the type MockContainerRuntime | 564 | // GetContainerNetworkInfo provides a mock function for the type MockContainerRuntime | ||
| 499 | func (_mock *MockContainerRuntime) GetContainerNetworkInfo(ctx context.Context, containerID string) (string, int, error) { | 565 | func (_mock *MockContainerRuntime) GetContainerNetworkInfo(ctx context.Context, containerID string) (string, int, error) { | ||
| 500 | ret := _mock.Called(ctx, containerID) | 566 | ret := _mock.Called(ctx, containerID) | ||
M internal/boundaries/out/runtime.go +1 -0
| @@ -46,6 +46,7 @@ type ContainerRuntime interface { | |||
| 46 | 46 | GetImageExposedPorts(ctx context.Context, imageRef string) ([]int, error) | |
| 47 | 47 | GetContainerExposedPorts(ctx context.Context, containerID string) ([]int, error) | |
| 48 | 48 | GetContainerNetworkInfo(ctx context.Context, containerID string) (string, int, error) | |
| 49 | + | GetContainerNetwork(ctx context.Context, containerID string) (string, error) | |
| 49 | 50 | ||
| 50 | 51 | // Volume management | |
| 51 | 52 | InspectImageVolumes(ctx context.Context, imageRef string) ([]string, error) | |
| @@ -46,6 +46,7 @@ type ContainerRuntime interface { | |||||
| 46 | GetImageExposedPorts(ctx context.Context, imageRef string) ([]int, error) | 46 | GetImageExposedPorts(ctx context.Context, imageRef string) ([]int, error) | ||
| 47 | GetContainerExposedPorts(ctx context.Context, containerID string) ([]int, error) | 47 | GetContainerExposedPorts(ctx context.Context, containerID string) ([]int, error) | ||
| 48 | GetContainerNetworkInfo(ctx context.Context, containerID string) (string, int, error) | 48 | GetContainerNetworkInfo(ctx context.Context, containerID string) (string, int, error) | ||
| 49 | + | GetContainerNetwork(ctx context.Context, containerID string) (string, error) | |||
| 49 | 50 | ||||
| 50 | // Volume management | 51 | // Volume management | ||
| 51 | InspectImageVolumes(ctx context.Context, imageRef string) ([]string, error) | 52 | InspectImageVolumes(ctx context.Context, imageRef string) ([]string, error) | ||
M internal/domain/container.go +18 -0
| @@ -21,6 +21,24 @@ type NetworkInfo struct { | |||
| 21 | 21 | Labels map[string]string | |
| 22 | 22 | } | |
| 23 | 23 | ||
| 24 | + | // Attachment represents an attached service container. | |
| 25 | + | type Attachment struct { | |
| 26 | + | Name string | |
| 27 | + | Image string | |
| 28 | + | ContainerID string | |
| 29 | + | Status string | |
| 30 | + | } | |
| 31 | + | ||
| 32 | + | // RouteInfo combines route configuration with runtime state. | |
| 33 | + | type RouteInfo struct { | |
| 34 | + | Domain string | |
| 35 | + | Image string | |
| 36 | + | ContainerID string | |
| 37 | + | ContainerStatus string | |
| 38 | + | Network string | |
| 39 | + | Attachments []Attachment | |
| 40 | + | } | |
| 41 | + | ||
| 24 | 42 | // ContainerConfig holds configuration for creating a container. | |
| 25 | 43 | type ContainerConfig struct { | |
| 26 | 44 | Image string | |
| @@ -21,6 +21,24 @@ type NetworkInfo struct { | |||||
| 21 | Labels map[string]string | 21 | Labels map[string]string | ||
| 22 | } | 22 | } | ||
| 23 | 23 | ||||
| 24 | + | // Attachment represents an attached service container. | |||
| 25 | + | type Attachment struct { | |||
| 26 | + | Name string | |||
| 27 | + | Image string | |||
| 28 | + | ContainerID string | |||
| 29 | + | Status string | |||
| 30 | + | } | |||
| 31 | + | ||||
| 32 | + | // RouteInfo combines route configuration with runtime state. | |||
| 33 | + | type RouteInfo struct { | |||
| 34 | + | Domain string | |||
| 35 | + | Image string | |||
| 36 | + | ContainerID string | |||
| 37 | + | ContainerStatus string | |||
| 38 | + | Network string | |||
| 39 | + | Attachments []Attachment | |||
| 40 | + | } | |||
| 41 | + | ||||
| 24 | // ContainerConfig holds configuration for creating a container. | 42 | // ContainerConfig holds configuration for creating a container. | ||
| 25 | type ContainerConfig struct { | 43 | type ContainerConfig struct { | ||
| 26 | Image string | 44 | Image string | ||
A internal/usecase/container/routes_test.go +162 -0
| @@ -0,0 +1,162 @@ | |||
| 1 | + | package container | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "testing" | |
| 5 | + | ||
| 6 | + | "github.com/stretchr/testify/assert" | |
| 7 | + | "github.com/stretchr/testify/mock" | |
| 8 | + | ||
| 9 | + | "gordon/internal/boundaries/out/mocks" | |
| 10 | + | "gordon/internal/domain" | |
| 11 | + | ) | |
| 12 | + | ||
| 13 | + | func TestService_ListRoutesWithDetails(t *testing.T) { | |
| 14 | + | runtime := mocks.NewMockContainerRuntime(t) | |
| 15 | + | svc := NewService(runtime, nil, nil, nil, Config{NetworkPrefix: "gordon"}) | |
| 16 | + | ctx := testContext() | |
| 17 | + | ||
| 18 | + | svc.containers["app.example.com"] = &domain.Container{ | |
| 19 | + | ID: "container-1", | |
| 20 | + | Image: "registry/app:latest", | |
| 21 | + | Status: "running", | |
| 22 | + | Labels: map[string]string{ | |
| 23 | + | domain.LabelImage: "app:latest", | |
| 24 | + | }, | |
| 25 | + | } | |
| 26 | + | ||
| 27 | + | runtime.EXPECT().GetContainerNetwork(mock.Anything, "container-1").Return("gordon-app-example-com", nil) | |
| 28 | + | runtime.EXPECT().ListContainers(mock.Anything, true).Return([]*domain.Container{ | |
| 29 | + | { | |
| 30 | + | ID: "attach-1", | |
| 31 | + | Name: "gordon-app-example-com-postgres", | |
| 32 | + | Image: "postgres:15", | |
| 33 | + | Status: "running", | |
| 34 | + | Labels: map[string]string{ | |
| 35 | + | domain.LabelAttachment: "true", | |
| 36 | + | domain.LabelAttachedTo: "app.example.com", | |
| 37 | + | domain.LabelImage: "postgres:15", | |
| 38 | + | }, | |
| 39 | + | }, | |
| 40 | + | { | |
| 41 | + | ID: "attach-2", | |
| 42 | + | Name: "gordon-other-redis", | |
| 43 | + | Image: "redis:7", | |
| 44 | + | Status: "running", | |
| 45 | + | Labels: map[string]string{ | |
| 46 | + | domain.LabelAttachment: "true", | |
| 47 | + | domain.LabelAttachedTo: "other.example.com", | |
| 48 | + | domain.LabelImage: "redis:7", | |
| 49 | + | }, | |
| 50 | + | }, | |
| 51 | + | { | |
| 52 | + | ID: "other", | |
| 53 | + | Name: "gordon-other", | |
| 54 | + | Image: "busybox", | |
| 55 | + | Status: "running", | |
| 56 | + | Labels: map[string]string{ | |
| 57 | + | domain.LabelAttachment: "false", | |
| 58 | + | }, | |
| 59 | + | }, | |
| 60 | + | }, nil) | |
| 61 | + | ||
| 62 | + | results := svc.ListRoutesWithDetails(ctx) | |
| 63 | + | ||
| 64 | + | assert.Len(t, results, 1) | |
| 65 | + | assert.Equal(t, "app.example.com", results[0].Domain) | |
| 66 | + | assert.Equal(t, "app:latest", results[0].Image) | |
| 67 | + | assert.Equal(t, "container-1", results[0].ContainerID) | |
| 68 | + | assert.Equal(t, "running", results[0].ContainerStatus) | |
| 69 | + | assert.Equal(t, "gordon-app-example-com", results[0].Network) | |
| 70 | + | if assert.Len(t, results[0].Attachments, 1) { | |
| 71 | + | assert.Equal(t, "postgres", results[0].Attachments[0].Name) | |
| 72 | + | assert.Equal(t, "postgres:15", results[0].Attachments[0].Image) | |
| 73 | + | assert.Equal(t, "attach-1", results[0].Attachments[0].ContainerID) | |
| 74 | + | } | |
| 75 | + | } | |
| 76 | + | ||
| 77 | + | func TestService_ListRoutesWithDetails_Empty(t *testing.T) { | |
| 78 | + | runtime := mocks.NewMockContainerRuntime(t) | |
| 79 | + | svc := NewService(runtime, nil, nil, nil, Config{NetworkPrefix: "gordon"}) | |
| 80 | + | ctx := testContext() | |
| 81 | + | ||
| 82 | + | results := svc.ListRoutesWithDetails(ctx) | |
| 83 | + | ||
| 84 | + | assert.Empty(t, results) | |
| 85 | + | } | |
| 86 | + | ||
| 87 | + | func TestService_ListRoutesWithDetails_NetworkError(t *testing.T) { | |
| 88 | + | runtime := mocks.NewMockContainerRuntime(t) | |
| 89 | + | svc := NewService(runtime, nil, nil, nil, Config{NetworkPrefix: "gordon"}) | |
| 90 | + | ctx := testContext() | |
| 91 | + | ||
| 92 | + | svc.containers["app.example.com"] = &domain.Container{ | |
| 93 | + | ID: "container-1", | |
| 94 | + | Image: "app:latest", | |
| 95 | + | Status: "running", | |
| 96 | + | } | |
| 97 | + | ||
| 98 | + | runtime.EXPECT().GetContainerNetwork(mock.Anything, "container-1").Return("", assert.AnError) | |
| 99 | + | runtime.EXPECT().ListContainers(mock.Anything, true).Return([]*domain.Container{}, nil) | |
| 100 | + | ||
| 101 | + | results := svc.ListRoutesWithDetails(ctx) | |
| 102 | + | ||
| 103 | + | assert.Len(t, results, 1) | |
| 104 | + | assert.Equal(t, "", results[0].Network) | |
| 105 | + | } | |
| 106 | + | ||
| 107 | + | func TestService_ListAttachments(t *testing.T) { | |
| 108 | + | runtime := mocks.NewMockContainerRuntime(t) | |
| 109 | + | svc := NewService(runtime, nil, nil, nil, Config{}) | |
| 110 | + | ctx := testContext() | |
| 111 | + | ||
| 112 | + | runtime.EXPECT().ListContainers(mock.Anything, true).Return([]*domain.Container{ | |
| 113 | + | { | |
| 114 | + | ID: "attach-1", | |
| 115 | + | Name: "gordon-app-example-com-postgres", | |
| 116 | + | Image: "postgres:15", | |
| 117 | + | Status: "running", | |
| 118 | + | Labels: map[string]string{ | |
| 119 | + | domain.LabelAttachment: "true", | |
| 120 | + | domain.LabelAttachedTo: "app.example.com", | |
| 121 | + | domain.LabelImage: "postgres:15", | |
| 122 | + | }, | |
| 123 | + | }, | |
| 124 | + | { | |
| 125 | + | ID: "attach-2", | |
| 126 | + | Name: "gordon-other-redis", | |
| 127 | + | Image: "redis:7", | |
| 128 | + | Status: "running", | |
| 129 | + | Labels: map[string]string{ | |
| 130 | + | domain.LabelAttachment: "true", | |
| 131 | + | domain.LabelAttachedTo: "other.example.com", | |
| 132 | + | domain.LabelImage: "redis:7", | |
| 133 | + | }, | |
| 134 | + | }, | |
| 135 | + | }, nil) | |
| 136 | + | ||
| 137 | + | attachments := svc.ListAttachments(ctx, "app.example.com") | |
| 138 | + | ||
| 139 | + | assert.Len(t, attachments, 1) | |
| 140 | + | assert.Equal(t, "postgres", attachments[0].Name) | |
| 141 | + | assert.Equal(t, "postgres:15", attachments[0].Image) | |
| 142 | + | assert.Equal(t, "attach-1", attachments[0].ContainerID) | |
| 143 | + | } | |
| 144 | + | ||
| 145 | + | func TestService_ListNetworks(t *testing.T) { | |
| 146 | + | runtime := mocks.NewMockContainerRuntime(t) | |
| 147 | + | svc := NewService(runtime, nil, nil, nil, Config{NetworkPrefix: "gordon"}) | |
| 148 | + | ctx := testContext() | |
| 149 | + | ||
| 150 | + | runtime.EXPECT().ListNetworks(mock.Anything).Return([]*domain.NetworkInfo{ | |
| 151 | + | {Name: "gordon-app"}, | |
| 152 | + | {Name: "bridge"}, | |
| 153 | + | {Name: "gordon-shared"}, | |
| 154 | + | }, nil) | |
| 155 | + | ||
| 156 | + | networks, err := svc.ListNetworks(ctx) | |
| 157 | + | ||
| 158 | + | assert.NoError(t, err) | |
| 159 | + | assert.Len(t, networks, 2) | |
| 160 | + | assert.Equal(t, "gordon-app", networks[0].Name) | |
| 161 | + | assert.Equal(t, "gordon-shared", networks[1].Name) | |
| 162 | + | } | |
| @@ -0,0 +1,162 @@ | |||||
| 1 | + | package container | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "testing" | |||
| 5 | + | ||||
| 6 | + | "github.com/stretchr/testify/assert" | |||
| 7 | + | "github.com/stretchr/testify/mock" | |||
| 8 | + | ||||
| 9 | + | "gordon/internal/boundaries/out/mocks" | |||
| 10 | + | "gordon/internal/domain" | |||
| 11 | + | ) | |||
| 12 | + | ||||
| 13 | + | func TestService_ListRoutesWithDetails(t *testing.T) { | |||
| 14 | + | runtime := mocks.NewMockContainerRuntime(t) | |||
| 15 | + | svc := NewService(runtime, nil, nil, nil, Config{NetworkPrefix: "gordon"}) | |||
| 16 | + | ctx := testContext() | |||
| 17 | + | ||||
| 18 | + | svc.containers["app.example.com"] = &domain.Container{ | |||
| 19 | + | ID: "container-1", | |||
| 20 | + | Image: "registry/app:latest", | |||
| 21 | + | Status: "running", | |||
| 22 | + | Labels: map[string]string{ | |||
| 23 | + | domain.LabelImage: "app:latest", | |||
| 24 | + | }, | |||
| 25 | + | } | |||
| 26 | + | ||||
| 27 | + | runtime.EXPECT().GetContainerNetwork(mock.Anything, "container-1").Return("gordon-app-example-com", nil) | |||
| 28 | + | runtime.EXPECT().ListContainers(mock.Anything, true).Return([]*domain.Container{ | |||
| 29 | + | { | |||
| 30 | + | ID: "attach-1", | |||
| 31 | + | Name: "gordon-app-example-com-postgres", | |||
| 32 | + | Image: "postgres:15", | |||
| 33 | + | Status: "running", | |||
| 34 | + | Labels: map[string]string{ | |||
| 35 | + | domain.LabelAttachment: "true", | |||
| 36 | + | domain.LabelAttachedTo: "app.example.com", | |||
| 37 | + | domain.LabelImage: "postgres:15", | |||
| 38 | + | }, | |||
| 39 | + | }, | |||
| 40 | + | { | |||
| 41 | + | ID: "attach-2", | |||
| 42 | + | Name: "gordon-other-redis", | |||
| 43 | + | Image: "redis:7", | |||
| 44 | + | Status: "running", | |||
| 45 | + | Labels: map[string]string{ | |||
| 46 | + | domain.LabelAttachment: "true", | |||
| 47 | + | domain.LabelAttachedTo: "other.example.com", | |||
| 48 | + | domain.LabelImage: "redis:7", | |||
| 49 | + | }, | |||
| 50 | + | }, | |||
| 51 | + | { | |||
| 52 | + | ID: "other", | |||
| 53 | + | Name: "gordon-other", | |||
| 54 | + | Image: "busybox", | |||
| 55 | + | Status: "running", | |||
| 56 | + | Labels: map[string]string{ | |||
| 57 | + | domain.LabelAttachment: "false", | |||
| 58 | + | }, | |||
| 59 | + | }, | |||
| 60 | + | }, nil) | |||
| 61 | + | ||||
| 62 | + | results := svc.ListRoutesWithDetails(ctx) | |||
| 63 | + | ||||
| 64 | + | assert.Len(t, results, 1) | |||
| 65 | + | assert.Equal(t, "app.example.com", results[0].Domain) | |||
| 66 | + | assert.Equal(t, "app:latest", results[0].Image) | |||
| 67 | + | assert.Equal(t, "container-1", results[0].ContainerID) | |||
| 68 | + | assert.Equal(t, "running", results[0].ContainerStatus) | |||
| 69 | + | assert.Equal(t, "gordon-app-example-com", results[0].Network) | |||
| 70 | + | if assert.Len(t, results[0].Attachments, 1) { | |||
| 71 | + | assert.Equal(t, "postgres", results[0].Attachments[0].Name) | |||
| 72 | + | assert.Equal(t, "postgres:15", results[0].Attachments[0].Image) | |||
| 73 | + | assert.Equal(t, "attach-1", results[0].Attachments[0].ContainerID) | |||
| 74 | + | } | |||
| 75 | + | } | |||
| 76 | + | ||||
| 77 | + | func TestService_ListRoutesWithDetails_Empty(t *testing.T) { | |||
| 78 | + | runtime := mocks.NewMockContainerRuntime(t) | |||
| 79 | + | svc := NewService(runtime, nil, nil, nil, Config{NetworkPrefix: "gordon"}) | |||
| 80 | + | ctx := testContext() | |||
| 81 | + | ||||
| 82 | + | results := svc.ListRoutesWithDetails(ctx) | |||
| 83 | + | ||||
| 84 | + | assert.Empty(t, results) | |||
| 85 | + | } | |||
| 86 | + | ||||
| 87 | + | func TestService_ListRoutesWithDetails_NetworkError(t *testing.T) { | |||
| 88 | + | runtime := mocks.NewMockContainerRuntime(t) | |||
| 89 | + | svc := NewService(runtime, nil, nil, nil, Config{NetworkPrefix: "gordon"}) | |||
| 90 | + | ctx := testContext() | |||
| 91 | + | ||||
| 92 | + | svc.containers["app.example.com"] = &domain.Container{ | |||
| 93 | + | ID: "container-1", | |||
| 94 | + | Image: "app:latest", | |||
| 95 | + | Status: "running", | |||
| 96 | + | } | |||
| 97 | + | ||||
| 98 | + | runtime.EXPECT().GetContainerNetwork(mock.Anything, "container-1").Return("", assert.AnError) | |||
| 99 | + | runtime.EXPECT().ListContainers(mock.Anything, true).Return([]*domain.Container{}, nil) | |||
| 100 | + | ||||
| 101 | + | results := svc.ListRoutesWithDetails(ctx) | |||
| 102 | + | ||||
| 103 | + | assert.Len(t, results, 1) | |||
| 104 | + | assert.Equal(t, "", results[0].Network) | |||
| 105 | + | } | |||
| 106 | + | ||||
| 107 | + | func TestService_ListAttachments(t *testing.T) { | |||
| 108 | + | runtime := mocks.NewMockContainerRuntime(t) | |||
| 109 | + | svc := NewService(runtime, nil, nil, nil, Config{}) | |||
| 110 | + | ctx := testContext() | |||
| 111 | + | ||||
| 112 | + | runtime.EXPECT().ListContainers(mock.Anything, true).Return([]*domain.Container{ | |||
| 113 | + | { | |||
| 114 | + | ID: "attach-1", | |||
| 115 | + | Name: "gordon-app-example-com-postgres", | |||
| 116 | + | Image: "postgres:15", | |||
| 117 | + | Status: "running", | |||
| 118 | + | Labels: map[string]string{ | |||
| 119 | + | domain.LabelAttachment: "true", | |||
| 120 | + | domain.LabelAttachedTo: "app.example.com", | |||
| 121 | + | domain.LabelImage: "postgres:15", | |||
| 122 | + | }, | |||
| 123 | + | }, | |||
| 124 | + | { | |||
| 125 | + | ID: "attach-2", | |||
| 126 | + | Name: "gordon-other-redis", | |||
| 127 | + | Image: "redis:7", | |||
| 128 | + | Status: "running", | |||
| 129 | + | Labels: map[string]string{ | |||
| 130 | + | domain.LabelAttachment: "true", | |||
| 131 | + | domain.LabelAttachedTo: "other.example.com", | |||
| 132 | + | domain.LabelImage: "redis:7", | |||
| 133 | + | }, | |||
| 134 | + | }, | |||
| 135 | + | }, nil) | |||
| 136 | + | ||||
| 137 | + | attachments := svc.ListAttachments(ctx, "app.example.com") | |||
| 138 | + | ||||
| 139 | + | assert.Len(t, attachments, 1) | |||
| 140 | + | assert.Equal(t, "postgres", attachments[0].Name) | |||
| 141 | + | assert.Equal(t, "postgres:15", attachments[0].Image) | |||
| 142 | + | assert.Equal(t, "attach-1", attachments[0].ContainerID) | |||
| 143 | + | } | |||
| 144 | + | ||||
| 145 | + | func TestService_ListNetworks(t *testing.T) { | |||
| 146 | + | runtime := mocks.NewMockContainerRuntime(t) | |||
| 147 | + | svc := NewService(runtime, nil, nil, nil, Config{NetworkPrefix: "gordon"}) | |||
| 148 | + | ctx := testContext() | |||
| 149 | + | ||||
| 150 | + | runtime.EXPECT().ListNetworks(mock.Anything).Return([]*domain.NetworkInfo{ | |||
| 151 | + | {Name: "gordon-app"}, | |||
| 152 | + | {Name: "bridge"}, | |||
| 153 | + | {Name: "gordon-shared"}, | |||
| 154 | + | }, nil) | |||
| 155 | + | ||||
| 156 | + | networks, err := svc.ListNetworks(ctx) | |||
| 157 | + | ||||
| 158 | + | assert.NoError(t, err) | |||
| 159 | + | assert.Len(t, networks, 2) | |||
| 160 | + | assert.Equal(t, "gordon-app", networks[0].Name) | |||
| 161 | + | assert.Equal(t, "gordon-shared", networks[1].Name) | |||
| 162 | + | } | |||
M internal/usecase/container/service.go +100 -0
| @@ -316,6 +316,71 @@ func (s *Service) List(_ context.Context) map[string]*domain.Container { | |||
| 316 | 316 | return result | |
| 317 | 317 | } | |
| 318 | 318 | ||
| 319 | + | // ListRoutesWithDetails returns routes with network and attachment info. | |
| 320 | + | // Note: Container map is copied under lock, then external calls are made without lock. | |
| 321 | + | // If containers are removed between copy and runtime calls, errors are handled gracefully | |
| 322 | + | // (network becomes empty string). This trade-off avoids holding locks during I/O. | |
| 323 | + | func (s *Service) ListRoutesWithDetails(ctx context.Context) []domain.RouteInfo { | |
| 324 | + | s.mu.RLock() | |
| 325 | + | containers := make(map[string]*domain.Container, len(s.containers)) | |
| 326 | + | maps.Copy(containers, s.containers) | |
| 327 | + | s.mu.RUnlock() | |
| 328 | + | ||
| 329 | + | results := make([]domain.RouteInfo, 0, len(containers)) | |
| 330 | + | for domainName, container := range containers { | |
| 331 | + | network := "" | |
| 332 | + | image := "" | |
| 333 | + | containerID := "" | |
| 334 | + | status := "" | |
| 335 | + | if container != nil { | |
| 336 | + | containerID = container.ID | |
| 337 | + | status = container.Status | |
| 338 | + | image = container.Image | |
| 339 | + | if container.Labels != nil { | |
| 340 | + | if labelImage, ok := container.Labels[domain.LabelImage]; ok && labelImage != "" { | |
| 341 | + | image = labelImage | |
| 342 | + | } | |
| 343 | + | } | |
| 344 | + | if networkName, err := s.runtime.GetContainerNetwork(ctx, container.ID); err == nil { | |
| 345 | + | network = networkName | |
| 346 | + | } | |
| 347 | + | } | |
| 348 | + | ||
| 349 | + | results = append(results, domain.RouteInfo{ | |
| 350 | + | Domain: domainName, | |
| 351 | + | Image: image, | |
| 352 | + | ContainerID: containerID, | |
| 353 | + | ContainerStatus: status, | |
| 354 | + | Network: network, | |
| 355 | + | Attachments: s.getAttachmentsForDomain(ctx, domainName), | |
| 356 | + | }) | |
| 357 | + | } | |
| 358 | + | ||
| 359 | + | return results | |
| 360 | + | } | |
| 361 | + | ||
| 362 | + | // ListAttachments returns attachments for a domain. | |
| 363 | + | func (s *Service) ListAttachments(ctx context.Context, domainName string) []domain.Attachment { | |
| 364 | + | return s.getAttachmentsForDomain(ctx, domainName) | |
| 365 | + | } | |
| 366 | + | ||
| 367 | + | // ListNetworks returns Gordon-managed networks. | |
| 368 | + | func (s *Service) ListNetworks(ctx context.Context) ([]*domain.NetworkInfo, error) { | |
| 369 | + | networks, err := s.runtime.ListNetworks(ctx) | |
| 370 | + | if err != nil { | |
| 371 | + | return nil, err | |
| 372 | + | } | |
| 373 | + | ||
| 374 | + | var filtered []*domain.NetworkInfo | |
| 375 | + | for _, network := range networks { | |
| 376 | + | if strings.HasPrefix(network.Name, s.config.NetworkPrefix+"-") { | |
| 377 | + | filtered = append(filtered, network) | |
| 378 | + | } | |
| 379 | + | } | |
| 380 | + | ||
| 381 | + | return filtered, nil | |
| 382 | + | } | |
| 383 | + | ||
| 319 | 384 | // HealthCheck performs health checks on all containers. | |
| 320 | 385 | func (s *Service) HealthCheck(ctx context.Context) map[string]bool { | |
| 321 | 386 | ctx = zerowrap.CtxWithFields(ctx, map[string]any{ | |
| @@ -869,6 +934,41 @@ func (s *Service) deployAttachments(ctx context.Context, domainName, networkName | |||
| 869 | 934 | return nil | |
| 870 | 935 | } | |
| 871 | 936 | ||
| 937 | + | func (s *Service) getAttachmentsForDomain(ctx context.Context, domainName string) []domain.Attachment { | |
| 938 | + | containers, err := s.runtime.ListContainers(ctx, true) | |
| 939 | + | if err != nil { | |
| 940 | + | return nil | |
| 941 | + | } | |
| 942 | + | ||
| 943 | + | attachments := make([]domain.Attachment, 0) | |
| 944 | + | for _, container := range containers { | |
| 945 | + | if container.Labels == nil { | |
| 946 | + | continue | |
| 947 | + | } | |
| 948 | + | if container.Labels[domain.LabelAttachment] != "true" { | |
| 949 | + | continue | |
| 950 | + | } | |
| 951 | + | if container.Labels[domain.LabelAttachedTo] != domainName { | |
| 952 | + | continue | |
| 953 | + | } | |
| 954 | + | image := container.Image | |
| 955 | + | serviceName := container.Name | |
| 956 | + | if labelImage, ok := container.Labels[domain.LabelImage]; ok && labelImage != "" { | |
| 957 | + | image = labelImage | |
| 958 | + | serviceName = extractServiceName(labelImage) | |
| 959 | + | } | |
| 960 | + | attachment := domain.Attachment{ | |
| 961 | + | Name: serviceName, | |
| 962 | + | Image: image, | |
| 963 | + | ContainerID: container.ID, | |
| 964 | + | Status: container.Status, | |
| 965 | + | } | |
| 966 | + | attachments = append(attachments, attachment) | |
| 967 | + | } | |
| 968 | + | ||
| 969 | + | return attachments | |
| 970 | + | } | |
| 971 | + | ||
| 872 | 972 | func (s *Service) deployAttachedService(ctx context.Context, ownerDomain, serviceImage, networkName string) error { | |
| 873 | 973 | log := zerowrap.FromCtx(ctx) | |
| 874 | 974 | ||
| @@ -316,6 +316,71 @@ func (s *Service) List(_ context.Context) map[string]*domain.Container { | |||||
| 316 | return result | 316 | return result | ||
| 317 | } | 317 | } | ||
| 318 | 318 | ||||
| 319 | + | // ListRoutesWithDetails returns routes with network and attachment info. | |||
| 320 | + | // Note: Container map is copied under lock, then external calls are made without lock. | |||
| 321 | + | // If containers are removed between copy and runtime calls, errors are handled gracefully | |||
| 322 | + | // (network becomes empty string). This trade-off avoids holding locks during I/O. | |||
| 323 | + | func (s *Service) ListRoutesWithDetails(ctx context.Context) []domain.RouteInfo { | |||
| 324 | + | s.mu.RLock() | |||
| 325 | + | containers := make(map[string]*domain.Container, len(s.containers)) | |||
| 326 | + | maps.Copy(containers, s.containers) | |||
| 327 | + | s.mu.RUnlock() | |||
| 328 | + | ||||
| 329 | + | results := make([]domain.RouteInfo, 0, len(containers)) | |||
| 330 | + | for domainName, container := range containers { | |||
| 331 | + | network := "" | |||
| 332 | + | image := "" | |||
| 333 | + | containerID := "" | |||
| 334 | + | status := "" | |||
| 335 | + | if container != nil { | |||
| 336 | + | containerID = container.ID | |||
| 337 | + | status = container.Status | |||
| 338 | + | image = container.Image | |||
| 339 | + | if container.Labels != nil { | |||
| 340 | + | if labelImage, ok := container.Labels[domain.LabelImage]; ok && labelImage != "" { | |||
| 341 | + | image = labelImage | |||
| 342 | + | } | |||
| 343 | + | } | |||
| 344 | + | if networkName, err := s.runtime.GetContainerNetwork(ctx, container.ID); err == nil { | |||
| 345 | + | network = networkName | |||
| 346 | + | } | |||
| 347 | + | } | |||
| 348 | + | ||||
| 349 | + | results = append(results, domain.RouteInfo{ | |||
| 350 | + | Domain: domainName, | |||
| 351 | + | Image: image, | |||
| 352 | + | ContainerID: containerID, | |||
| 353 | + | ContainerStatus: status, | |||
| 354 | + | Network: network, | |||
| 355 | + | Attachments: s.getAttachmentsForDomain(ctx, domainName), | |||
| 356 | + | }) | |||
| 357 | + | } | |||
| 358 | + | ||||
| 359 | + | return results | |||
| 360 | + | } | |||
| 361 | + | ||||
| 362 | + | // ListAttachments returns attachments for a domain. | |||
| 363 | + | func (s *Service) ListAttachments(ctx context.Context, domainName string) []domain.Attachment { | |||
| 364 | + | return s.getAttachmentsForDomain(ctx, domainName) | |||
| 365 | + | } | |||
| 366 | + | ||||
| 367 | + | // ListNetworks returns Gordon-managed networks. | |||
| 368 | + | func (s *Service) ListNetworks(ctx context.Context) ([]*domain.NetworkInfo, error) { | |||
| 369 | + | networks, err := s.runtime.ListNetworks(ctx) | |||
| 370 | + | if err != nil { | |||
| 371 | + | return nil, err | |||
| 372 | + | } | |||
| 373 | + | ||||
| 374 | + | var filtered []*domain.NetworkInfo | |||
| 375 | + | for _, network := range networks { | |||
| 376 | + | if strings.HasPrefix(network.Name, s.config.NetworkPrefix+"-") { | |||
| 377 | + | filtered = append(filtered, network) | |||
| 378 | + | } | |||
| 379 | + | } | |||
| 380 | + | ||||
| 381 | + | return filtered, nil | |||
| 382 | + | } | |||
| 383 | + | ||||
| 319 | // HealthCheck performs health checks on all containers. | 384 | // HealthCheck performs health checks on all containers. | ||
| 320 | func (s *Service) HealthCheck(ctx context.Context) map[string]bool { | 385 | func (s *Service) HealthCheck(ctx context.Context) map[string]bool { | ||
| 321 | ctx = zerowrap.CtxWithFields(ctx, map[string]any{ | 386 | ctx = zerowrap.CtxWithFields(ctx, map[string]any{ | ||
| @@ -869,6 +934,41 @@ func (s *Service) deployAttachments(ctx context.Context, domainName, networkName | |||||
| 869 | return nil | 934 | return nil | ||
| 870 | } | 935 | } | ||
| 871 | 936 | ||||
| 937 | + | func (s *Service) getAttachmentsForDomain(ctx context.Context, domainName string) []domain.Attachment { | |||
| 938 | + | containers, err := s.runtime.ListContainers(ctx, true) | |||
| 939 | + | if err != nil { | |||
| 940 | + | return nil | |||
| 941 | + | } | |||
| 942 | + | ||||
| 943 | + | attachments := make([]domain.Attachment, 0) | |||
| 944 | + | for _, container := range containers { | |||
| 945 | + | if container.Labels == nil { | |||
| 946 | + | continue | |||
| 947 | + | } | |||
| 948 | + | if container.Labels[domain.LabelAttachment] != "true" { | |||
| 949 | + | continue | |||
| 950 | + | } | |||
| 951 | + | if container.Labels[domain.LabelAttachedTo] != domainName { | |||
| 952 | + | continue | |||
| 953 | + | } | |||
| 954 | + | image := container.Image | |||
| 955 | + | serviceName := container.Name | |||
| 956 | + | if labelImage, ok := container.Labels[domain.LabelImage]; ok && labelImage != "" { | |||
| 957 | + | image = labelImage | |||
| 958 | + | serviceName = extractServiceName(labelImage) | |||
| 959 | + | } | |||
| 960 | + | attachment := domain.Attachment{ | |||
| 961 | + | Name: serviceName, | |||
| 962 | + | Image: image, | |||
| 963 | + | ContainerID: container.ID, | |||
| 964 | + | Status: container.Status, | |||
| 965 | + | } | |||
| 966 | + | attachments = append(attachments, attachment) | |||
| 967 | + | } | |||
| 968 | + | ||||
| 969 | + | return attachments | |||
| 970 | + | } | |||
| 971 | + | ||||
| 872 | func (s *Service) deployAttachedService(ctx context.Context, ownerDomain, serviceImage, networkName string) error { | 972 | func (s *Service) deployAttachedService(ctx context.Context, ownerDomain, serviceImage, networkName string) error { | ||
| 873 | log := zerowrap.FromCtx(ctx) | 973 | log := zerowrap.FromCtx(ctx) | ||
| 874 | 974 | ||||
M internal/usecase/container/service_test.go +10 -9
| @@ -4,6 +4,7 @@ import ( | |||
| 4 | 4 | "context" | |
| 5 | 5 | "errors" | |
| 6 | 6 | "testing" | |
| 7 | + | "time" | |
| 7 | 8 | ||
| 8 | 9 | "github.com/bnema/zerowrap" | |
| 9 | 10 | "github.com/stretchr/testify/assert" | |
| @@ -25,7 +26,7 @@ func TestService_Deploy_Success(t *testing.T) { | |||
| 25 | 26 | config := Config{ | |
| 26 | 27 | NetworkIsolation: false, | |
| 27 | 28 | VolumeAutoCreate: false, | |
| 28 | - | ReadinessDelay: 0, // No delay for tests | |
| 29 | + | ReadinessDelay: time.Millisecond, // Minimal delay for tests | |
| 29 | 30 | } | |
| 30 | 31 | ||
| 31 | 32 | svc := NewService(runtime, envLoader, eventBus, nil, config) | |
| @@ -124,7 +125,7 @@ func TestService_Deploy_ReplacesExistingContainer(t *testing.T) { | |||
| 124 | 125 | eventBus := mocks.NewMockEventPublisher(t) | |
| 125 | 126 | ||
| 126 | 127 | config := Config{ | |
| 127 | - | ReadinessDelay: 0, // No delay for tests | |
| 128 | + | ReadinessDelay: time.Millisecond, // Minimal delay for tests | |
| 128 | 129 | } | |
| 129 | 130 | svc := NewService(runtime, envLoader, eventBus, nil, config) | |
| 130 | 131 | ctx := testContext() | |
| @@ -199,7 +200,7 @@ func TestService_Deploy_WithNetworkIsolation(t *testing.T) { | |||
| 199 | 200 | config := Config{ | |
| 200 | 201 | NetworkIsolation: true, | |
| 201 | 202 | NetworkPrefix: "gordon", | |
| 202 | - | ReadinessDelay: 0, // No delay for tests | |
| 203 | + | ReadinessDelay: time.Millisecond, // Minimal delay for tests | |
| 203 | 204 | } | |
| 204 | 205 | svc := NewService(runtime, envLoader, eventBus, nil, config) | |
| 205 | 206 | ctx := testContext() | |
| @@ -250,7 +251,7 @@ func TestService_Deploy_WithVolumeAutoCreate(t *testing.T) { | |||
| 250 | 251 | config := Config{ | |
| 251 | 252 | VolumeAutoCreate: true, | |
| 252 | 253 | VolumePrefix: "gordon", | |
| 253 | - | ReadinessDelay: 0, // No delay for tests | |
| 254 | + | ReadinessDelay: time.Millisecond, // Minimal delay for tests | |
| 254 | 255 | } | |
| 255 | 256 | svc := NewService(runtime, envLoader, eventBus, nil, config) | |
| 256 | 257 | ctx := testContext() | |
| @@ -723,7 +724,7 @@ func TestService_Deploy_InternalDeployForcesPull(t *testing.T) { | |||
| 723 | 724 | RegistryPort: 5000, | |
| 724 | 725 | InternalRegistryUsername: "internal", | |
| 725 | 726 | InternalRegistryPassword: "secret", | |
| 726 | - | ReadinessDelay: 0, | |
| 727 | + | ReadinessDelay: time.Millisecond, | |
| 727 | 728 | } | |
| 728 | 729 | ||
| 729 | 730 | svc := NewService(runtime, envLoader, eventBus, nil, config) | |
| @@ -791,7 +792,7 @@ func TestService_AutoStart_StartsNewContainers(t *testing.T) { | |||
| 791 | 792 | eventBus := mocks.NewMockEventPublisher(t) | |
| 792 | 793 | ||
| 793 | 794 | config := Config{ | |
| 794 | - | ReadinessDelay: 0, | |
| 795 | + | ReadinessDelay: time.Millisecond, | |
| 795 | 796 | } | |
| 796 | 797 | svc := NewService(runtime, envLoader, eventBus, nil, config) | |
| 797 | 798 | ctx := testContext() | |
| @@ -834,7 +835,7 @@ func TestService_AutoStart_SkipsExistingContainers(t *testing.T) { | |||
| 834 | 835 | eventBus := mocks.NewMockEventPublisher(t) | |
| 835 | 836 | ||
| 836 | 837 | config := Config{ | |
| 837 | - | ReadinessDelay: 0, | |
| 838 | + | ReadinessDelay: time.Millisecond, | |
| 838 | 839 | } | |
| 839 | 840 | svc := NewService(runtime, envLoader, eventBus, nil, config) | |
| 840 | 841 | ctx := testContext() | |
| @@ -906,7 +907,7 @@ func TestService_Deploy_OrphanCleanupSkipsTrackedContainer(t *testing.T) { | |||
| 906 | 907 | eventBus := mocks.NewMockEventPublisher(t) | |
| 907 | 908 | ||
| 908 | 909 | config := Config{ | |
| 909 | - | ReadinessDelay: 0, // No delay for tests | |
| 910 | + | ReadinessDelay: time.Millisecond, // Minimal delay for tests | |
| 910 | 911 | } | |
| 911 | 912 | svc := NewService(runtime, envLoader, eventBus, nil, config) | |
| 912 | 913 | ctx := testContext() | |
| @@ -990,7 +991,7 @@ func TestService_Deploy_OrphanCleanupRemovesTrueOrphans(t *testing.T) { | |||
| 990 | 991 | eventBus := mocks.NewMockEventPublisher(t) | |
| 991 | 992 | ||
| 992 | 993 | config := Config{ | |
| 993 | - | ReadinessDelay: 0, // No delay for tests | |
| 994 | + | ReadinessDelay: time.Millisecond, // Minimal delay for tests | |
| 994 | 995 | } | |
| 995 | 996 | svc := NewService(runtime, envLoader, eventBus, nil, config) | |
| 996 | 997 | ctx := testContext() | |
| @@ -4,6 +4,7 @@ import ( | |||||
| 4 | "context" | 4 | "context" | ||
| 5 | "errors" | 5 | "errors" | ||
| 6 | "testing" | 6 | "testing" | ||
| 7 | + | "time" | |||
| 7 | 8 | ||||
| 8 | "github.com/bnema/zerowrap" | 9 | "github.com/bnema/zerowrap" | ||
| 9 | "github.com/stretchr/testify/assert" | 10 | "github.com/stretchr/testify/assert" | ||
| @@ -25,7 +26,7 @@ func TestService_Deploy_Success(t *testing.T) { | |||||
| 25 | config := Config{ | 26 | config := Config{ | ||
| 26 | NetworkIsolation: false, | 27 | NetworkIsolation: false, | ||
| 27 | VolumeAutoCreate: false, | 28 | VolumeAutoCreate: false, | ||
| 28 | - | ReadinessDelay: 0, // No delay for tests | 29 | + | ReadinessDelay: time.Millisecond, // Minimal delay for tests |
| 29 | } | 30 | } | ||
| 30 | 31 | ||||
| 31 | svc := NewService(runtime, envLoader, eventBus, nil, config) | 32 | svc := NewService(runtime, envLoader, eventBus, nil, config) | ||
| @@ -124,7 +125,7 @@ func TestService_Deploy_ReplacesExistingContainer(t *testing.T) { | |||||
| 124 | eventBus := mocks.NewMockEventPublisher(t) | 125 | eventBus := mocks.NewMockEventPublisher(t) | ||
| 125 | 126 | ||||
| 126 | config := Config{ | 127 | config := Config{ | ||
| 127 | - | ReadinessDelay: 0, // No delay for tests | 128 | + | ReadinessDelay: time.Millisecond, // Minimal delay for tests |
| 128 | } | 129 | } | ||
| 129 | svc := NewService(runtime, envLoader, eventBus, nil, config) | 130 | svc := NewService(runtime, envLoader, eventBus, nil, config) | ||
| 130 | ctx := testContext() | 131 | ctx := testContext() | ||
| @@ -199,7 +200,7 @@ func TestService_Deploy_WithNetworkIsolation(t *testing.T) { | |||||
| 199 | config := Config{ | 200 | config := Config{ | ||
| 200 | NetworkIsolation: true, | 201 | NetworkIsolation: true, | ||
| 201 | NetworkPrefix: "gordon", | 202 | NetworkPrefix: "gordon", | ||
| 202 | - | ReadinessDelay: 0, // No delay for tests | 203 | + | ReadinessDelay: time.Millisecond, // Minimal delay for tests |
| 203 | } | 204 | } | ||
| 204 | svc := NewService(runtime, envLoader, eventBus, nil, config) | 205 | svc := NewService(runtime, envLoader, eventBus, nil, config) | ||
| 205 | ctx := testContext() | 206 | ctx := testContext() | ||
| @@ -250,7 +251,7 @@ func TestService_Deploy_WithVolumeAutoCreate(t *testing.T) { | |||||
| 250 | config := Config{ | 251 | config := Config{ | ||
| 251 | VolumeAutoCreate: true, | 252 | VolumeAutoCreate: true, | ||
| 252 | VolumePrefix: "gordon", | 253 | VolumePrefix: "gordon", | ||
| 253 | - | ReadinessDelay: 0, // No delay for tests | 254 | + | ReadinessDelay: time.Millisecond, // Minimal delay for tests |
| 254 | } | 255 | } | ||
| 255 | svc := NewService(runtime, envLoader, eventBus, nil, config) | 256 | svc := NewService(runtime, envLoader, eventBus, nil, config) | ||
| 256 | ctx := testContext() | 257 | ctx := testContext() | ||
| @@ -723,7 +724,7 @@ func TestService_Deploy_InternalDeployForcesPull(t *testing.T) { | |||||
| 723 | RegistryPort: 5000, | 724 | RegistryPort: 5000, | ||
| 724 | InternalRegistryUsername: "internal", | 725 | InternalRegistryUsername: "internal", | ||
| 725 | InternalRegistryPassword: "secret", | 726 | InternalRegistryPassword: "secret", | ||
| 726 | - | ReadinessDelay: 0, | 727 | + | ReadinessDelay: time.Millisecond, |
| 727 | } | 728 | } | ||
| 728 | 729 | ||||
| 729 | svc := NewService(runtime, envLoader, eventBus, nil, config) | 730 | svc := NewService(runtime, envLoader, eventBus, nil, config) | ||
| @@ -791,7 +792,7 @@ func TestService_AutoStart_StartsNewContainers(t *testing.T) { | |||||
| 791 | eventBus := mocks.NewMockEventPublisher(t) | 792 | eventBus := mocks.NewMockEventPublisher(t) | ||
| 792 | 793 | ||||
| 793 | config := Config{ | 794 | config := Config{ | ||
| 794 | - | ReadinessDelay: 0, | 795 | + | ReadinessDelay: time.Millisecond, |
| 795 | } | 796 | } | ||
| 796 | svc := NewService(runtime, envLoader, eventBus, nil, config) | 797 | svc := NewService(runtime, envLoader, eventBus, nil, config) | ||
| 797 | ctx := testContext() | 798 | ctx := testContext() | ||
| @@ -834,7 +835,7 @@ func TestService_AutoStart_SkipsExistingContainers(t *testing.T) { | |||||
| 834 | eventBus := mocks.NewMockEventPublisher(t) | 835 | eventBus := mocks.NewMockEventPublisher(t) | ||
| 835 | 836 | ||||
| 836 | config := Config{ | 837 | config := Config{ | ||
| 837 | - | ReadinessDelay: 0, | 838 | + | ReadinessDelay: time.Millisecond, |
| 838 | } | 839 | } | ||
| 839 | svc := NewService(runtime, envLoader, eventBus, nil, config) | 840 | svc := NewService(runtime, envLoader, eventBus, nil, config) | ||
| 840 | ctx := testContext() | 841 | ctx := testContext() | ||
| @@ -906,7 +907,7 @@ func TestService_Deploy_OrphanCleanupSkipsTrackedContainer(t *testing.T) { | |||||
| 906 | eventBus := mocks.NewMockEventPublisher(t) | 907 | eventBus := mocks.NewMockEventPublisher(t) | ||
| 907 | 908 | ||||
| 908 | config := Config{ | 909 | config := Config{ | ||
| 909 | - | ReadinessDelay: 0, // No delay for tests | 910 | + | ReadinessDelay: time.Millisecond, // Minimal delay for tests |
| 910 | } | 911 | } | ||
| 911 | svc := NewService(runtime, envLoader, eventBus, nil, config) | 912 | svc := NewService(runtime, envLoader, eventBus, nil, config) | ||
| 912 | ctx := testContext() | 913 | ctx := testContext() | ||
| @@ -990,7 +991,7 @@ func TestService_Deploy_OrphanCleanupRemovesTrueOrphans(t *testing.T) { | |||||
| 990 | eventBus := mocks.NewMockEventPublisher(t) | 991 | eventBus := mocks.NewMockEventPublisher(t) | ||
| 991 | 992 | ||||
| 992 | config := Config{ | 993 | config := Config{ | ||
| 993 | - | ReadinessDelay: 0, // No delay for tests | 994 | + | ReadinessDelay: time.Millisecond, // Minimal delay for tests |
| 994 | } | 995 | } | ||
| 995 | svc := NewService(runtime, envLoader, eventBus, nil, config) | 996 | svc := NewService(runtime, envLoader, eventBus, nil, config) | ||
| 996 | ctx := testContext() | 997 | ctx := testContext() | ||