detached 3e849f3

refactor: use shared DTO types and improve HTTP handler consistency

- Use dto.RouteInfo and dto.Attachment via type aliases
- Add toAttachmentResponse and toRouteInfoResponse helper functions
- Replace http.Error with h.sendError for consistent JSON responses
- Add input validation in ListAttachments client method
commit
3e849f349beaaaaa98b7e84ce57deb02be06361b
author
brice · · 2026-01-17 11:42 UTC
parent
d2abd75
signature
unsigned
2 files · +45 -59
M internal/adapters/in/cli/remote/client.go +7 -15
@@ -12,6 +12,7 @@ import (
1212 "strings"
1313 "time"
1414
15+ "gordon/internal/adapters/dto"
1516 "gordon/internal/domain"
1617 )
1718
@@ -119,21 +120,9 @@ func parseResponse(resp *http.Response, target any) error {
119120
120121 // Routes API
121122
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-}
123+// Type aliases for API types using shared DTO package.
124+type RouteInfo = dto.RouteInfo
125+type Attachment = dto.Attachment
137126
138127 // ListRoutes returns all configured routes.
139128 func (c *Client) ListRoutes(ctx context.Context) ([]domain.Route, error) {
@@ -188,6 +177,9 @@ func (c *Client) ListNetworks(ctx context.Context) ([]*domain.NetworkInfo, error
188177
189178 // ListAttachments returns attachments for a domain.
190179 func (c *Client) ListAttachments(ctx context.Context, routeDomain string) ([]Attachment, error) {
180+ if routeDomain == "" {
181+ return nil, fmt.Errorf("domain is required")
182+ }
191183 path := "/routes/" + url.PathEscape(routeDomain) + "/attachments"
192184 resp, err := c.request(ctx, http.MethodGet, path, nil)
193185 if err != 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,21 +120,9 @@ func parseResponse(resp *http.Response, target any) error {
119 120
120 // Routes API121 // Routes API
121 122
122-type RouteInfo struct {123+// Type aliases for API types using shared DTO package.
123- Domain string `json:"domain"`124+type RouteInfo = dto.RouteInfo
124- Image string `json:"image"`125+type Attachment = dto.Attachment
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 126
138 // ListRoutes returns all configured routes.127 // ListRoutes returns all configured routes.
139 func (c *Client) ListRoutes(ctx context.Context) ([]domain.Route, error) {128 func (c *Client) ListRoutes(ctx context.Context) ([]domain.Route, error) {
@@ -188,6 +177,9 @@ func (c *Client) ListNetworks(ctx context.Context) ([]*domain.NetworkInfo, error
188 177
189 // ListAttachments returns attachments for a domain.178 // ListAttachments returns attachments for a domain.
190 func (c *Client) ListAttachments(ctx context.Context, routeDomain string) ([]Attachment, error) {179 func (c *Client) ListAttachments(ctx context.Context, routeDomain string) ([]Attachment, error) {
180+ if routeDomain == "" {
181+ return nil, fmt.Errorf("domain is required")
182+ }
191 path := "/routes/" + url.PathEscape(routeDomain) + "/attachments"183 path := "/routes/" + url.PathEscape(routeDomain) + "/attachments"
192 resp, err := c.request(ctx, http.MethodGet, path, nil)184 resp, err := c.request(ctx, http.MethodGet, path, nil)
193 if err != nil {185 if err != nil {
M internal/adapters/in/http/admin/handler.go +38 -44
@@ -11,6 +11,7 @@ import (
1111
1212 "github.com/bnema/zerowrap"
1313
14+ "gordon/internal/adapters/dto"
1415 "gordon/internal/boundaries/in"
1516 "gordon/internal/boundaries/out"
1617 "gordon/internal/domain"
@@ -34,20 +35,34 @@ type Handler struct {
3435 log zerowrap.Logger
3536 }
3637
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"`
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+ }
4450 }
4551
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"`
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+ }
5166 }
5267
5368 // NewHandler creates a new admin HTTP handler.
@@ -152,7 +167,7 @@ func (h *Handler) handleRoutes(w http.ResponseWriter, r *http.Request, path stri
152167 case http.MethodDelete:
153168 h.handleRoutesDelete(w, r, routeDomain)
154169 default:
155- http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
170+ h.sendError(w, http.StatusMethodNotAllowed, "method not allowed")
156171 }
157172 }
158173
@@ -170,23 +185,7 @@ func (h *Handler) handleRoutesGet(w http.ResponseWriter, r *http.Request, routeD
170185 routes := h.containerSvc.ListRoutesWithDetails(ctx)
171186 response := make([]routeInfoResponse, 0, len(routes))
172187 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- })
188+ response = append(response, toRouteInfoResponse(route))
190189 }
191190 h.sendJSON(w, http.StatusOK, map[string]any{"routes": response})
192191 return
@@ -206,12 +205,7 @@ func (h *Handler) handleRoutesGet(w http.ResponseWriter, r *http.Request, routeD
206205 attachments := h.containerSvc.ListAttachments(ctx, parentDomain)
207206 response := make([]attachmentResponse, 0, len(attachments))
208207 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- })
208+ response = append(response, toAttachmentResponse(attachment))
215209 }
216210 h.sendJSON(w, http.StatusOK, map[string]any{"attachments": response})
217211 return
@@ -331,7 +325,7 @@ func (h *Handler) handleRoutesDelete(w http.ResponseWriter, r *http.Request, rou
331325
332326 func (h *Handler) handleNetworks(w http.ResponseWriter, r *http.Request) {
333327 if r.Method != http.MethodGet {
334- http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
328+ h.sendError(w, http.StatusMethodNotAllowed, "method not allowed")
335329 return
336330 }
337331
@@ -433,7 +427,7 @@ func (h *Handler) handleSecrets(w http.ResponseWriter, r *http.Request, path str
433427 h.sendJSON(w, http.StatusOK, map[string]string{"status": "deleted"})
434428
435429 default:
436- http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
430+ h.sendError(w, http.StatusMethodNotAllowed, "method not allowed")
437431 }
438432 }
439433
@@ -441,7 +435,7 @@ func (h *Handler) handleSecrets(w http.ResponseWriter, r *http.Request, path str
441435 // Returns detailed health status for all routes including HTTP probe results.
442436 func (h *Handler) handleHealth(w http.ResponseWriter, r *http.Request) {
443437 if r.Method != http.MethodGet {
444- http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
438+ h.sendError(w, http.StatusMethodNotAllowed, "method not allowed")
445439 return
446440 }
447441
@@ -473,7 +467,7 @@ func (h *Handler) handleHealth(w http.ResponseWriter, r *http.Request) {
473467 // handleStatus handles /admin/status endpoint.
474468 func (h *Handler) handleStatus(w http.ResponseWriter, r *http.Request) {
475469 if r.Method != http.MethodGet {
476- http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
470+ h.sendError(w, http.StatusMethodNotAllowed, "method not allowed")
477471 return
478472 }
479473
@@ -515,7 +509,7 @@ func (h *Handler) handleStatus(w http.ResponseWriter, r *http.Request) {
515509 // This reloads configuration from file into memory and triggers container sync.
516510 func (h *Handler) handleReload(w http.ResponseWriter, r *http.Request) {
517511 if r.Method != http.MethodPost {
518- http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
512+ h.sendError(w, http.StatusMethodNotAllowed, "method not allowed")
519513 return
520514 }
521515
@@ -550,7 +544,7 @@ func (h *Handler) handleReload(w http.ResponseWriter, r *http.Request) {
550544 // handleConfig handles /admin/config endpoint.
551545 func (h *Handler) handleConfig(w http.ResponseWriter, r *http.Request) {
552546 if r.Method != http.MethodGet {
553- http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
547+ h.sendError(w, http.StatusMethodNotAllowed, "method not allowed")
554548 return
555549 }
556550
@@ -587,7 +581,7 @@ func (h *Handler) handleConfig(w http.ResponseWriter, r *http.Request) {
587581 // POST triggers a deployment for the specified domain.
588582 func (h *Handler) handleDeploy(w http.ResponseWriter, r *http.Request, path string) {
589583 if r.Method != http.MethodPost {
590- http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
584+ h.sendError(w, http.StatusMethodNotAllowed, "method not allowed")
591585 return
592586 }
593587
@@ -635,7 +629,7 @@ func (h *Handler) handleDeploy(w http.ResponseWriter, r *http.Request, path stri
635629 // GET /admin/logs/:domain - Container logs for a specific domain
636630 func (h *Handler) handleLogs(w http.ResponseWriter, r *http.Request, path string) {
637631 if r.Method != http.MethodGet {
638- http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
632+ h.sendError(w, http.StatusMethodNotAllowed, "method not allowed")
639633 return
640634 }
641635
@@ -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,20 +35,34 @@ type Handler struct {
34 log zerowrap.Logger35 log zerowrap.Logger
35 }36 }
36 37
37-type routeInfoResponse struct {38+// Type aliases for API responses using shared DTO types.
38- Domain string `json:"domain"`39+type routeInfoResponse = dto.RouteInfo
39- Image string `json:"image"`40+type attachmentResponse = dto.Attachment
40- ContainerID string `json:"container_id"`41+
41- ContainerStatus string `json:"container_status"`42+// toAttachmentResponse converts a domain.Attachment to a dto.Attachment.
42- Network string `json:"network"`43+func toAttachmentResponse(a domain.Attachment) dto.Attachment {
43- Attachments []attachmentResponse `json:"attachments"`44+ return dto.Attachment{
45+ Name: a.Name,
46+ Image: a.Image,
47+ ContainerID: a.ContainerID,
48+ Status: a.Status,
49+ }
44 }50 }
45 51
46-type attachmentResponse struct {52+// toRouteInfoResponse converts a domain.RouteInfo to a dto.RouteInfo.
47- Name string `json:"name"`53+func toRouteInfoResponse(r domain.RouteInfo) dto.RouteInfo {
48- Image string `json:"image"`54+ attachments := make([]dto.Attachment, 0, len(r.Attachments))
49- ContainerID string `json:"container_id"`55+ for _, a := range r.Attachments {
50- Status string `json:"status"`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+ }
51 }66 }
52 67
53 // NewHandler creates a new admin HTTP handler.68 // NewHandler creates a new admin HTTP handler.
@@ -152,7 +167,7 @@ func (h *Handler) handleRoutes(w http.ResponseWriter, r *http.Request, path stri
152 case http.MethodDelete:167 case http.MethodDelete:
153 h.handleRoutesDelete(w, r, routeDomain)168 h.handleRoutesDelete(w, r, routeDomain)
154 default:169 default:
155- http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)170+ h.sendError(w, http.StatusMethodNotAllowed, "method not allowed")
156 }171 }
157 }172 }
158 173
@@ -170,23 +185,7 @@ func (h *Handler) handleRoutesGet(w http.ResponseWriter, r *http.Request, routeD
170 routes := h.containerSvc.ListRoutesWithDetails(ctx)185 routes := h.containerSvc.ListRoutesWithDetails(ctx)
171 response := make([]routeInfoResponse, 0, len(routes))186 response := make([]routeInfoResponse, 0, len(routes))
172 for _, route := range routes {187 for _, route := range routes {
173- attachments := make([]attachmentResponse, 0, len(route.Attachments))188+ response = append(response, toRouteInfoResponse(route))
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 }189 }
191 h.sendJSON(w, http.StatusOK, map[string]any{"routes": response})190 h.sendJSON(w, http.StatusOK, map[string]any{"routes": response})
192 return191 return
@@ -206,12 +205,7 @@ func (h *Handler) handleRoutesGet(w http.ResponseWriter, r *http.Request, routeD
206 attachments := h.containerSvc.ListAttachments(ctx, parentDomain)205 attachments := h.containerSvc.ListAttachments(ctx, parentDomain)
207 response := make([]attachmentResponse, 0, len(attachments))206 response := make([]attachmentResponse, 0, len(attachments))
208 for _, attachment := range attachments {207 for _, attachment := range attachments {
209- response = append(response, attachmentResponse{208+ response = append(response, toAttachmentResponse(attachment))
210- Name: attachment.Name,
211- Image: attachment.Image,
212- ContainerID: attachment.ContainerID,
213- Status: attachment.Status,
214- })
215 }209 }
216 h.sendJSON(w, http.StatusOK, map[string]any{"attachments": response})210 h.sendJSON(w, http.StatusOK, map[string]any{"attachments": response})
217 return211 return
@@ -331,7 +325,7 @@ func (h *Handler) handleRoutesDelete(w http.ResponseWriter, r *http.Request, rou
331 325
332 func (h *Handler) handleNetworks(w http.ResponseWriter, r *http.Request) {326 func (h *Handler) handleNetworks(w http.ResponseWriter, r *http.Request) {
333 if r.Method != http.MethodGet {327 if r.Method != http.MethodGet {
334- http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)328+ h.sendError(w, http.StatusMethodNotAllowed, "method not allowed")
335 return329 return
336 }330 }
337 331
@@ -433,7 +427,7 @@ func (h *Handler) handleSecrets(w http.ResponseWriter, r *http.Request, path str
433 h.sendJSON(w, http.StatusOK, map[string]string{"status": "deleted"})427 h.sendJSON(w, http.StatusOK, map[string]string{"status": "deleted"})
434 428
435 default:429 default:
436- http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)430+ h.sendError(w, http.StatusMethodNotAllowed, "method not allowed")
437 }431 }
438 }432 }
439 433
@@ -441,7 +435,7 @@ func (h *Handler) handleSecrets(w http.ResponseWriter, r *http.Request, path str
441 // Returns detailed health status for all routes including HTTP probe results.435 // Returns detailed health status for all routes including HTTP probe results.
442 func (h *Handler) handleHealth(w http.ResponseWriter, r *http.Request) {436 func (h *Handler) handleHealth(w http.ResponseWriter, r *http.Request) {
443 if r.Method != http.MethodGet {437 if r.Method != http.MethodGet {
444- http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)438+ h.sendError(w, http.StatusMethodNotAllowed, "method not allowed")
445 return439 return
446 }440 }
447 441
@@ -473,7 +467,7 @@ func (h *Handler) handleHealth(w http.ResponseWriter, r *http.Request) {
473 // handleStatus handles /admin/status endpoint.467 // handleStatus handles /admin/status endpoint.
474 func (h *Handler) handleStatus(w http.ResponseWriter, r *http.Request) {468 func (h *Handler) handleStatus(w http.ResponseWriter, r *http.Request) {
475 if r.Method != http.MethodGet {469 if r.Method != http.MethodGet {
476- http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)470+ h.sendError(w, http.StatusMethodNotAllowed, "method not allowed")
477 return471 return
478 }472 }
479 473
@@ -515,7 +509,7 @@ func (h *Handler) handleStatus(w http.ResponseWriter, r *http.Request) {
515 // This reloads configuration from file into memory and triggers container sync.509 // This reloads configuration from file into memory and triggers container sync.
516 func (h *Handler) handleReload(w http.ResponseWriter, r *http.Request) {510 func (h *Handler) handleReload(w http.ResponseWriter, r *http.Request) {
517 if r.Method != http.MethodPost {511 if r.Method != http.MethodPost {
518- http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)512+ h.sendError(w, http.StatusMethodNotAllowed, "method not allowed")
519 return513 return
520 }514 }
521 515
@@ -550,7 +544,7 @@ func (h *Handler) handleReload(w http.ResponseWriter, r *http.Request) {
550 // handleConfig handles /admin/config endpoint.544 // handleConfig handles /admin/config endpoint.
551 func (h *Handler) handleConfig(w http.ResponseWriter, r *http.Request) {545 func (h *Handler) handleConfig(w http.ResponseWriter, r *http.Request) {
552 if r.Method != http.MethodGet {546 if r.Method != http.MethodGet {
553- http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)547+ h.sendError(w, http.StatusMethodNotAllowed, "method not allowed")
554 return548 return
555 }549 }
556 550
@@ -587,7 +581,7 @@ func (h *Handler) handleConfig(w http.ResponseWriter, r *http.Request) {
587 // POST triggers a deployment for the specified domain.581 // POST triggers a deployment for the specified domain.
588 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) {
589 if r.Method != http.MethodPost {583 if r.Method != http.MethodPost {
590- http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)584+ h.sendError(w, http.StatusMethodNotAllowed, "method not allowed")
591 return585 return
592 }586 }
593 587
@@ -635,7 +629,7 @@ func (h *Handler) handleDeploy(w http.ResponseWriter, r *http.Request, path stri
635 // GET /admin/logs/:domain - Container logs for a specific domain629 // GET /admin/logs/:domain - Container logs for a specific domain
636 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) {
637 if r.Method != http.MethodGet {631 if r.Method != http.MethodGet {
638- http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)632+ h.sendError(w, http.StatusMethodNotAllowed, "method not allowed")
639 return633 return
640 }634 }
641 635

Keyboard shortcuts

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

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

Branches 7

Tags 106

↑↓ moveenter openesc close