feat: expose route network and attachments
- commit
- 9a89e96cfb31f679d340fa38e148fc8d66cd4c77
- author
- brice · · 2026-01-17 11:28 UTC
- parent
- d96ffdd
- signature
- unsigned
10 files · +611 -14
M internal/adapters/in/cli/remote/client.go +68 -0
| @@ -119,6 +119,22 @@ func parseResponse(resp *http.Response, target any) error { | |||
| 119 | 119 | ||
| 120 | 120 | // Routes API | |
| 121 | 121 | ||
| 122 | + | type RouteInfo struct { | |
| 123 | + | Domain string `json:"domain"` | |
| 124 | + | Image string `json:"image"` | |
| 125 | + | ContainerID string `json:"container_id"` | |
| 126 | + | ContainerStatus string `json:"container_status"` | |
| 127 | + | Network string `json:"network"` | |
| 128 | + | Attachments []Attachment `json:"attachments"` | |
| 129 | + | } | |
| 130 | + | ||
| 131 | + | type Attachment struct { | |
| 132 | + | Name string `json:"name"` | |
| 133 | + | Image string `json:"image"` | |
| 134 | + | ContainerID string `json:"container_id"` | |
| 135 | + | Status string `json:"status"` | |
| 136 | + | } | |
| 137 | + | ||
| 122 | 138 | // ListRoutes returns all configured routes. | |
| 123 | 139 | func (c *Client) ListRoutes(ctx context.Context) ([]domain.Route, error) { | |
| 124 | 140 | resp, err := c.request(ctx, http.MethodGet, "/routes", nil) | |
| @@ -136,6 +152,58 @@ func (c *Client) ListRoutes(ctx context.Context) ([]domain.Route, error) { | |||
| 136 | 152 | return result.Routes, nil | |
| 137 | 153 | } | |
| 138 | 154 | ||
| 155 | + | // ListRoutesWithDetails returns routes with network and attachment info. | |
| 156 | + | func (c *Client) ListRoutesWithDetails(ctx context.Context) ([]RouteInfo, error) { | |
| 157 | + | resp, err := c.request(ctx, http.MethodGet, "/routes?detailed=true", nil) | |
| 158 | + | if err != nil { | |
| 159 | + | return nil, err | |
| 160 | + | } | |
| 161 | + | ||
| 162 | + | var result struct { | |
| 163 | + | Routes []RouteInfo `json:"routes"` | |
| 164 | + | } | |
| 165 | + | if err := parseResponse(resp, &result); err != nil { | |
| 166 | + | return nil, err | |
| 167 | + | } | |
| 168 | + | ||
| 169 | + | return result.Routes, nil | |
| 170 | + | } | |
| 171 | + | ||
| 172 | + | // ListNetworks returns Gordon-managed networks. | |
| 173 | + | func (c *Client) ListNetworks(ctx context.Context) ([]*domain.NetworkInfo, error) { | |
| 174 | + | resp, err := c.request(ctx, http.MethodGet, "/networks", nil) | |
| 175 | + | if err != nil { | |
| 176 | + | return nil, err | |
| 177 | + | } | |
| 178 | + | ||
| 179 | + | var result struct { | |
| 180 | + | Networks []*domain.NetworkInfo `json:"networks"` | |
| 181 | + | } | |
| 182 | + | if err := parseResponse(resp, &result); err != nil { | |
| 183 | + | return nil, err | |
| 184 | + | } | |
| 185 | + | ||
| 186 | + | return result.Networks, nil | |
| 187 | + | } | |
| 188 | + | ||
| 189 | + | // ListAttachments returns attachments for a domain. | |
| 190 | + | func (c *Client) ListAttachments(ctx context.Context, routeDomain string) ([]Attachment, error) { | |
| 191 | + | path := "/routes/" + url.PathEscape(routeDomain) + "/attachments" | |
| 192 | + | resp, err := c.request(ctx, http.MethodGet, path, nil) | |
| 193 | + | if err != nil { | |
| 194 | + | return nil, err | |
| 195 | + | } | |
| 196 | + | ||
| 197 | + | var result struct { | |
| 198 | + | Attachments []Attachment `json:"attachments"` | |
| 199 | + | } | |
| 200 | + | if err := parseResponse(resp, &result); err != nil { | |
| 201 | + | return nil, err | |
| 202 | + | } | |
| 203 | + | ||
| 204 | + | return result.Attachments, nil | |
| 205 | + | } | |
| 206 | + | ||
| 139 | 207 | // GetRoute returns a specific route by domain. | |
| 140 | 208 | func (c *Client) GetRoute(ctx context.Context, routeDomain string) (*domain.Route, error) { | |
| 141 | 209 | resp, err := c.request(ctx, http.MethodGet, "/routes/"+url.PathEscape(routeDomain), nil) | |
| @@ -119,6 +119,22 @@ func parseResponse(resp *http.Response, target any) error { | |||||
| 119 | 119 | ||||
| 120 | // Routes API | 120 | // Routes API | ||
| 121 | 121 | ||||
| 122 | + | type RouteInfo struct { | |||
| 123 | + | Domain string `json:"domain"` | |||
| 124 | + | Image string `json:"image"` | |||
| 125 | + | ContainerID string `json:"container_id"` | |||
| 126 | + | ContainerStatus string `json:"container_status"` | |||
| 127 | + | Network string `json:"network"` | |||
| 128 | + | Attachments []Attachment `json:"attachments"` | |||
| 129 | + | } | |||
| 130 | + | ||||
| 131 | + | type Attachment struct { | |||
| 132 | + | Name string `json:"name"` | |||
| 133 | + | Image string `json:"image"` | |||
| 134 | + | ContainerID string `json:"container_id"` | |||
| 135 | + | Status string `json:"status"` | |||
| 136 | + | } | |||
| 137 | + | ||||
| 122 | // ListRoutes returns all configured routes. | 138 | // ListRoutes returns all configured routes. | ||
| 123 | func (c *Client) ListRoutes(ctx context.Context) ([]domain.Route, error) { | 139 | func (c *Client) ListRoutes(ctx context.Context) ([]domain.Route, error) { | ||
| 124 | resp, err := c.request(ctx, http.MethodGet, "/routes", nil) | 140 | resp, err := c.request(ctx, http.MethodGet, "/routes", nil) | ||
| @@ -136,6 +152,58 @@ func (c *Client) ListRoutes(ctx context.Context) ([]domain.Route, error) { | |||||
| 136 | return result.Routes, nil | 152 | return result.Routes, nil | ||
| 137 | } | 153 | } | ||
| 138 | 154 | ||||
| 155 | + | // ListRoutesWithDetails returns routes with network and attachment info. | |||
| 156 | + | func (c *Client) ListRoutesWithDetails(ctx context.Context) ([]RouteInfo, error) { | |||
| 157 | + | resp, err := c.request(ctx, http.MethodGet, "/routes?detailed=true", nil) | |||
| 158 | + | if err != nil { | |||
| 159 | + | return nil, err | |||
| 160 | + | } | |||
| 161 | + | ||||
| 162 | + | var result struct { | |||
| 163 | + | Routes []RouteInfo `json:"routes"` | |||
| 164 | + | } | |||
| 165 | + | if err := parseResponse(resp, &result); err != nil { | |||
| 166 | + | return nil, err | |||
| 167 | + | } | |||
| 168 | + | ||||
| 169 | + | return result.Routes, nil | |||
| 170 | + | } | |||
| 171 | + | ||||
| 172 | + | // ListNetworks returns Gordon-managed networks. | |||
| 173 | + | func (c *Client) ListNetworks(ctx context.Context) ([]*domain.NetworkInfo, error) { | |||
| 174 | + | resp, err := c.request(ctx, http.MethodGet, "/networks", nil) | |||
| 175 | + | if err != nil { | |||
| 176 | + | return nil, err | |||
| 177 | + | } | |||
| 178 | + | ||||
| 179 | + | var result struct { | |||
| 180 | + | Networks []*domain.NetworkInfo `json:"networks"` | |||
| 181 | + | } | |||
| 182 | + | if err := parseResponse(resp, &result); err != nil { | |||
| 183 | + | return nil, err | |||
| 184 | + | } | |||
| 185 | + | ||||
| 186 | + | return result.Networks, nil | |||
| 187 | + | } | |||
| 188 | + | ||||
| 189 | + | // ListAttachments returns attachments for a domain. | |||
| 190 | + | func (c *Client) ListAttachments(ctx context.Context, routeDomain string) ([]Attachment, error) { | |||
| 191 | + | path := "/routes/" + url.PathEscape(routeDomain) + "/attachments" | |||
| 192 | + | resp, err := c.request(ctx, http.MethodGet, path, nil) | |||
| 193 | + | if err != nil { | |||
| 194 | + | return nil, err | |||
| 195 | + | } | |||
| 196 | + | ||||
| 197 | + | var result struct { | |||
| 198 | + | Attachments []Attachment `json:"attachments"` | |||
| 199 | + | } | |||
| 200 | + | if err := parseResponse(resp, &result); err != nil { | |||
| 201 | + | return nil, err | |||
| 202 | + | } | |||
| 203 | + | ||||
| 204 | + | return result.Attachments, nil | |||
| 205 | + | } | |||
| 206 | + | ||||
| 139 | // GetRoute returns a specific route by domain. | 207 | // GetRoute returns a specific route by domain. | ||
| 140 | func (c *Client) GetRoute(ctx context.Context, routeDomain string) (*domain.Route, error) { | 208 | 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) | 209 | resp, err := c.request(ctx, http.MethodGet, "/routes/"+url.PathEscape(routeDomain), nil) | ||
M internal/adapters/in/cli/routes.go +53 -14
| @@ -29,25 +29,44 @@ 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 | 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] + "…" | |
| 35 | + | available := maxLen - len("@sha256:") - len(digest) - 3 // 3 for ellipsis | |
| 36 | + | if available > 0 { | |
| 37 | + | if len(name) > available { | |
| 38 | + | name = name[:available] | |
| 39 | + | } | |
| 40 | + | name = name + "..." | |
| 41 | + | short = fmt.Sprintf("%s@sha256:%s", name, digest) | |
| 38 | 42 | } | |
| 39 | - | short = fmt.Sprintf("%s@sha256:%s…", name, digest) | |
| 40 | 43 | } | |
| 41 | 44 | return short | |
| 42 | 45 | } | |
| 43 | 46 | ||
| 44 | 47 | // Regular tag: truncate if needed | |
| 45 | 48 | if len(image) > maxLen { | |
| 46 | - | return image[:maxLen-1] + "…" | |
| 49 | + | if maxLen <= 3 { | |
| 50 | + | return image[:maxLen] | |
| 51 | + | } | |
| 52 | + | return image[:maxLen-3] + "..." | |
| 47 | 53 | } | |
| 48 | 54 | return image | |
| 49 | 55 | } | |
| 50 | 56 | ||
| 57 | + | func truncateNetwork(network string, maxLen int) string { | |
| 58 | + | if network == "" || network == "-" { | |
| 59 | + | return "-" | |
| 60 | + | } | |
| 61 | + | if len(network) > maxLen { | |
| 62 | + | if maxLen <= 3 { | |
| 63 | + | return network[:maxLen] | |
| 64 | + | } | |
| 65 | + | return network[:maxLen-3] + "..." | |
| 66 | + | } | |
| 67 | + | return network | |
| 68 | + | } | |
| 69 | + | ||
| 51 | 70 | // newRoutesCmd creates the routes command group. | |
| 52 | 71 | func newRoutesCmd() *cobra.Command { | |
| 53 | 72 | cmd := &cobra.Command{ | |
| @@ -107,7 +126,7 @@ func newRoutesListCmd() *cobra.Command { | |||
| 107 | 126 | ||
| 108 | 127 | // runRoutesListRemote lists routes from a remote Gordon instance. | |
| 109 | 128 | func runRoutesListRemote(ctx context.Context, client *remote.Client) error { | |
| 110 | - | routes, err := client.ListRoutes(ctx) | |
| 129 | + | routes, err := client.ListRoutesWithDetails(ctx) | |
| 111 | 130 | if err != nil { | |
| 112 | 131 | return fmt.Errorf("failed to list routes: %w", err) | |
| 113 | 132 | } | |
| @@ -123,16 +142,20 @@ func runRoutesListRemote(ctx context.Context, client *remote.Client) error { | |||
| 123 | 142 | health = make(map[string]*remote.RouteHealth) | |
| 124 | 143 | } | |
| 125 | 144 | ||
| 126 | - | // Build table rows | |
| 127 | 145 | const imageColWidth = 35 | |
| 128 | - | rows := make([][]string, len(routes)) | |
| 129 | - | for i, route := range routes { | |
| 146 | + | const networkColWidth = 22 | |
| 147 | + | rows := make([][]string, 0, len(routes)) | |
| 148 | + | for _, route := range routes { | |
| 130 | 149 | routeHealth := health[route.Domain] | |
| 131 | 150 | ||
| 132 | 151 | // Container status column | |
| 133 | - | containerStatus := "unknown" | |
| 134 | - | if routeHealth != nil { | |
| 135 | - | containerStatus = routeHealth.ContainerStatus | |
| 152 | + | containerStatus := route.ContainerStatus | |
| 153 | + | if containerStatus == "" { | |
| 154 | + | if routeHealth != nil { | |
| 155 | + | containerStatus = routeHealth.ContainerStatus | |
| 156 | + | } else { | |
| 157 | + | containerStatus = "unknown" | |
| 158 | + | } | |
| 136 | 159 | } | |
| 137 | 160 | containerBadge := components.ContainerStatusBadge(containerStatus) | |
| 138 | 161 | ||
| @@ -140,7 +163,22 @@ func runRoutesListRemote(ctx context.Context, client *remote.Client) error { | |||
| 140 | 163 | httpStatus := formatHTTPStatus(routeHealth) | |
| 141 | 164 | ||
| 142 | 165 | displayImage := truncateImage(route.Image, imageColWidth) | |
| 143 | - | rows[i] = []string{route.Domain, displayImage, containerBadge, httpStatus} | |
| 166 | + | displayNetwork := truncateNetwork(route.Network, networkColWidth) | |
| 167 | + | rows = append(rows, []string{route.Domain, displayImage, displayNetwork, containerBadge, httpStatus}) | |
| 168 | + | ||
| 169 | + | for i, attachment := range route.Attachments { | |
| 170 | + | prefix := "|-" | |
| 171 | + | if i == len(route.Attachments)-1 { | |
| 172 | + | prefix = "`-" | |
| 173 | + | } | |
| 174 | + | attachmentName := prefix + " " + attachment.Name | |
| 175 | + | if len(attachmentName) > 25 { | |
| 176 | + | attachmentName = attachmentName[:22] + "..." | |
| 177 | + | } | |
| 178 | + | attachmentStatus := components.ContainerStatusBadge(attachment.Status) | |
| 179 | + | attachmentImage := truncateImage(attachment.Image, imageColWidth) | |
| 180 | + | rows = append(rows, []string{attachmentName, attachmentImage, "-", attachmentStatus, "-"}) | |
| 181 | + | } | |
| 144 | 182 | } | |
| 145 | 183 | ||
| 146 | 184 | // Render table | |
| @@ -148,6 +186,7 @@ func runRoutesListRemote(ctx context.Context, client *remote.Client) error { | |||
| 148 | 186 | components.WithColumns([]components.TableColumn{ | |
| 149 | 187 | {Title: "Domain", Width: 25}, | |
| 150 | 188 | {Title: "Image", Width: imageColWidth}, | |
| 189 | + | {Title: "Network", Width: networkColWidth}, | |
| 151 | 190 | {Title: "Container", Width: 12}, | |
| 152 | 191 | {Title: "HTTP", Width: 14}, | |
| 153 | 192 | }), | |
| @@ -29,25 +29,44 @@ 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 name if still too long | ||
| 35 | - | available := maxLen - len("@sha256:") - 12 - 1 // 1 for ellipsis | 35 | + | available := maxLen - len("@sha256:") - len(digest) - 3 // 3 for ellipsis |
| 36 | - | if available > 3 { | 36 | + | if available > 0 { |
| 37 | - | name = name[:available-1] + "…" | 37 | + | if len(name) > available { |
| 38 | + | name = name[:available] | |||
| 39 | + | } | |||
| 40 | + | name = name + "..." | |||
| 41 | + | short = fmt.Sprintf("%s@sha256:%s", name, digest) | |||
| 38 | } | 42 | } | ||
| 39 | - | short = fmt.Sprintf("%s@sha256:%s…", name, digest) | |||
| 40 | } | 43 | } | ||
| 41 | return short | 44 | return short | ||
| 42 | } | 45 | } | ||
| 43 | 46 | ||||
| 44 | // Regular tag: truncate if needed | 47 | // Regular tag: truncate if needed | ||
| 45 | if len(image) > maxLen { | 48 | if len(image) > maxLen { | ||
| 46 | - | return image[:maxLen-1] + "…" | 49 | + | if maxLen <= 3 { |
| 50 | + | return image[:maxLen] | |||
| 51 | + | } | |||
| 52 | + | return image[:maxLen-3] + "..." | |||
| 47 | } | 53 | } | ||
| 48 | return image | 54 | return image | ||
| 49 | } | 55 | } | ||
| 50 | 56 | ||||
| 57 | + | func truncateNetwork(network string, maxLen int) string { | |||
| 58 | + | if network == "" || network == "-" { | |||
| 59 | + | return "-" | |||
| 60 | + | } | |||
| 61 | + | if len(network) > maxLen { | |||
| 62 | + | if maxLen <= 3 { | |||
| 63 | + | return network[:maxLen] | |||
| 64 | + | } | |||
| 65 | + | return network[:maxLen-3] + "..." | |||
| 66 | + | } | |||
| 67 | + | return network | |||
| 68 | + | } | |||
| 69 | + | ||||
| 51 | // newRoutesCmd creates the routes command group. | 70 | // newRoutesCmd creates the routes command group. | ||
| 52 | func newRoutesCmd() *cobra.Command { | 71 | func newRoutesCmd() *cobra.Command { | ||
| 53 | cmd := &cobra.Command{ | 72 | cmd := &cobra.Command{ | ||
| @@ -107,7 +126,7 @@ func newRoutesListCmd() *cobra.Command { | |||||
| 107 | 126 | ||||
| 108 | // runRoutesListRemote lists routes from a remote Gordon instance. | 127 | // runRoutesListRemote lists routes from a remote Gordon instance. | ||
| 109 | func runRoutesListRemote(ctx context.Context, client *remote.Client) error { | 128 | func runRoutesListRemote(ctx context.Context, client *remote.Client) error { | ||
| 110 | - | routes, err := client.ListRoutes(ctx) | 129 | + | routes, err := client.ListRoutesWithDetails(ctx) |
| 111 | if err != nil { | 130 | if err != nil { | ||
| 112 | return fmt.Errorf("failed to list routes: %w", err) | 131 | return fmt.Errorf("failed to list routes: %w", err) | ||
| 113 | } | 132 | } | ||
| @@ -123,16 +142,20 @@ func runRoutesListRemote(ctx context.Context, client *remote.Client) error { | |||||
| 123 | health = make(map[string]*remote.RouteHealth) | 142 | health = make(map[string]*remote.RouteHealth) | ||
| 124 | } | 143 | } | ||
| 125 | 144 | ||||
| 126 | - | // Build table rows | |||
| 127 | const imageColWidth = 35 | 145 | const imageColWidth = 35 | ||
| 128 | - | rows := make([][]string, len(routes)) | 146 | + | const networkColWidth = 22 |
| 129 | - | for i, route := range routes { | 147 | + | rows := make([][]string, 0, len(routes)) |
| 148 | + | for _, route := range routes { | |||
| 130 | routeHealth := health[route.Domain] | 149 | routeHealth := health[route.Domain] | ||
| 131 | 150 | ||||
| 132 | // Container status column | 151 | // Container status column | ||
| 133 | - | containerStatus := "unknown" | 152 | + | containerStatus := route.ContainerStatus |
| 134 | - | if routeHealth != nil { | 153 | + | if containerStatus == "" { |
| 135 | - | containerStatus = routeHealth.ContainerStatus | 154 | + | if routeHealth != nil { |
| 155 | + | containerStatus = routeHealth.ContainerStatus | |||
| 156 | + | } else { | |||
| 157 | + | containerStatus = "unknown" | |||
| 158 | + | } | |||
| 136 | } | 159 | } | ||
| 137 | containerBadge := components.ContainerStatusBadge(containerStatus) | 160 | containerBadge := components.ContainerStatusBadge(containerStatus) | ||
| 138 | 161 | ||||
| @@ -140,7 +163,22 @@ func runRoutesListRemote(ctx context.Context, client *remote.Client) error { | |||||
| 140 | httpStatus := formatHTTPStatus(routeHealth) | 163 | httpStatus := formatHTTPStatus(routeHealth) | ||
| 141 | 164 | ||||
| 142 | displayImage := truncateImage(route.Image, imageColWidth) | 165 | displayImage := truncateImage(route.Image, imageColWidth) | ||
| 143 | - | rows[i] = []string{route.Domain, displayImage, containerBadge, httpStatus} | 166 | + | displayNetwork := truncateNetwork(route.Network, networkColWidth) |
| 167 | + | rows = append(rows, []string{route.Domain, displayImage, displayNetwork, containerBadge, httpStatus}) | |||
| 168 | + | ||||
| 169 | + | for i, attachment := range route.Attachments { | |||
| 170 | + | prefix := "|-" | |||
| 171 | + | if i == len(route.Attachments)-1 { | |||
| 172 | + | prefix = "`-" | |||
| 173 | + | } | |||
| 174 | + | attachmentName := prefix + " " + attachment.Name | |||
| 175 | + | if len(attachmentName) > 25 { | |||
| 176 | + | attachmentName = attachmentName[:22] + "..." | |||
| 177 | + | } | |||
| 178 | + | attachmentStatus := components.ContainerStatusBadge(attachment.Status) | |||
| 179 | + | attachmentImage := truncateImage(attachment.Image, imageColWidth) | |||
| 180 | + | rows = append(rows, []string{attachmentName, attachmentImage, "-", attachmentStatus, "-"}) | |||
| 181 | + | } | |||
| 144 | } | 182 | } | ||
| 145 | 183 | ||||
| 146 | // Render table | 184 | // Render table | ||
| @@ -148,6 +186,7 @@ func runRoutesListRemote(ctx context.Context, client *remote.Client) error { | |||||
| 148 | components.WithColumns([]components.TableColumn{ | 186 | components.WithColumns([]components.TableColumn{ | ||
| 149 | {Title: "Domain", Width: 25}, | 187 | {Title: "Domain", Width: 25}, | ||
| 150 | {Title: "Image", Width: imageColWidth}, | 188 | {Title: "Image", Width: imageColWidth}, | ||
| 189 | + | {Title: "Network", Width: networkColWidth}, | |||
| 151 | {Title: "Container", Width: 12}, | 190 | {Title: "Container", Width: 12}, | ||
| 152 | {Title: "HTTP", Width: 14}, | 191 | {Title: "HTTP", Width: 14}, | ||
| 153 | }), | 192 | }), | ||
M internal/adapters/in/http/admin/handler.go +86 -0
| @@ -34,6 +34,22 @@ type Handler struct { | |||
| 34 | 34 | log zerowrap.Logger | |
| 35 | 35 | } | |
| 36 | 36 | ||
| 37 | + | type routeInfoResponse struct { | |
| 38 | + | Domain string `json:"domain"` | |
| 39 | + | Image string `json:"image"` | |
| 40 | + | ContainerID string `json:"container_id"` | |
| 41 | + | ContainerStatus string `json:"container_status"` | |
| 42 | + | Network string `json:"network"` | |
| 43 | + | Attachments []attachmentResponse `json:"attachments"` | |
| 44 | + | } | |
| 45 | + | ||
| 46 | + | type attachmentResponse struct { | |
| 47 | + | Name string `json:"name"` | |
| 48 | + | Image string `json:"image"` | |
| 49 | + | ContainerID string `json:"container_id"` | |
| 50 | + | Status string `json:"status"` | |
| 51 | + | } | |
| 52 | + | ||
| 37 | 53 | // NewHandler creates a new admin HTTP handler. | |
| 38 | 54 | func NewHandler( | |
| 39 | 55 | configSvc in.ConfigService, | |
| @@ -83,6 +99,8 @@ func (h *Handler) handleAdminRoutes(w http.ResponseWriter, r *http.Request) { | |||
| 83 | 99 | switch { | |
| 84 | 100 | case path == "/routes" || strings.HasPrefix(path, "/routes/"): | |
| 85 | 101 | h.handleRoutes(w, r, path) | |
| 102 | + | case path == "/networks": | |
| 103 | + | h.handleNetworks(w, r) | |
| 86 | 104 | case path == "/secrets" || strings.HasPrefix(path, "/secrets/"): | |
| 87 | 105 | h.handleSecrets(w, r, path) | |
| 88 | 106 | case path == "/deploy" || strings.HasPrefix(path, "/deploy/"): | |
| @@ -148,11 +166,57 @@ func (h *Handler) handleRoutesGet(w http.ResponseWriter, r *http.Request, routeD | |||
| 148 | 166 | } | |
| 149 | 167 | ||
| 150 | 168 | if routeDomain == "" { | |
| 169 | + | if r.URL.Query().Get("detailed") == "true" { | |
| 170 | + | routes := h.containerSvc.ListRoutesWithDetails(ctx) | |
| 171 | + | response := make([]routeInfoResponse, 0, len(routes)) | |
| 172 | + | for _, route := range routes { | |
| 173 | + | attachments := make([]attachmentResponse, 0, len(route.Attachments)) | |
| 174 | + | for _, attachment := range route.Attachments { | |
| 175 | + | attachments = append(attachments, attachmentResponse{ | |
| 176 | + | Name: attachment.Name, | |
| 177 | + | Image: attachment.Image, | |
| 178 | + | ContainerID: attachment.ContainerID, | |
| 179 | + | Status: attachment.Status, | |
| 180 | + | }) | |
| 181 | + | } | |
| 182 | + | response = append(response, routeInfoResponse{ | |
| 183 | + | Domain: route.Domain, | |
| 184 | + | Image: route.Image, | |
| 185 | + | ContainerID: route.ContainerID, | |
| 186 | + | ContainerStatus: route.ContainerStatus, | |
| 187 | + | Network: route.Network, | |
| 188 | + | Attachments: attachments, | |
| 189 | + | }) | |
| 190 | + | } | |
| 191 | + | h.sendJSON(w, http.StatusOK, map[string]any{"routes": response}) | |
| 192 | + | return | |
| 193 | + | } | |
| 194 | + | ||
| 151 | 195 | routes := h.configSvc.GetRoutes(ctx) | |
| 152 | 196 | h.sendJSON(w, http.StatusOK, map[string]any{"routes": routes}) | |
| 153 | 197 | return | |
| 154 | 198 | } | |
| 155 | 199 | ||
| 200 | + | if strings.HasSuffix(routeDomain, "/attachments") { | |
| 201 | + | parentDomain := strings.TrimSuffix(routeDomain, "/attachments") | |
| 202 | + | if parentDomain == "" { | |
| 203 | + | h.sendError(w, http.StatusBadRequest, "domain required in path") | |
| 204 | + | return | |
| 205 | + | } | |
| 206 | + | attachments := h.containerSvc.ListAttachments(ctx, parentDomain) | |
| 207 | + | response := make([]attachmentResponse, 0, len(attachments)) | |
| 208 | + | for _, attachment := range attachments { | |
| 209 | + | response = append(response, attachmentResponse{ | |
| 210 | + | Name: attachment.Name, | |
| 211 | + | Image: attachment.Image, | |
| 212 | + | ContainerID: attachment.ContainerID, | |
| 213 | + | Status: attachment.Status, | |
| 214 | + | }) | |
| 215 | + | } | |
| 216 | + | h.sendJSON(w, http.StatusOK, map[string]any{"attachments": response}) | |
| 217 | + | return | |
| 218 | + | } | |
| 219 | + | ||
| 156 | 220 | route, err := h.configSvc.GetRoute(ctx, routeDomain) | |
| 157 | 221 | if err != nil { | |
| 158 | 222 | h.sendError(w, http.StatusNotFound, "route not found") | |
| @@ -265,6 +329,28 @@ func (h *Handler) handleRoutesDelete(w http.ResponseWriter, r *http.Request, rou | |||
| 265 | 329 | h.sendJSON(w, http.StatusOK, map[string]string{"status": "removed"}) | |
| 266 | 330 | } | |
| 267 | 331 | ||
| 332 | + | func (h *Handler) handleNetworks(w http.ResponseWriter, r *http.Request) { | |
| 333 | + | if r.Method != http.MethodGet { | |
| 334 | + | http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) | |
| 335 | + | return | |
| 336 | + | } | |
| 337 | + | ||
| 338 | + | ctx := r.Context() | |
| 339 | + | ||
| 340 | + | if !HasAccess(ctx, domain.AdminResourceStatus, domain.AdminActionRead) { | |
| 341 | + | h.sendError(w, http.StatusForbidden, "insufficient permissions for status:read") | |
| 342 | + | return | |
| 343 | + | } | |
| 344 | + | ||
| 345 | + | networks, err := h.containerSvc.ListNetworks(ctx) | |
| 346 | + | if err != nil { | |
| 347 | + | h.sendError(w, http.StatusInternalServerError, "failed to list networks") | |
| 348 | + | return | |
| 349 | + | } | |
| 350 | + | ||
| 351 | + | h.sendJSON(w, http.StatusOK, map[string]any{"networks": networks}) | |
| 352 | + | } | |
| 353 | + | ||
| 268 | 354 | // handleSecrets handles /admin/secrets endpoints. | |
| 269 | 355 | func (h *Handler) handleSecrets(w http.ResponseWriter, r *http.Request, path string) { | |
| 270 | 356 | ctx := r.Context() | |
| @@ -34,6 +34,22 @@ type Handler struct { | |||||
| 34 | log zerowrap.Logger | 34 | log zerowrap.Logger | ||
| 35 | } | 35 | } | ||
| 36 | 36 | ||||
| 37 | + | type routeInfoResponse struct { | |||
| 38 | + | Domain string `json:"domain"` | |||
| 39 | + | Image string `json:"image"` | |||
| 40 | + | ContainerID string `json:"container_id"` | |||
| 41 | + | ContainerStatus string `json:"container_status"` | |||
| 42 | + | Network string `json:"network"` | |||
| 43 | + | Attachments []attachmentResponse `json:"attachments"` | |||
| 44 | + | } | |||
| 45 | + | ||||
| 46 | + | type attachmentResponse struct { | |||
| 47 | + | Name string `json:"name"` | |||
| 48 | + | Image string `json:"image"` | |||
| 49 | + | ContainerID string `json:"container_id"` | |||
| 50 | + | Status string `json:"status"` | |||
| 51 | + | } | |||
| 52 | + | ||||
| 37 | // NewHandler creates a new admin HTTP handler. | 53 | // NewHandler creates a new admin HTTP handler. | ||
| 38 | func NewHandler( | 54 | func NewHandler( | ||
| 39 | configSvc in.ConfigService, | 55 | configSvc in.ConfigService, | ||
| @@ -83,6 +99,8 @@ func (h *Handler) handleAdminRoutes(w http.ResponseWriter, r *http.Request) { | |||||
| 83 | switch { | 99 | switch { | ||
| 84 | case path == "/routes" || strings.HasPrefix(path, "/routes/"): | 100 | case path == "/routes" || strings.HasPrefix(path, "/routes/"): | ||
| 85 | h.handleRoutes(w, r, path) | 101 | h.handleRoutes(w, r, path) | ||
| 102 | + | case path == "/networks": | |||
| 103 | + | h.handleNetworks(w, r) | |||
| 86 | case path == "/secrets" || strings.HasPrefix(path, "/secrets/"): | 104 | case path == "/secrets" || strings.HasPrefix(path, "/secrets/"): | ||
| 87 | h.handleSecrets(w, r, path) | 105 | h.handleSecrets(w, r, path) | ||
| 88 | case path == "/deploy" || strings.HasPrefix(path, "/deploy/"): | 106 | case path == "/deploy" || strings.HasPrefix(path, "/deploy/"): | ||
| @@ -148,11 +166,57 @@ func (h *Handler) handleRoutesGet(w http.ResponseWriter, r *http.Request, routeD | |||||
| 148 | } | 166 | } | ||
| 149 | 167 | ||||
| 150 | if routeDomain == "" { | 168 | if routeDomain == "" { | ||
| 169 | + | if r.URL.Query().Get("detailed") == "true" { | |||
| 170 | + | routes := h.containerSvc.ListRoutesWithDetails(ctx) | |||
| 171 | + | response := make([]routeInfoResponse, 0, len(routes)) | |||
| 172 | + | for _, route := range routes { | |||
| 173 | + | attachments := make([]attachmentResponse, 0, len(route.Attachments)) | |||
| 174 | + | for _, attachment := range route.Attachments { | |||
| 175 | + | attachments = append(attachments, attachmentResponse{ | |||
| 176 | + | Name: attachment.Name, | |||
| 177 | + | Image: attachment.Image, | |||
| 178 | + | ContainerID: attachment.ContainerID, | |||
| 179 | + | Status: attachment.Status, | |||
| 180 | + | }) | |||
| 181 | + | } | |||
| 182 | + | response = append(response, routeInfoResponse{ | |||
| 183 | + | Domain: route.Domain, | |||
| 184 | + | Image: route.Image, | |||
| 185 | + | ContainerID: route.ContainerID, | |||
| 186 | + | ContainerStatus: route.ContainerStatus, | |||
| 187 | + | Network: route.Network, | |||
| 188 | + | Attachments: attachments, | |||
| 189 | + | }) | |||
| 190 | + | } | |||
| 191 | + | h.sendJSON(w, http.StatusOK, map[string]any{"routes": response}) | |||
| 192 | + | return | |||
| 193 | + | } | |||
| 194 | + | ||||
| 151 | routes := h.configSvc.GetRoutes(ctx) | 195 | routes := h.configSvc.GetRoutes(ctx) | ||
| 152 | h.sendJSON(w, http.StatusOK, map[string]any{"routes": routes}) | 196 | h.sendJSON(w, http.StatusOK, map[string]any{"routes": routes}) | ||
| 153 | return | 197 | return | ||
| 154 | } | 198 | } | ||
| 155 | 199 | ||||
| 200 | + | if strings.HasSuffix(routeDomain, "/attachments") { | |||
| 201 | + | parentDomain := strings.TrimSuffix(routeDomain, "/attachments") | |||
| 202 | + | if parentDomain == "" { | |||
| 203 | + | h.sendError(w, http.StatusBadRequest, "domain required in path") | |||
| 204 | + | return | |||
| 205 | + | } | |||
| 206 | + | attachments := h.containerSvc.ListAttachments(ctx, parentDomain) | |||
| 207 | + | response := make([]attachmentResponse, 0, len(attachments)) | |||
| 208 | + | for _, attachment := range attachments { | |||
| 209 | + | response = append(response, attachmentResponse{ | |||
| 210 | + | Name: attachment.Name, | |||
| 211 | + | Image: attachment.Image, | |||
| 212 | + | ContainerID: attachment.ContainerID, | |||
| 213 | + | Status: attachment.Status, | |||
| 214 | + | }) | |||
| 215 | + | } | |||
| 216 | + | h.sendJSON(w, http.StatusOK, map[string]any{"attachments": response}) | |||
| 217 | + | return | |||
| 218 | + | } | |||
| 219 | + | ||||
| 156 | route, err := h.configSvc.GetRoute(ctx, routeDomain) | 220 | route, err := h.configSvc.GetRoute(ctx, routeDomain) | ||
| 157 | if err != nil { | 221 | if err != nil { | ||
| 158 | h.sendError(w, http.StatusNotFound, "route not found") | 222 | h.sendError(w, http.StatusNotFound, "route not found") | ||
| @@ -265,6 +329,28 @@ func (h *Handler) handleRoutesDelete(w http.ResponseWriter, r *http.Request, rou | |||||
| 265 | h.sendJSON(w, http.StatusOK, map[string]string{"status": "removed"}) | 329 | h.sendJSON(w, http.StatusOK, map[string]string{"status": "removed"}) | ||
| 266 | } | 330 | } | ||
| 267 | 331 | ||||
| 332 | + | func (h *Handler) handleNetworks(w http.ResponseWriter, r *http.Request) { | |||
| 333 | + | if r.Method != http.MethodGet { | |||
| 334 | + | http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) | |||
| 335 | + | return | |||
| 336 | + | } | |||
| 337 | + | ||||
| 338 | + | ctx := r.Context() | |||
| 339 | + | ||||
| 340 | + | if !HasAccess(ctx, domain.AdminResourceStatus, domain.AdminActionRead) { | |||
| 341 | + | h.sendError(w, http.StatusForbidden, "insufficient permissions for status:read") | |||
| 342 | + | return | |||
| 343 | + | } | |||
| 344 | + | ||||
| 345 | + | networks, err := h.containerSvc.ListNetworks(ctx) | |||
| 346 | + | if err != nil { | |||
| 347 | + | h.sendError(w, http.StatusInternalServerError, "failed to list networks") | |||
| 348 | + | return | |||
| 349 | + | } | |||
| 350 | + | ||||
| 351 | + | h.sendJSON(w, http.StatusOK, map[string]any{"networks": networks}) | |||
| 352 | + | } | |||
| 353 | + | ||||
| 268 | // handleSecrets handles /admin/secrets endpoints. | 354 | // handleSecrets handles /admin/secrets endpoints. | ||
| 269 | func (h *Handler) handleSecrets(w http.ResponseWriter, r *http.Request, path string) { | 355 | func (h *Handler) handleSecrets(w http.ResponseWriter, r *http.Request, path string) { | ||
| 270 | ctx := r.Context() | 356 | ctx := r.Context() | ||
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 | ||
M internal/usecase/container/service.go +97 -0
| @@ -316,6 +316,68 @@ 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 | + | func (s *Service) ListRoutesWithDetails(ctx context.Context) []domain.RouteInfo { | |
| 321 | + | s.mu.RLock() | |
| 322 | + | containers := make(map[string]*domain.Container, len(s.containers)) | |
| 323 | + | maps.Copy(containers, s.containers) | |
| 324 | + | s.mu.RUnlock() | |
| 325 | + | ||
| 326 | + | results := make([]domain.RouteInfo, 0, len(containers)) | |
| 327 | + | for domainName, container := range containers { | |
| 328 | + | network := "" | |
| 329 | + | image := "" | |
| 330 | + | containerID := "" | |
| 331 | + | status := "" | |
| 332 | + | if container != nil { | |
| 333 | + | containerID = container.ID | |
| 334 | + | status = container.Status | |
| 335 | + | image = container.Image | |
| 336 | + | if container.Labels != nil { | |
| 337 | + | if labelImage, ok := container.Labels[domain.LabelImage]; ok && labelImage != "" { | |
| 338 | + | image = labelImage | |
| 339 | + | } | |
| 340 | + | } | |
| 341 | + | if networkName, err := s.runtime.GetContainerNetwork(ctx, container.ID); err == nil { | |
| 342 | + | network = networkName | |
| 343 | + | } | |
| 344 | + | } | |
| 345 | + | ||
| 346 | + | results = append(results, domain.RouteInfo{ | |
| 347 | + | Domain: domainName, | |
| 348 | + | Image: image, | |
| 349 | + | ContainerID: containerID, | |
| 350 | + | ContainerStatus: status, | |
| 351 | + | Network: network, | |
| 352 | + | Attachments: s.getAttachmentsForDomain(ctx, domainName), | |
| 353 | + | }) | |
| 354 | + | } | |
| 355 | + | ||
| 356 | + | return results | |
| 357 | + | } | |
| 358 | + | ||
| 359 | + | // ListAttachments returns attachments for a domain. | |
| 360 | + | func (s *Service) ListAttachments(ctx context.Context, domainName string) []domain.Attachment { | |
| 361 | + | return s.getAttachmentsForDomain(ctx, domainName) | |
| 362 | + | } | |
| 363 | + | ||
| 364 | + | // ListNetworks returns Gordon-managed networks. | |
| 365 | + | func (s *Service) ListNetworks(ctx context.Context) ([]*domain.NetworkInfo, error) { | |
| 366 | + | networks, err := s.runtime.ListNetworks(ctx) | |
| 367 | + | if err != nil { | |
| 368 | + | return nil, err | |
| 369 | + | } | |
| 370 | + | ||
| 371 | + | var filtered []*domain.NetworkInfo | |
| 372 | + | for _, network := range networks { | |
| 373 | + | if strings.HasPrefix(network.Name, s.config.NetworkPrefix+"-") { | |
| 374 | + | filtered = append(filtered, network) | |
| 375 | + | } | |
| 376 | + | } | |
| 377 | + | ||
| 378 | + | return filtered, nil | |
| 379 | + | } | |
| 380 | + | ||
| 319 | 381 | // HealthCheck performs health checks on all containers. | |
| 320 | 382 | func (s *Service) HealthCheck(ctx context.Context) map[string]bool { | |
| 321 | 383 | ctx = zerowrap.CtxWithFields(ctx, map[string]any{ | |
| @@ -869,6 +931,41 @@ func (s *Service) deployAttachments(ctx context.Context, domainName, networkName | |||
| 869 | 931 | return nil | |
| 870 | 932 | } | |
| 871 | 933 | ||
| 934 | + | func (s *Service) getAttachmentsForDomain(ctx context.Context, domainName string) []domain.Attachment { | |
| 935 | + | containers, err := s.runtime.ListContainers(ctx, true) | |
| 936 | + | if err != nil { | |
| 937 | + | return nil | |
| 938 | + | } | |
| 939 | + | ||
| 940 | + | attachments := make([]domain.Attachment, 0) | |
| 941 | + | for _, container := range containers { | |
| 942 | + | if container.Labels == nil { | |
| 943 | + | continue | |
| 944 | + | } | |
| 945 | + | if container.Labels[domain.LabelAttachment] != "true" { | |
| 946 | + | continue | |
| 947 | + | } | |
| 948 | + | if container.Labels[domain.LabelAttachedTo] != domainName { | |
| 949 | + | continue | |
| 950 | + | } | |
| 951 | + | image := container.Image | |
| 952 | + | serviceName := container.Name | |
| 953 | + | if labelImage, ok := container.Labels[domain.LabelImage]; ok && labelImage != "" { | |
| 954 | + | image = labelImage | |
| 955 | + | serviceName = extractServiceName(labelImage) | |
| 956 | + | } | |
| 957 | + | attachment := domain.Attachment{ | |
| 958 | + | Name: serviceName, | |
| 959 | + | Image: image, | |
| 960 | + | ContainerID: container.ID, | |
| 961 | + | Status: container.Status, | |
| 962 | + | } | |
| 963 | + | attachments = append(attachments, attachment) | |
| 964 | + | } | |
| 965 | + | ||
| 966 | + | return attachments | |
| 967 | + | } | |
| 968 | + | ||
| 872 | 969 | func (s *Service) deployAttachedService(ctx context.Context, ownerDomain, serviceImage, networkName string) error { | |
| 873 | 970 | log := zerowrap.FromCtx(ctx) | |
| 874 | 971 | ||
| @@ -316,6 +316,68 @@ 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 | + | func (s *Service) ListRoutesWithDetails(ctx context.Context) []domain.RouteInfo { | |||
| 321 | + | s.mu.RLock() | |||
| 322 | + | containers := make(map[string]*domain.Container, len(s.containers)) | |||
| 323 | + | maps.Copy(containers, s.containers) | |||
| 324 | + | s.mu.RUnlock() | |||
| 325 | + | ||||
| 326 | + | results := make([]domain.RouteInfo, 0, len(containers)) | |||
| 327 | + | for domainName, container := range containers { | |||
| 328 | + | network := "" | |||
| 329 | + | image := "" | |||
| 330 | + | containerID := "" | |||
| 331 | + | status := "" | |||
| 332 | + | if container != nil { | |||
| 333 | + | containerID = container.ID | |||
| 334 | + | status = container.Status | |||
| 335 | + | image = container.Image | |||
| 336 | + | if container.Labels != nil { | |||
| 337 | + | if labelImage, ok := container.Labels[domain.LabelImage]; ok && labelImage != "" { | |||
| 338 | + | image = labelImage | |||
| 339 | + | } | |||
| 340 | + | } | |||
| 341 | + | if networkName, err := s.runtime.GetContainerNetwork(ctx, container.ID); err == nil { | |||
| 342 | + | network = networkName | |||
| 343 | + | } | |||
| 344 | + | } | |||
| 345 | + | ||||
| 346 | + | results = append(results, domain.RouteInfo{ | |||
| 347 | + | Domain: domainName, | |||
| 348 | + | Image: image, | |||
| 349 | + | ContainerID: containerID, | |||
| 350 | + | ContainerStatus: status, | |||
| 351 | + | Network: network, | |||
| 352 | + | Attachments: s.getAttachmentsForDomain(ctx, domainName), | |||
| 353 | + | }) | |||
| 354 | + | } | |||
| 355 | + | ||||
| 356 | + | return results | |||
| 357 | + | } | |||
| 358 | + | ||||
| 359 | + | // ListAttachments returns attachments for a domain. | |||
| 360 | + | func (s *Service) ListAttachments(ctx context.Context, domainName string) []domain.Attachment { | |||
| 361 | + | return s.getAttachmentsForDomain(ctx, domainName) | |||
| 362 | + | } | |||
| 363 | + | ||||
| 364 | + | // ListNetworks returns Gordon-managed networks. | |||
| 365 | + | func (s *Service) ListNetworks(ctx context.Context) ([]*domain.NetworkInfo, error) { | |||
| 366 | + | networks, err := s.runtime.ListNetworks(ctx) | |||
| 367 | + | if err != nil { | |||
| 368 | + | return nil, err | |||
| 369 | + | } | |||
| 370 | + | ||||
| 371 | + | var filtered []*domain.NetworkInfo | |||
| 372 | + | for _, network := range networks { | |||
| 373 | + | if strings.HasPrefix(network.Name, s.config.NetworkPrefix+"-") { | |||
| 374 | + | filtered = append(filtered, network) | |||
| 375 | + | } | |||
| 376 | + | } | |||
| 377 | + | ||||
| 378 | + | return filtered, nil | |||
| 379 | + | } | |||
| 380 | + | ||||
| 319 | // HealthCheck performs health checks on all containers. | 381 | // HealthCheck performs health checks on all containers. | ||
| 320 | func (s *Service) HealthCheck(ctx context.Context) map[string]bool { | 382 | func (s *Service) HealthCheck(ctx context.Context) map[string]bool { | ||
| 321 | ctx = zerowrap.CtxWithFields(ctx, map[string]any{ | 383 | ctx = zerowrap.CtxWithFields(ctx, map[string]any{ | ||
| @@ -869,6 +931,41 @@ func (s *Service) deployAttachments(ctx context.Context, domainName, networkName | |||||
| 869 | return nil | 931 | return nil | ||
| 870 | } | 932 | } | ||
| 871 | 933 | ||||
| 934 | + | func (s *Service) getAttachmentsForDomain(ctx context.Context, domainName string) []domain.Attachment { | |||
| 935 | + | containers, err := s.runtime.ListContainers(ctx, true) | |||
| 936 | + | if err != nil { | |||
| 937 | + | return nil | |||
| 938 | + | } | |||
| 939 | + | ||||
| 940 | + | attachments := make([]domain.Attachment, 0) | |||
| 941 | + | for _, container := range containers { | |||
| 942 | + | if container.Labels == nil { | |||
| 943 | + | continue | |||
| 944 | + | } | |||
| 945 | + | if container.Labels[domain.LabelAttachment] != "true" { | |||
| 946 | + | continue | |||
| 947 | + | } | |||
| 948 | + | if container.Labels[domain.LabelAttachedTo] != domainName { | |||
| 949 | + | continue | |||
| 950 | + | } | |||
| 951 | + | image := container.Image | |||
| 952 | + | serviceName := container.Name | |||
| 953 | + | if labelImage, ok := container.Labels[domain.LabelImage]; ok && labelImage != "" { | |||
| 954 | + | image = labelImage | |||
| 955 | + | serviceName = extractServiceName(labelImage) | |||
| 956 | + | } | |||
| 957 | + | attachment := domain.Attachment{ | |||
| 958 | + | Name: serviceName, | |||
| 959 | + | Image: image, | |||
| 960 | + | ContainerID: container.ID, | |||
| 961 | + | Status: container.Status, | |||
| 962 | + | } | |||
| 963 | + | attachments = append(attachments, attachment) | |||
| 964 | + | } | |||
| 965 | + | ||||
| 966 | + | return attachments | |||
| 967 | + | } | |||
| 968 | + | ||||
| 872 | func (s *Service) deployAttachedService(ctx context.Context, ownerDomain, serviceImage, networkName string) error { | 969 | func (s *Service) deployAttachedService(ctx context.Context, ownerDomain, serviceImage, networkName string) error { | ||
| 873 | log := zerowrap.FromCtx(ctx) | 970 | log := zerowrap.FromCtx(ctx) | ||
| 874 | 971 | ||||