fix(cleanup): keep route delete response DTO scoped
- commit
- 33a66ecb9cdce1de4ec248fb68a490f48b07e45e
- author
- brice · · 2026-05-22 04:10 UTC
- parent
- 6e7b92e
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
9 files · +188 -69
M internal/adapters/dto/admin_route.go +2 -4
| @@ -1,8 +1,6 @@ | |||
| 1 | 1 | // Package dto provides shared data transfer objects for API responses. | |
| 2 | 2 | package dto | |
| 3 | 3 | ||
| 4 | - | import "github.com/bnema/gordon/internal/domain" | |
| 5 | - | ||
| 6 | 4 | // Route represents route configuration in API responses. | |
| 7 | 5 | type Route struct { | |
| 8 | 6 | Domain string `json:"domain"` | |
| @@ -52,6 +50,6 @@ type RoutesByImageResponse struct { | |||
| 52 | 50 | ||
| 53 | 51 | // RouteDeleteResponse represents a route removal response. | |
| 54 | 52 | type RouteDeleteResponse struct { | |
| 55 | - | Status string `json:"status"` | |
| 56 | - | Cleanup *domain.CleanupReport `json:"cleanup,omitempty"` | |
| 53 | + | Status string `json:"status"` | |
| 54 | + | Cleanup *CleanupReport `json:"cleanup,omitempty"` | |
| 57 | 55 | } | |
| @@ -1,8 +1,6 @@ | |||||
| 1 | // Package dto provides shared data transfer objects for API responses. | 1 | // Package dto provides shared data transfer objects for API responses. | ||
| 2 | package dto | 2 | package dto | ||
| 3 | 3 | ||||
| 4 | - | import "github.com/bnema/gordon/internal/domain" | |||
| 5 | - | ||||
| 6 | // Route represents route configuration in API responses. | 4 | // Route represents route configuration in API responses. | ||
| 7 | type Route struct { | 5 | type Route struct { | ||
| 8 | Domain string `json:"domain"` | 6 | Domain string `json:"domain"` | ||
| @@ -52,6 +50,6 @@ type RoutesByImageResponse struct { | |||||
| 52 | 50 | ||||
| 53 | // RouteDeleteResponse represents a route removal response. | 51 | // RouteDeleteResponse represents a route removal response. | ||
| 54 | type RouteDeleteResponse struct { | 52 | type RouteDeleteResponse struct { | ||
| 55 | - | Status string `json:"status"` | 53 | + | Status string `json:"status"` |
| 56 | - | Cleanup *domain.CleanupReport `json:"cleanup,omitempty"` | 54 | + | Cleanup *CleanupReport `json:"cleanup,omitempty"` |
| 57 | } | 55 | } | ||
A internal/adapters/dto/cleanup.go +86 -0
| @@ -0,0 +1,86 @@ | |||
| 1 | + | package dto | |
| 2 | + | ||
| 3 | + | import "github.com/bnema/gordon/internal/domain" | |
| 4 | + | ||
| 5 | + | // CleanupReport describes cleanup results in API responses. | |
| 6 | + | type CleanupReport struct { | |
| 7 | + | Domain string `json:"domain"` | |
| 8 | + | RemovedContainers []CleanupContainer `json:"removed_containers"` | |
| 9 | + | PreservedVolumes []CleanupVolume `json:"preserved_volumes"` | |
| 10 | + | PreservedAttachments []CleanupAttachment `json:"preserved_attachments"` | |
| 11 | + | OrphanedEntities []CleanupOrphaned `json:"orphaned_entities"` | |
| 12 | + | Warnings []string `json:"warnings"` | |
| 13 | + | Hints []string `json:"hints"` | |
| 14 | + | PartialFailures []CleanupFailure `json:"partial_failures"` | |
| 15 | + | } | |
| 16 | + | ||
| 17 | + | // CleanupContainer identifies a container affected by cleanup. | |
| 18 | + | type CleanupContainer struct { | |
| 19 | + | ID string `json:"id"` | |
| 20 | + | Name string `json:"name"` | |
| 21 | + | Image string `json:"image"` | |
| 22 | + | Status string `json:"status"` | |
| 23 | + | } | |
| 24 | + | ||
| 25 | + | // CleanupVolume identifies preserved stateful data. | |
| 26 | + | type CleanupVolume struct { | |
| 27 | + | Name string `json:"name"` | |
| 28 | + | ContainerPath string `json:"container_path,omitempty"` | |
| 29 | + | Reason string `json:"reason,omitempty"` | |
| 30 | + | } | |
| 31 | + | ||
| 32 | + | // CleanupAttachment identifies an attachment retained for follow-up cleanup. | |
| 33 | + | type CleanupAttachment struct { | |
| 34 | + | Name string `json:"name"` | |
| 35 | + | Image string `json:"image"` | |
| 36 | + | ContainerID string `json:"container_id"` | |
| 37 | + | Status string `json:"status"` | |
| 38 | + | Owner string `json:"owner,omitempty"` | |
| 39 | + | Reason string `json:"reason,omitempty"` | |
| 40 | + | } | |
| 41 | + | ||
| 42 | + | // CleanupOrphaned identifies runtime state no longer configured. | |
| 43 | + | type CleanupOrphaned struct { | |
| 44 | + | Kind string `json:"kind"` | |
| 45 | + | ID string `json:"id,omitempty"` | |
| 46 | + | Name string `json:"name,omitempty"` | |
| 47 | + | Status string `json:"status,omitempty"` | |
| 48 | + | Reason string `json:"reason,omitempty"` | |
| 49 | + | } | |
| 50 | + | ||
| 51 | + | // CleanupFailure records non-fatal cleanup failures. | |
| 52 | + | type CleanupFailure struct { | |
| 53 | + | Action string `json:"action"` | |
| 54 | + | Kind string `json:"kind"` | |
| 55 | + | ID string `json:"id,omitempty"` | |
| 56 | + | Name string `json:"name,omitempty"` | |
| 57 | + | Error string `json:"error"` | |
| 58 | + | } | |
| 59 | + | ||
| 60 | + | // CleanupReportFromDomain maps a domain cleanup report to an API DTO. | |
| 61 | + | func CleanupReportFromDomain(report *domain.CleanupReport) *CleanupReport { | |
| 62 | + | if report == nil { | |
| 63 | + | return nil | |
| 64 | + | } | |
| 65 | + | out := &CleanupReport{ | |
| 66 | + | Domain: report.Domain, | |
| 67 | + | Warnings: append([]string(nil), report.Warnings...), | |
| 68 | + | Hints: append([]string(nil), report.Hints...), | |
| 69 | + | } | |
| 70 | + | for _, item := range report.RemovedContainers { | |
| 71 | + | out.RemovedContainers = append(out.RemovedContainers, CleanupContainer(item)) | |
| 72 | + | } | |
| 73 | + | for _, item := range report.PreservedVolumes { | |
| 74 | + | out.PreservedVolumes = append(out.PreservedVolumes, CleanupVolume(item)) | |
| 75 | + | } | |
| 76 | + | for _, item := range report.PreservedAttachments { | |
| 77 | + | out.PreservedAttachments = append(out.PreservedAttachments, CleanupAttachment(item)) | |
| 78 | + | } | |
| 79 | + | for _, item := range report.OrphanedEntities { | |
| 80 | + | out.OrphanedEntities = append(out.OrphanedEntities, CleanupOrphaned(item)) | |
| 81 | + | } | |
| 82 | + | for _, item := range report.PartialFailures { | |
| 83 | + | out.PartialFailures = append(out.PartialFailures, CleanupFailure(item)) | |
| 84 | + | } | |
| 85 | + | return out | |
| 86 | + | } | |
| @@ -0,0 +1,86 @@ | |||||
| 1 | + | package dto | |||
| 2 | + | ||||
| 3 | + | import "github.com/bnema/gordon/internal/domain" | |||
| 4 | + | ||||
| 5 | + | // CleanupReport describes cleanup results in API responses. | |||
| 6 | + | type CleanupReport struct { | |||
| 7 | + | Domain string `json:"domain"` | |||
| 8 | + | RemovedContainers []CleanupContainer `json:"removed_containers"` | |||
| 9 | + | PreservedVolumes []CleanupVolume `json:"preserved_volumes"` | |||
| 10 | + | PreservedAttachments []CleanupAttachment `json:"preserved_attachments"` | |||
| 11 | + | OrphanedEntities []CleanupOrphaned `json:"orphaned_entities"` | |||
| 12 | + | Warnings []string `json:"warnings"` | |||
| 13 | + | Hints []string `json:"hints"` | |||
| 14 | + | PartialFailures []CleanupFailure `json:"partial_failures"` | |||
| 15 | + | } | |||
| 16 | + | ||||
| 17 | + | // CleanupContainer identifies a container affected by cleanup. | |||
| 18 | + | type CleanupContainer struct { | |||
| 19 | + | ID string `json:"id"` | |||
| 20 | + | Name string `json:"name"` | |||
| 21 | + | Image string `json:"image"` | |||
| 22 | + | Status string `json:"status"` | |||
| 23 | + | } | |||
| 24 | + | ||||
| 25 | + | // CleanupVolume identifies preserved stateful data. | |||
| 26 | + | type CleanupVolume struct { | |||
| 27 | + | Name string `json:"name"` | |||
| 28 | + | ContainerPath string `json:"container_path,omitempty"` | |||
| 29 | + | Reason string `json:"reason,omitempty"` | |||
| 30 | + | } | |||
| 31 | + | ||||
| 32 | + | // CleanupAttachment identifies an attachment retained for follow-up cleanup. | |||
| 33 | + | type CleanupAttachment struct { | |||
| 34 | + | Name string `json:"name"` | |||
| 35 | + | Image string `json:"image"` | |||
| 36 | + | ContainerID string `json:"container_id"` | |||
| 37 | + | Status string `json:"status"` | |||
| 38 | + | Owner string `json:"owner,omitempty"` | |||
| 39 | + | Reason string `json:"reason,omitempty"` | |||
| 40 | + | } | |||
| 41 | + | ||||
| 42 | + | // CleanupOrphaned identifies runtime state no longer configured. | |||
| 43 | + | type CleanupOrphaned struct { | |||
| 44 | + | Kind string `json:"kind"` | |||
| 45 | + | ID string `json:"id,omitempty"` | |||
| 46 | + | Name string `json:"name,omitempty"` | |||
| 47 | + | Status string `json:"status,omitempty"` | |||
| 48 | + | Reason string `json:"reason,omitempty"` | |||
| 49 | + | } | |||
| 50 | + | ||||
| 51 | + | // CleanupFailure records non-fatal cleanup failures. | |||
| 52 | + | type CleanupFailure struct { | |||
| 53 | + | Action string `json:"action"` | |||
| 54 | + | Kind string `json:"kind"` | |||
| 55 | + | ID string `json:"id,omitempty"` | |||
| 56 | + | Name string `json:"name,omitempty"` | |||
| 57 | + | Error string `json:"error"` | |||
| 58 | + | } | |||
| 59 | + | ||||
| 60 | + | // CleanupReportFromDomain maps a domain cleanup report to an API DTO. | |||
| 61 | + | func CleanupReportFromDomain(report *domain.CleanupReport) *CleanupReport { | |||
| 62 | + | if report == nil { | |||
| 63 | + | return nil | |||
| 64 | + | } | |||
| 65 | + | out := &CleanupReport{ | |||
| 66 | + | Domain: report.Domain, | |||
| 67 | + | Warnings: append([]string(nil), report.Warnings...), | |||
| 68 | + | Hints: append([]string(nil), report.Hints...), | |||
| 69 | + | } | |||
| 70 | + | for _, item := range report.RemovedContainers { | |||
| 71 | + | out.RemovedContainers = append(out.RemovedContainers, CleanupContainer(item)) | |||
| 72 | + | } | |||
| 73 | + | for _, item := range report.PreservedVolumes { | |||
| 74 | + | out.PreservedVolumes = append(out.PreservedVolumes, CleanupVolume(item)) | |||
| 75 | + | } | |||
| 76 | + | for _, item := range report.PreservedAttachments { | |||
| 77 | + | out.PreservedAttachments = append(out.PreservedAttachments, CleanupAttachment(item)) | |||
| 78 | + | } | |||
| 79 | + | for _, item := range report.OrphanedEntities { | |||
| 80 | + | out.OrphanedEntities = append(out.OrphanedEntities, CleanupOrphaned(item)) | |||
| 81 | + | } | |||
| 82 | + | for _, item := range report.PartialFailures { | |||
| 83 | + | out.PartialFailures = append(out.PartialFailures, CleanupFailure(item)) | |||
| 84 | + | } | |||
| 85 | + | return out | |||
| 86 | + | } | |||
M internal/adapters/in/cli/controlplane_local.go +1 -1
| @@ -149,7 +149,7 @@ func (l *localControlPlane) RemoveRouteWithCleanup(ctx context.Context, routeDom | |||
| 149 | 149 | if err != nil { | |
| 150 | 150 | return nil, fmt.Errorf("failed to cleanup removed route runtime state: %w", err) | |
| 151 | 151 | } | |
| 152 | - | resp.Cleanup = report | |
| 152 | + | resp.Cleanup = dto.CleanupReportFromDomain(report) | |
| 153 | 153 | return resp, nil | |
| 154 | 154 | } | |
| 155 | 155 | ||
| @@ -149,7 +149,7 @@ func (l *localControlPlane) RemoveRouteWithCleanup(ctx context.Context, routeDom | |||||
| 149 | if err != nil { | 149 | if err != nil { | ||
| 150 | return nil, fmt.Errorf("failed to cleanup removed route runtime state: %w", err) | 150 | return nil, fmt.Errorf("failed to cleanup removed route runtime state: %w", err) | ||
| 151 | } | 151 | } | ||
| 152 | - | resp.Cleanup = report | 152 | + | resp.Cleanup = dto.CleanupReportFromDomain(report) |
| 153 | return resp, nil | 153 | return resp, nil | ||
| 154 | } | 154 | } | ||
| 155 | 155 | ||||
M internal/adapters/in/cli/controlplane_local_test.go +1 -1
| @@ -229,5 +229,5 @@ func TestLocalControlPlane_RemoveRouteReconcilesRuntimeWhenRouteAlreadyMissing(t | |||
| 229 | 229 | resp, err := cp.RemoveRouteWithCleanup(ctx, "app.local") | |
| 230 | 230 | require.NoError(t, err) | |
| 231 | 231 | require.NotNil(t, resp) | |
| 232 | - | assert.Equal(t, report, resp.Cleanup) | |
| 232 | + | assert.Equal(t, dto.CleanupReportFromDomain(report), resp.Cleanup) | |
| 233 | 233 | } | |
| @@ -229,5 +229,5 @@ func TestLocalControlPlane_RemoveRouteReconcilesRuntimeWhenRouteAlreadyMissing(t | |||||
| 229 | resp, err := cp.RemoveRouteWithCleanup(ctx, "app.local") | 229 | resp, err := cp.RemoveRouteWithCleanup(ctx, "app.local") | ||
| 230 | require.NoError(t, err) | 230 | require.NoError(t, err) | ||
| 231 | require.NotNil(t, resp) | 231 | require.NotNil(t, resp) | ||
| 232 | - | assert.Equal(t, report, resp.Cleanup) | 232 | + | assert.Equal(t, dto.CleanupReportFromDomain(report), resp.Cleanup) |
| 233 | } | 233 | } | ||
M internal/adapters/in/cli/remote/client.go +4 -4
| @@ -556,11 +556,11 @@ func (c *Client) RemoveRoute(ctx context.Context, routeDomain string) error { | |||
| 556 | 556 | func (c *Client) RemoveRouteWithCleanup(ctx context.Context, routeDomain string) (*dto.RouteDeleteResponse, error) { | |
| 557 | 557 | resp, err := c.request(ctx, http.MethodDelete, "/routes/"+url.PathEscape(routeDomain), nil) | |
| 558 | 558 | if err != nil { | |
| 559 | - | return nil, err | |
| 559 | + | return nil, fmt.Errorf("delete route %s failed: %w", routeDomain, err) | |
| 560 | 560 | } | |
| 561 | 561 | var result dto.RouteDeleteResponse | |
| 562 | 562 | if err := parseResponse(resp, &result); err != nil { | |
| 563 | - | return nil, err | |
| 563 | + | return nil, fmt.Errorf("parse delete route %s response: %w", routeDomain, err) | |
| 564 | 564 | } | |
| 565 | 565 | return &result, nil | |
| 566 | 566 | } | |
| @@ -1065,11 +1065,11 @@ func (c *Client) CleanupOrphanedAttachments(ctx context.Context, owner string, s | |||
| 1065 | 1065 | } | |
| 1066 | 1066 | resp, err := c.request(ctx, http.MethodPost, path, nil) | |
| 1067 | 1067 | if err != nil { | |
| 1068 | - | return nil, err | |
| 1068 | + | return nil, fmt.Errorf("cleanup orphaned attachments failed: %w", err) | |
| 1069 | 1069 | } | |
| 1070 | 1070 | var result domain.CleanupReport | |
| 1071 | 1071 | if err := parseResponse(resp, &result); err != nil { | |
| 1072 | - | return nil, err | |
| 1072 | + | return nil, fmt.Errorf("parse orphaned attachment cleanup response: %w", err) | |
| 1073 | 1073 | } | |
| 1074 | 1074 | return &result, nil | |
| 1075 | 1075 | } | |
| @@ -556,11 +556,11 @@ func (c *Client) RemoveRoute(ctx context.Context, routeDomain string) error { | |||||
| 556 | func (c *Client) RemoveRouteWithCleanup(ctx context.Context, routeDomain string) (*dto.RouteDeleteResponse, error) { | 556 | func (c *Client) RemoveRouteWithCleanup(ctx context.Context, routeDomain string) (*dto.RouteDeleteResponse, error) { | ||
| 557 | resp, err := c.request(ctx, http.MethodDelete, "/routes/"+url.PathEscape(routeDomain), nil) | 557 | resp, err := c.request(ctx, http.MethodDelete, "/routes/"+url.PathEscape(routeDomain), nil) | ||
| 558 | if err != nil { | 558 | if err != nil { | ||
| 559 | - | return nil, err | 559 | + | return nil, fmt.Errorf("delete route %s failed: %w", routeDomain, err) |
| 560 | } | 560 | } | ||
| 561 | var result dto.RouteDeleteResponse | 561 | var result dto.RouteDeleteResponse | ||
| 562 | if err := parseResponse(resp, &result); err != nil { | 562 | if err := parseResponse(resp, &result); err != nil { | ||
| 563 | - | return nil, err | 563 | + | return nil, fmt.Errorf("parse delete route %s response: %w", routeDomain, err) |
| 564 | } | 564 | } | ||
| 565 | return &result, nil | 565 | return &result, nil | ||
| 566 | } | 566 | } | ||
| @@ -1065,11 +1065,11 @@ func (c *Client) CleanupOrphanedAttachments(ctx context.Context, owner string, s | |||||
| 1065 | } | 1065 | } | ||
| 1066 | resp, err := c.request(ctx, http.MethodPost, path, nil) | 1066 | resp, err := c.request(ctx, http.MethodPost, path, nil) | ||
| 1067 | if err != nil { | 1067 | if err != nil { | ||
| 1068 | - | return nil, err | 1068 | + | return nil, fmt.Errorf("cleanup orphaned attachments failed: %w", err) |
| 1069 | } | 1069 | } | ||
| 1070 | var result domain.CleanupReport | 1070 | var result domain.CleanupReport | ||
| 1071 | if err := parseResponse(resp, &result); err != nil { | 1071 | if err := parseResponse(resp, &result); err != nil { | ||
| 1072 | - | return nil, err | 1072 | + | return nil, fmt.Errorf("parse orphaned attachment cleanup response: %w", err) |
| 1073 | } | 1073 | } | ||
| 1074 | return &result, nil | 1074 | return &result, nil | ||
| 1075 | } | 1075 | } | ||
M internal/adapters/in/cli/routes.go +62 -33
| @@ -1101,25 +1101,29 @@ func newRoutesDiagnoseCmd() *cobra.Command { | |||
| 1101 | 1101 | Short: "Diagnose route configuration, runtime state, and cleanup leftovers", | |
| 1102 | 1102 | Args: cobra.ExactArgs(1), | |
| 1103 | 1103 | RunE: func(cmd *cobra.Command, args []string) error { | |
| 1104 | - | handle, err := resolveControlPlaneForRouteDomain(cmd.Context(), args[0]) | |
| 1105 | - | if err != nil { | |
| 1106 | - | return err | |
| 1107 | - | } | |
| 1108 | - | defer handle.close() | |
| 1109 | - | diag, err := buildRouteDiagnosis(cmd.Context(), handle.plane, args[0]) | |
| 1110 | - | if err != nil { | |
| 1111 | - | return err | |
| 1112 | - | } | |
| 1113 | - | if jsonOut { | |
| 1114 | - | return writeJSON(cmd.OutOrStdout(), diag) | |
| 1115 | - | } | |
| 1116 | - | return writeRouteDiagnosisText(cmd.OutOrStdout(), diag) | |
| 1104 | + | return runRouteDiagnosis(cmd.Context(), args[0], cmd.OutOrStdout(), jsonOut) | |
| 1117 | 1105 | }, | |
| 1118 | 1106 | } | |
| 1119 | 1107 | cmd.Flags().BoolVar(&jsonOut, "json", false, "Output as JSON") | |
| 1120 | 1108 | return cmd | |
| 1121 | 1109 | } | |
| 1122 | 1110 | ||
| 1111 | + | func runRouteDiagnosis(ctx context.Context, domainName string, out io.Writer, jsonOut bool) error { | |
| 1112 | + | handle, err := resolveControlPlaneForRouteDomain(ctx, domainName) | |
| 1113 | + | if err != nil { | |
| 1114 | + | return err | |
| 1115 | + | } | |
| 1116 | + | defer handle.close() | |
| 1117 | + | diag, err := buildRouteDiagnosis(ctx, handle.plane, domainName) | |
| 1118 | + | if err != nil { | |
| 1119 | + | return err | |
| 1120 | + | } | |
| 1121 | + | if jsonOut { | |
| 1122 | + | return writeJSON(out, diag) | |
| 1123 | + | } | |
| 1124 | + | return writeRouteDiagnosisText(out, diag) | |
| 1125 | + | } | |
| 1126 | + | ||
| 1123 | 1127 | func buildRouteDiagnosis(ctx context.Context, cp ControlPlane, domainName string) (*routeDiagnosis, error) { | |
| 1124 | 1128 | diag := &routeDiagnosis{Domain: domainName} | |
| 1125 | 1129 | loadRouteDiagnosisConfig(ctx, cp, domainName, diag) | |
| @@ -1266,19 +1270,7 @@ func newRoutesPurgeCmd() *cobra.Command { | |||
| 1266 | 1270 | Short: "Review or explicitly purge retained route resources", | |
| 1267 | 1271 | Args: cobra.ExactArgs(1), | |
| 1268 | 1272 | RunE: func(cmd *cobra.Command, args []string) error { | |
| 1269 | - | handle, err := resolveControlPlaneForRouteDomain(cmd.Context(), args[0]) | |
| 1270 | - | if err != nil { | |
| 1271 | - | return err | |
| 1272 | - | } | |
| 1273 | - | defer handle.close() | |
| 1274 | - | report, err := runRoutePurge(cmd.Context(), handle.plane, args[0], routePurgeOptions{Force: force, Attachments: includeAttachments, Volumes: includeVolumes}) | |
| 1275 | - | if err != nil { | |
| 1276 | - | return err | |
| 1277 | - | } | |
| 1278 | - | if jsonOut { | |
| 1279 | - | return writeJSON(cmd.OutOrStdout(), report) | |
| 1280 | - | } | |
| 1281 | - | return writeRoutePurgeText(cmd.OutOrStdout(), report, force) | |
| 1273 | + | return runRoutePurgeCommand(cmd.Context(), args[0], cmd.OutOrStdout(), routePurgeOptions{Force: force, Attachments: includeAttachments, Volumes: includeVolumes}, jsonOut) | |
| 1282 | 1274 | }, | |
| 1283 | 1275 | } | |
| 1284 | 1276 | cmd.Flags().BoolVar(&jsonOut, "json", false, "Output as JSON") | |
| @@ -1294,6 +1286,22 @@ type routePurgeOptions struct { | |||
| 1294 | 1286 | Volumes bool | |
| 1295 | 1287 | } | |
| 1296 | 1288 | ||
| 1289 | + | func runRoutePurgeCommand(ctx context.Context, domainName string, out io.Writer, opts routePurgeOptions, jsonOut bool) error { | |
| 1290 | + | handle, err := resolveControlPlaneForRouteDomain(ctx, domainName) | |
| 1291 | + | if err != nil { | |
| 1292 | + | return err | |
| 1293 | + | } | |
| 1294 | + | defer handle.close() | |
| 1295 | + | report, err := runRoutePurge(ctx, handle.plane, domainName, opts) | |
| 1296 | + | if err != nil { | |
| 1297 | + | return err | |
| 1298 | + | } | |
| 1299 | + | if jsonOut { | |
| 1300 | + | return writeJSON(out, report) | |
| 1301 | + | } | |
| 1302 | + | return writeRoutePurgeText(out, report, opts.Force) | |
| 1303 | + | } | |
| 1304 | + | ||
| 1297 | 1305 | func runRoutePurge(ctx context.Context, cp ControlPlane, domainName string, opts routePurgeOptions) (*domain.CleanupReport, error) { | |
| 1298 | 1306 | diag, err := buildRouteDiagnosis(ctx, cp, domainName) | |
| 1299 | 1307 | if err != nil { | |
| @@ -1303,8 +1311,10 @@ func runRoutePurge(ctx context.Context, cp ControlPlane, domainName string, opts | |||
| 1303 | 1311 | if diag.Runtime != nil && diag.Runtime.ContainerID != "" { | |
| 1304 | 1312 | report.OrphanedEntities = append(report.OrphanedEntities, domain.CleanupOrphanedEntity{Kind: "route_container", ID: diag.Runtime.ContainerID, Status: diag.Runtime.ContainerStatus, Reason: "route runtime state matched purge target"}) | |
| 1305 | 1313 | } | |
| 1306 | - | for _, volume := range diag.Volumes { | |
| 1307 | - | report.PreservedVolumes = append(report.PreservedVolumes, domain.CleanupVolume{Name: volume.Name, Reason: "volume purge requires explicit --volumes and runtime support"}) | |
| 1314 | + | if opts.Volumes { | |
| 1315 | + | for _, volume := range diag.Volumes { | |
| 1316 | + | report.PreservedVolumes = append(report.PreservedVolumes, domain.CleanupVolume{Name: volume.Name, Reason: "volume purge requires explicit --volumes and runtime support"}) | |
| 1317 | + | } | |
| 1308 | 1318 | } | |
| 1309 | 1319 | if !opts.Attachments || !opts.Force { | |
| 1310 | 1320 | report.PreservedAttachments = append(report.PreservedAttachments, diag.OrphanedAttachments...) | |
| @@ -1468,7 +1478,7 @@ Examples: | |||
| 1468 | 1478 | return err | |
| 1469 | 1479 | } | |
| 1470 | 1480 | defer handle.close() | |
| 1471 | - | var cleanup *domain.CleanupReport | |
| 1481 | + | var cleanup *dto.CleanupReport | |
| 1472 | 1482 | if remover, ok := handle.plane.(interface { | |
| 1473 | 1483 | RemoveRouteWithCleanup(context.Context, string) (*dto.RouteDeleteResponse, error) | |
| 1474 | 1484 | }); ok { | |
| @@ -1492,20 +1502,39 @@ Examples: | |||
| 1492 | 1502 | return cmd | |
| 1493 | 1503 | } | |
| 1494 | 1504 | ||
| 1495 | - | func writeRouteRemoveText(out io.Writer, routeDomain string, cleanup *domain.CleanupReport) error { | |
| 1505 | + | func writeRouteRemoveText(out io.Writer, routeDomain string, cleanup *dto.CleanupReport) error { | |
| 1496 | 1506 | if err := cliWriteLine(out, styles.RenderSuccess(fmt.Sprintf("Route removed: %s", routeDomain))); err != nil { | |
| 1497 | 1507 | return err | |
| 1498 | 1508 | } | |
| 1499 | 1509 | if cleanup == nil { | |
| 1500 | 1510 | return nil | |
| 1501 | 1511 | } | |
| 1502 | - | if err := writeRemovedContainers(out, cleanup.RemovedContainers); err != nil { | |
| 1512 | + | cleanupReport := cleanupReportDTOToDomain(cleanup) | |
| 1513 | + | if err := writeRemovedContainers(out, cleanupReport.RemovedContainers); err != nil { | |
| 1503 | 1514 | return err | |
| 1504 | 1515 | } | |
| 1505 | - | if err := writePreservedAttachments(out, cleanup.PreservedAttachments); err != nil { | |
| 1516 | + | if err := writePreservedAttachments(out, cleanupReport.PreservedAttachments); err != nil { | |
| 1506 | 1517 | return err | |
| 1507 | 1518 | } | |
| 1508 | - | return writeCleanupMessages(out, cleanup.Warnings, cleanup.Hints) | |
| 1519 | + | return writeCleanupMessages(out, cleanupReport.Warnings, cleanupReport.Hints) | |
| 1520 | + | } | |
| 1521 | + | ||
| 1522 | + | func cleanupReportDTOToDomain(cleanup *dto.CleanupReport) *domain.CleanupReport { | |
| 1523 | + | if cleanup == nil { | |
| 1524 | + | return nil | |
| 1525 | + | } | |
| 1526 | + | out := &domain.CleanupReport{ | |
| 1527 | + | Domain: cleanup.Domain, | |
| 1528 | + | Warnings: append([]string(nil), cleanup.Warnings...), | |
| 1529 | + | Hints: append([]string(nil), cleanup.Hints...), | |
| 1530 | + | } | |
| 1531 | + | for _, item := range cleanup.RemovedContainers { | |
| 1532 | + | out.RemovedContainers = append(out.RemovedContainers, domain.CleanupContainer(item)) | |
| 1533 | + | } | |
| 1534 | + | for _, item := range cleanup.PreservedAttachments { | |
| 1535 | + | out.PreservedAttachments = append(out.PreservedAttachments, domain.CleanupAttachment(item)) | |
| 1536 | + | } | |
| 1537 | + | return out | |
| 1509 | 1538 | } | |
| 1510 | 1539 | ||
| 1511 | 1540 | func writeRemovedContainers(out io.Writer, containers []domain.CleanupContainer) error { | |
| @@ -1101,25 +1101,29 @@ func newRoutesDiagnoseCmd() *cobra.Command { | |||||
| 1101 | Short: "Diagnose route configuration, runtime state, and cleanup leftovers", | 1101 | Short: "Diagnose route configuration, runtime state, and cleanup leftovers", | ||
| 1102 | Args: cobra.ExactArgs(1), | 1102 | Args: cobra.ExactArgs(1), | ||
| 1103 | RunE: func(cmd *cobra.Command, args []string) error { | 1103 | RunE: func(cmd *cobra.Command, args []string) error { | ||
| 1104 | - | handle, err := resolveControlPlaneForRouteDomain(cmd.Context(), args[0]) | 1104 | + | return runRouteDiagnosis(cmd.Context(), args[0], cmd.OutOrStdout(), jsonOut) |
| 1105 | - | if err != nil { | |||
| 1106 | - | return err | |||
| 1107 | - | } | |||
| 1108 | - | defer handle.close() | |||
| 1109 | - | diag, err := buildRouteDiagnosis(cmd.Context(), handle.plane, args[0]) | |||
| 1110 | - | if err != nil { | |||
| 1111 | - | return err | |||
| 1112 | - | } | |||
| 1113 | - | if jsonOut { | |||
| 1114 | - | return writeJSON(cmd.OutOrStdout(), diag) | |||
| 1115 | - | } | |||
| 1116 | - | return writeRouteDiagnosisText(cmd.OutOrStdout(), diag) | |||
| 1117 | }, | 1105 | }, | ||
| 1118 | } | 1106 | } | ||
| 1119 | cmd.Flags().BoolVar(&jsonOut, "json", false, "Output as JSON") | 1107 | cmd.Flags().BoolVar(&jsonOut, "json", false, "Output as JSON") | ||
| 1120 | return cmd | 1108 | return cmd | ||
| 1121 | } | 1109 | } | ||
| 1122 | 1110 | ||||
| 1111 | + | func runRouteDiagnosis(ctx context.Context, domainName string, out io.Writer, jsonOut bool) error { | |||
| 1112 | + | handle, err := resolveControlPlaneForRouteDomain(ctx, domainName) | |||
| 1113 | + | if err != nil { | |||
| 1114 | + | return err | |||
| 1115 | + | } | |||
| 1116 | + | defer handle.close() | |||
| 1117 | + | diag, err := buildRouteDiagnosis(ctx, handle.plane, domainName) | |||
| 1118 | + | if err != nil { | |||
| 1119 | + | return err | |||
| 1120 | + | } | |||
| 1121 | + | if jsonOut { | |||
| 1122 | + | return writeJSON(out, diag) | |||
| 1123 | + | } | |||
| 1124 | + | return writeRouteDiagnosisText(out, diag) | |||
| 1125 | + | } | |||
| 1126 | + | ||||
| 1123 | func buildRouteDiagnosis(ctx context.Context, cp ControlPlane, domainName string) (*routeDiagnosis, error) { | 1127 | func buildRouteDiagnosis(ctx context.Context, cp ControlPlane, domainName string) (*routeDiagnosis, error) { | ||
| 1124 | diag := &routeDiagnosis{Domain: domainName} | 1128 | diag := &routeDiagnosis{Domain: domainName} | ||
| 1125 | loadRouteDiagnosisConfig(ctx, cp, domainName, diag) | 1129 | loadRouteDiagnosisConfig(ctx, cp, domainName, diag) | ||
| @@ -1266,19 +1270,7 @@ func newRoutesPurgeCmd() *cobra.Command { | |||||
| 1266 | Short: "Review or explicitly purge retained route resources", | 1270 | Short: "Review or explicitly purge retained route resources", | ||
| 1267 | Args: cobra.ExactArgs(1), | 1271 | Args: cobra.ExactArgs(1), | ||
| 1268 | RunE: func(cmd *cobra.Command, args []string) error { | 1272 | RunE: func(cmd *cobra.Command, args []string) error { | ||
| 1269 | - | handle, err := resolveControlPlaneForRouteDomain(cmd.Context(), args[0]) | 1273 | + | return runRoutePurgeCommand(cmd.Context(), args[0], cmd.OutOrStdout(), routePurgeOptions{Force: force, Attachments: includeAttachments, Volumes: includeVolumes}, jsonOut) |
| 1270 | - | if err != nil { | |||
| 1271 | - | return err | |||
| 1272 | - | } | |||
| 1273 | - | defer handle.close() | |||
| 1274 | - | report, err := runRoutePurge(cmd.Context(), handle.plane, args[0], routePurgeOptions{Force: force, Attachments: includeAttachments, Volumes: includeVolumes}) | |||
| 1275 | - | if err != nil { | |||
| 1276 | - | return err | |||
| 1277 | - | } | |||
| 1278 | - | if jsonOut { | |||
| 1279 | - | return writeJSON(cmd.OutOrStdout(), report) | |||
| 1280 | - | } | |||
| 1281 | - | return writeRoutePurgeText(cmd.OutOrStdout(), report, force) | |||
| 1282 | }, | 1274 | }, | ||
| 1283 | } | 1275 | } | ||
| 1284 | cmd.Flags().BoolVar(&jsonOut, "json", false, "Output as JSON") | 1276 | cmd.Flags().BoolVar(&jsonOut, "json", false, "Output as JSON") | ||
| @@ -1294,6 +1286,22 @@ type routePurgeOptions struct { | |||||
| 1294 | Volumes bool | 1286 | Volumes bool | ||
| 1295 | } | 1287 | } | ||
| 1296 | 1288 | ||||
| 1289 | + | func runRoutePurgeCommand(ctx context.Context, domainName string, out io.Writer, opts routePurgeOptions, jsonOut bool) error { | |||
| 1290 | + | handle, err := resolveControlPlaneForRouteDomain(ctx, domainName) | |||
| 1291 | + | if err != nil { | |||
| 1292 | + | return err | |||
| 1293 | + | } | |||
| 1294 | + | defer handle.close() | |||
| 1295 | + | report, err := runRoutePurge(ctx, handle.plane, domainName, opts) | |||
| 1296 | + | if err != nil { | |||
| 1297 | + | return err | |||
| 1298 | + | } | |||
| 1299 | + | if jsonOut { | |||
| 1300 | + | return writeJSON(out, report) | |||
| 1301 | + | } | |||
| 1302 | + | return writeRoutePurgeText(out, report, opts.Force) | |||
| 1303 | + | } | |||
| 1304 | + | ||||
| 1297 | func runRoutePurge(ctx context.Context, cp ControlPlane, domainName string, opts routePurgeOptions) (*domain.CleanupReport, error) { | 1305 | func runRoutePurge(ctx context.Context, cp ControlPlane, domainName string, opts routePurgeOptions) (*domain.CleanupReport, error) { | ||
| 1298 | diag, err := buildRouteDiagnosis(ctx, cp, domainName) | 1306 | diag, err := buildRouteDiagnosis(ctx, cp, domainName) | ||
| 1299 | if err != nil { | 1307 | if err != nil { | ||
| @@ -1303,8 +1311,10 @@ func runRoutePurge(ctx context.Context, cp ControlPlane, domainName string, opts | |||||
| 1303 | if diag.Runtime != nil && diag.Runtime.ContainerID != "" { | 1311 | if diag.Runtime != nil && diag.Runtime.ContainerID != "" { | ||
| 1304 | report.OrphanedEntities = append(report.OrphanedEntities, domain.CleanupOrphanedEntity{Kind: "route_container", ID: diag.Runtime.ContainerID, Status: diag.Runtime.ContainerStatus, Reason: "route runtime state matched purge target"}) | 1312 | report.OrphanedEntities = append(report.OrphanedEntities, domain.CleanupOrphanedEntity{Kind: "route_container", ID: diag.Runtime.ContainerID, Status: diag.Runtime.ContainerStatus, Reason: "route runtime state matched purge target"}) | ||
| 1305 | } | 1313 | } | ||
| 1306 | - | for _, volume := range diag.Volumes { | 1314 | + | if opts.Volumes { |
| 1307 | - | report.PreservedVolumes = append(report.PreservedVolumes, domain.CleanupVolume{Name: volume.Name, Reason: "volume purge requires explicit --volumes and runtime support"}) | 1315 | + | for _, volume := range diag.Volumes { |
| 1316 | + | report.PreservedVolumes = append(report.PreservedVolumes, domain.CleanupVolume{Name: volume.Name, Reason: "volume purge requires explicit --volumes and runtime support"}) | |||
| 1317 | + | } | |||
| 1308 | } | 1318 | } | ||
| 1309 | if !opts.Attachments || !opts.Force { | 1319 | if !opts.Attachments || !opts.Force { | ||
| 1310 | report.PreservedAttachments = append(report.PreservedAttachments, diag.OrphanedAttachments...) | 1320 | report.PreservedAttachments = append(report.PreservedAttachments, diag.OrphanedAttachments...) | ||
| @@ -1468,7 +1478,7 @@ Examples: | |||||
| 1468 | return err | 1478 | return err | ||
| 1469 | } | 1479 | } | ||
| 1470 | defer handle.close() | 1480 | defer handle.close() | ||
| 1471 | - | var cleanup *domain.CleanupReport | 1481 | + | var cleanup *dto.CleanupReport |
| 1472 | if remover, ok := handle.plane.(interface { | 1482 | if remover, ok := handle.plane.(interface { | ||
| 1473 | RemoveRouteWithCleanup(context.Context, string) (*dto.RouteDeleteResponse, error) | 1483 | RemoveRouteWithCleanup(context.Context, string) (*dto.RouteDeleteResponse, error) | ||
| 1474 | }); ok { | 1484 | }); ok { | ||
| @@ -1492,20 +1502,39 @@ Examples: | |||||
| 1492 | return cmd | 1502 | return cmd | ||
| 1493 | } | 1503 | } | ||
| 1494 | 1504 | ||||
| 1495 | - | func writeRouteRemoveText(out io.Writer, routeDomain string, cleanup *domain.CleanupReport) error { | 1505 | + | func writeRouteRemoveText(out io.Writer, routeDomain string, cleanup *dto.CleanupReport) error { |
| 1496 | if err := cliWriteLine(out, styles.RenderSuccess(fmt.Sprintf("Route removed: %s", routeDomain))); err != nil { | 1506 | if err := cliWriteLine(out, styles.RenderSuccess(fmt.Sprintf("Route removed: %s", routeDomain))); err != nil { | ||
| 1497 | return err | 1507 | return err | ||
| 1498 | } | 1508 | } | ||
| 1499 | if cleanup == nil { | 1509 | if cleanup == nil { | ||
| 1500 | return nil | 1510 | return nil | ||
| 1501 | } | 1511 | } | ||
| 1502 | - | if err := writeRemovedContainers(out, cleanup.RemovedContainers); err != nil { | 1512 | + | cleanupReport := cleanupReportDTOToDomain(cleanup) |
| 1513 | + | if err := writeRemovedContainers(out, cleanupReport.RemovedContainers); err != nil { | |||
| 1503 | return err | 1514 | return err | ||
| 1504 | } | 1515 | } | ||
| 1505 | - | if err := writePreservedAttachments(out, cleanup.PreservedAttachments); err != nil { | 1516 | + | if err := writePreservedAttachments(out, cleanupReport.PreservedAttachments); err != nil { |
| 1506 | return err | 1517 | return err | ||
| 1507 | } | 1518 | } | ||
| 1508 | - | return writeCleanupMessages(out, cleanup.Warnings, cleanup.Hints) | 1519 | + | return writeCleanupMessages(out, cleanupReport.Warnings, cleanupReport.Hints) |
| 1520 | + | } | |||
| 1521 | + | ||||
| 1522 | + | func cleanupReportDTOToDomain(cleanup *dto.CleanupReport) *domain.CleanupReport { | |||
| 1523 | + | if cleanup == nil { | |||
| 1524 | + | return nil | |||
| 1525 | + | } | |||
| 1526 | + | out := &domain.CleanupReport{ | |||
| 1527 | + | Domain: cleanup.Domain, | |||
| 1528 | + | Warnings: append([]string(nil), cleanup.Warnings...), | |||
| 1529 | + | Hints: append([]string(nil), cleanup.Hints...), | |||
| 1530 | + | } | |||
| 1531 | + | for _, item := range cleanup.RemovedContainers { | |||
| 1532 | + | out.RemovedContainers = append(out.RemovedContainers, domain.CleanupContainer(item)) | |||
| 1533 | + | } | |||
| 1534 | + | for _, item := range cleanup.PreservedAttachments { | |||
| 1535 | + | out.PreservedAttachments = append(out.PreservedAttachments, domain.CleanupAttachment(item)) | |||
| 1536 | + | } | |||
| 1537 | + | return out | |||
| 1509 | } | 1538 | } | ||
| 1510 | 1539 | ||||
| 1511 | func writeRemovedContainers(out io.Writer, containers []domain.CleanupContainer) error { | 1540 | func writeRemovedContainers(out io.Writer, containers []domain.CleanupContainer) error { | ||
M internal/adapters/in/cli/routes_test.go +2 -1
| @@ -13,6 +13,7 @@ import ( | |||
| 13 | 13 | "sync/atomic" | |
| 14 | 14 | "testing" | |
| 15 | 15 | ||
| 16 | + | "github.com/bnema/gordon/internal/adapters/dto" | |
| 16 | 17 | climocks "github.com/bnema/gordon/internal/adapters/in/cli/mocks" | |
| 17 | 18 | "github.com/bnema/gordon/internal/adapters/in/cli/remote" | |
| 18 | 19 | "github.com/bnema/gordon/internal/adapters/in/cli/ui/components" | |
| @@ -911,7 +912,7 @@ func TestWriteRouteRemoveTextShowsCleanupReport(t *testing.T) { | |||
| 911 | 912 | Hints: []string{"run diagnose"}, | |
| 912 | 913 | } | |
| 913 | 914 | ||
| 914 | - | err := writeRouteRemoveText(&out, "app.example.com", report) | |
| 915 | + | err := writeRouteRemoveText(&out, "app.example.com", dto.CleanupReportFromDomain(report)) | |
| 915 | 916 | require.NoError(t, err) | |
| 916 | 917 | rendered := stripANSI(out.String()) | |
| 917 | 918 | assert.Contains(t, rendered, "Route removed: app.example.com") | |
| @@ -13,6 +13,7 @@ import ( | |||||
| 13 | "sync/atomic" | 13 | "sync/atomic" | ||
| 14 | "testing" | 14 | "testing" | ||
| 15 | 15 | ||||
| 16 | + | "github.com/bnema/gordon/internal/adapters/dto" | |||
| 16 | climocks "github.com/bnema/gordon/internal/adapters/in/cli/mocks" | 17 | climocks "github.com/bnema/gordon/internal/adapters/in/cli/mocks" | ||
| 17 | "github.com/bnema/gordon/internal/adapters/in/cli/remote" | 18 | "github.com/bnema/gordon/internal/adapters/in/cli/remote" | ||
| 18 | "github.com/bnema/gordon/internal/adapters/in/cli/ui/components" | 19 | "github.com/bnema/gordon/internal/adapters/in/cli/ui/components" | ||
| @@ -911,7 +912,7 @@ func TestWriteRouteRemoveTextShowsCleanupReport(t *testing.T) { | |||||
| 911 | Hints: []string{"run diagnose"}, | 912 | Hints: []string{"run diagnose"}, | ||
| 912 | } | 913 | } | ||
| 913 | 914 | ||||
| 914 | - | err := writeRouteRemoveText(&out, "app.example.com", report) | 915 | + | err := writeRouteRemoveText(&out, "app.example.com", dto.CleanupReportFromDomain(report)) |
| 915 | require.NoError(t, err) | 916 | require.NoError(t, err) | ||
| 916 | rendered := stripANSI(out.String()) | 917 | rendered := stripANSI(out.String()) | ||
| 917 | assert.Contains(t, rendered, "Route removed: app.example.com") | 918 | assert.Contains(t, rendered, "Route removed: app.example.com") | ||
M internal/adapters/in/http/admin/handler.go +2 -2
| @@ -793,7 +793,7 @@ func (h *Handler) handleRoutesDelete(w http.ResponseWriter, r *http.Request, rou | |||
| 793 | 793 | } | |
| 794 | 794 | } | |
| 795 | 795 | ||
| 796 | - | var cleanup *domain.CleanupReport | |
| 796 | + | var cleanup *dto.CleanupReport | |
| 797 | 797 | if h.containerSvc != nil { | |
| 798 | 798 | report, err := h.containerSvc.ReconcileRemovedRoute(ctx, routeDomain) | |
| 799 | 799 | if err != nil { | |
| @@ -801,7 +801,7 @@ func (h *Handler) handleRoutesDelete(w http.ResponseWriter, r *http.Request, rou | |||
| 801 | 801 | h.sendError(w, http.StatusInternalServerError, "route removed but runtime cleanup failed") | |
| 802 | 802 | return | |
| 803 | 803 | } | |
| 804 | - | cleanup = report | |
| 804 | + | cleanup = dto.CleanupReportFromDomain(report) | |
| 805 | 805 | } | |
| 806 | 806 | ||
| 807 | 807 | log.Info().Str("domain", routeDomain).Msg("route removed") | |
| @@ -793,7 +793,7 @@ func (h *Handler) handleRoutesDelete(w http.ResponseWriter, r *http.Request, rou | |||||
| 793 | } | 793 | } | ||
| 794 | } | 794 | } | ||
| 795 | 795 | ||||
| 796 | - | var cleanup *domain.CleanupReport | 796 | + | var cleanup *dto.CleanupReport |
| 797 | if h.containerSvc != nil { | 797 | if h.containerSvc != nil { | ||
| 798 | report, err := h.containerSvc.ReconcileRemovedRoute(ctx, routeDomain) | 798 | report, err := h.containerSvc.ReconcileRemovedRoute(ctx, routeDomain) | ||
| 799 | if err != nil { | 799 | if err != nil { | ||
| @@ -801,7 +801,7 @@ func (h *Handler) handleRoutesDelete(w http.ResponseWriter, r *http.Request, rou | |||||
| 801 | h.sendError(w, http.StatusInternalServerError, "route removed but runtime cleanup failed") | 801 | h.sendError(w, http.StatusInternalServerError, "route removed but runtime cleanup failed") | ||
| 802 | return | 802 | return | ||
| 803 | } | 803 | } | ||
| 804 | - | cleanup = report | 804 | + | cleanup = dto.CleanupReportFromDomain(report) |
| 805 | } | 805 | } | ||
| 806 | 806 | ||||
| 807 | log.Info().Str("domain", routeDomain).Msg("route removed") | 807 | log.Info().Str("domain", routeDomain).Msg("route removed") | ||
M internal/adapters/in/http/admin/handler_test.go +28 -23
| @@ -720,8 +720,8 @@ func TestHandler_RoutesDelete_RequiresWriteScope(t *testing.T) { | |||
| 720 | 720 | for _, tt := range tests { | |
| 721 | 721 | t.Run(tt.name, func(t *testing.T) { | |
| 722 | 722 | if tt.wantStatus == http.StatusOK { | |
| 723 | - | configSvc.EXPECT().RemoveRoute(mock.Anything, "app.example.com").Return(nil).Maybe() | |
| 724 | - | containerSvc.EXPECT().ReconcileRemovedRoute(mock.Anything, "app.example.com").Return(&domain.CleanupReport{Domain: "app.example.com"}, nil).Maybe() | |
| 723 | + | configSvc.EXPECT().RemoveRoute(mock.Anything, "app.example.com").Return(nil).Once() | |
| 724 | + | containerSvc.EXPECT().ReconcileRemovedRoute(mock.Anything, "app.example.com").Return(&domain.CleanupReport{Domain: "app.example.com"}, nil).Once() | |
| 725 | 725 | } | |
| 726 | 726 | ||
| 727 | 727 | req := httptest.NewRequest("DELETE", "/admin/routes/app.example.com", nil) | |
| @@ -751,16 +751,17 @@ func TestHandler_RoutesDelete_ReconcilesRuntimeWhenRouteAlreadyMissing(t *testin | |||
| 751 | 751 | d.ContainerSvc = containerSvc | |
| 752 | 752 | d.SecretSvc = secretSvc | |
| 753 | 753 | }) | |
| 754 | + | server := newScopedTestServer(t, handler, "admin:routes:write") | |
| 754 | 755 | ||
| 755 | - | req := httptest.NewRequest("DELETE", "/admin/routes/app.example.com", nil) | |
| 756 | - | req = req.WithContext(ctxWithScopes("admin:routes:write")) | |
| 757 | - | rec := httptest.NewRecorder() | |
| 758 | - | ||
| 759 | - | handler.ServeHTTP(rec, req) | |
| 756 | + | req, err := http.NewRequest(http.MethodDelete, server.URL+"/admin/routes/app.example.com", nil) | |
| 757 | + | require.NoError(t, err) | |
| 758 | + | respHTTP, err := http.DefaultClient.Do(req) | |
| 759 | + | require.NoError(t, err) | |
| 760 | + | defer respHTTP.Body.Close() | |
| 760 | 761 | ||
| 761 | - | require.Equal(t, http.StatusOK, rec.Code) | |
| 762 | + | require.Equal(t, http.StatusOK, respHTTP.StatusCode) | |
| 762 | 763 | var resp dto.RouteDeleteResponse | |
| 763 | - | require.NoError(t, json.NewDecoder(rec.Body).Decode(&resp)) | |
| 764 | + | require.NoError(t, json.NewDecoder(respHTTP.Body).Decode(&resp)) | |
| 764 | 765 | require.NotNil(t, resp.Cleanup) | |
| 765 | 766 | assert.Equal(t, "app.example.com", resp.Cleanup.Domain) | |
| 766 | 767 | } | |
| @@ -780,15 +781,18 @@ func TestHandler_RoutesDelete_ReturnsServerErrorWhenRuntimeCleanupFails(t *testi | |||
| 780 | 781 | d.ContainerSvc = containerSvc | |
| 781 | 782 | d.SecretSvc = secretSvc | |
| 782 | 783 | }) | |
| 784 | + | server := newScopedTestServer(t, handler, "admin:routes:write") | |
| 783 | 785 | ||
| 784 | - | req := httptest.NewRequest("DELETE", "/admin/routes/app.example.com", nil) | |
| 785 | - | req = req.WithContext(ctxWithScopes("admin:routes:write")) | |
| 786 | - | rec := httptest.NewRecorder() | |
| 787 | - | ||
| 788 | - | handler.ServeHTTP(rec, req) | |
| 786 | + | req, err := http.NewRequest(http.MethodDelete, server.URL+"/admin/routes/app.example.com", nil) | |
| 787 | + | require.NoError(t, err) | |
| 788 | + | resp, err := http.DefaultClient.Do(req) | |
| 789 | + | require.NoError(t, err) | |
| 790 | + | defer resp.Body.Close() | |
| 791 | + | body, err := io.ReadAll(resp.Body) | |
| 792 | + | require.NoError(t, err) | |
| 789 | 793 | ||
| 790 | - | assert.Equal(t, http.StatusInternalServerError, rec.Code) | |
| 791 | - | assert.Contains(t, rec.Body.String(), "route removed but runtime cleanup failed") | |
| 794 | + | assert.Equal(t, http.StatusInternalServerError, resp.StatusCode) | |
| 795 | + | assert.Contains(t, string(body), "route removed but runtime cleanup failed") | |
| 792 | 796 | } | |
| 793 | 797 | ||
| 794 | 798 | func TestHandler_RoutesDelete_ReconcilesRuntimeAndReturnsCleanupReport(t *testing.T) { | |
| @@ -820,16 +824,17 @@ func TestHandler_RoutesDelete_ReconcilesRuntimeAndReturnsCleanupReport(t *testin | |||
| 820 | 824 | d.ContainerSvc = containerSvc | |
| 821 | 825 | d.SecretSvc = secretSvc | |
| 822 | 826 | }) | |
| 827 | + | server := newScopedTestServer(t, handler, "admin:routes:write") | |
| 823 | 828 | ||
| 824 | - | req := httptest.NewRequest("DELETE", "/admin/routes/app.example.com", nil) | |
| 825 | - | req = req.WithContext(ctxWithScopes("admin:routes:write")) | |
| 826 | - | rec := httptest.NewRecorder() | |
| 827 | - | ||
| 828 | - | handler.ServeHTTP(rec, req) | |
| 829 | + | req, err := http.NewRequest(http.MethodDelete, server.URL+"/admin/routes/app.example.com", nil) | |
| 830 | + | require.NoError(t, err) | |
| 831 | + | respHTTP, err := http.DefaultClient.Do(req) | |
| 832 | + | require.NoError(t, err) | |
| 833 | + | defer respHTTP.Body.Close() | |
| 829 | 834 | ||
| 830 | - | require.Equal(t, http.StatusOK, rec.Code) | |
| 835 | + | require.Equal(t, http.StatusOK, respHTTP.StatusCode) | |
| 831 | 836 | var resp dto.RouteDeleteResponse | |
| 832 | - | require.NoError(t, json.NewDecoder(rec.Body).Decode(&resp)) | |
| 837 | + | require.NoError(t, json.NewDecoder(respHTTP.Body).Decode(&resp)) | |
| 833 | 838 | assert.Equal(t, "removed", resp.Status) | |
| 834 | 839 | require.NotNil(t, resp.Cleanup) | |
| 835 | 840 | assert.Equal(t, "app.example.com", resp.Cleanup.Domain) | |
| @@ -720,8 +720,8 @@ func TestHandler_RoutesDelete_RequiresWriteScope(t *testing.T) { | |||||
| 720 | for _, tt := range tests { | 720 | for _, tt := range tests { | ||
| 721 | t.Run(tt.name, func(t *testing.T) { | 721 | t.Run(tt.name, func(t *testing.T) { | ||
| 722 | if tt.wantStatus == http.StatusOK { | 722 | if tt.wantStatus == http.StatusOK { | ||
| 723 | - | configSvc.EXPECT().RemoveRoute(mock.Anything, "app.example.com").Return(nil).Maybe() | 723 | + | configSvc.EXPECT().RemoveRoute(mock.Anything, "app.example.com").Return(nil).Once() |
| 724 | - | containerSvc.EXPECT().ReconcileRemovedRoute(mock.Anything, "app.example.com").Return(&domain.CleanupReport{Domain: "app.example.com"}, nil).Maybe() | 724 | + | containerSvc.EXPECT().ReconcileRemovedRoute(mock.Anything, "app.example.com").Return(&domain.CleanupReport{Domain: "app.example.com"}, nil).Once() |
| 725 | } | 725 | } | ||
| 726 | 726 | ||||
| 727 | req := httptest.NewRequest("DELETE", "/admin/routes/app.example.com", nil) | 727 | req := httptest.NewRequest("DELETE", "/admin/routes/app.example.com", nil) | ||
| @@ -751,16 +751,17 @@ func TestHandler_RoutesDelete_ReconcilesRuntimeWhenRouteAlreadyMissing(t *testin | |||||
| 751 | d.ContainerSvc = containerSvc | 751 | d.ContainerSvc = containerSvc | ||
| 752 | d.SecretSvc = secretSvc | 752 | d.SecretSvc = secretSvc | ||
| 753 | }) | 753 | }) | ||
| 754 | + | server := newScopedTestServer(t, handler, "admin:routes:write") | |||
| 754 | 755 | ||||
| 755 | - | req := httptest.NewRequest("DELETE", "/admin/routes/app.example.com", nil) | 756 | + | req, err := http.NewRequest(http.MethodDelete, server.URL+"/admin/routes/app.example.com", nil) |
| 756 | - | req = req.WithContext(ctxWithScopes("admin:routes:write")) | 757 | + | require.NoError(t, err) |
| 757 | - | rec := httptest.NewRecorder() | 758 | + | respHTTP, err := http.DefaultClient.Do(req) |
| 758 | - | 759 | + | require.NoError(t, err) | |
| 759 | - | handler.ServeHTTP(rec, req) | 760 | + | defer respHTTP.Body.Close() |
| 760 | 761 | ||||
| 761 | - | require.Equal(t, http.StatusOK, rec.Code) | 762 | + | require.Equal(t, http.StatusOK, respHTTP.StatusCode) |
| 762 | var resp dto.RouteDeleteResponse | 763 | var resp dto.RouteDeleteResponse | ||
| 763 | - | require.NoError(t, json.NewDecoder(rec.Body).Decode(&resp)) | 764 | + | require.NoError(t, json.NewDecoder(respHTTP.Body).Decode(&resp)) |
| 764 | require.NotNil(t, resp.Cleanup) | 765 | require.NotNil(t, resp.Cleanup) | ||
| 765 | assert.Equal(t, "app.example.com", resp.Cleanup.Domain) | 766 | assert.Equal(t, "app.example.com", resp.Cleanup.Domain) | ||
| 766 | } | 767 | } | ||
| @@ -780,15 +781,18 @@ func TestHandler_RoutesDelete_ReturnsServerErrorWhenRuntimeCleanupFails(t *testi | |||||
| 780 | d.ContainerSvc = containerSvc | 781 | d.ContainerSvc = containerSvc | ||
| 781 | d.SecretSvc = secretSvc | 782 | d.SecretSvc = secretSvc | ||
| 782 | }) | 783 | }) | ||
| 784 | + | server := newScopedTestServer(t, handler, "admin:routes:write") | |||
| 783 | 785 | ||||
| 784 | - | req := httptest.NewRequest("DELETE", "/admin/routes/app.example.com", nil) | 786 | + | req, err := http.NewRequest(http.MethodDelete, server.URL+"/admin/routes/app.example.com", nil) |
| 785 | - | req = req.WithContext(ctxWithScopes("admin:routes:write")) | 787 | + | require.NoError(t, err) |
| 786 | - | rec := httptest.NewRecorder() | 788 | + | resp, err := http.DefaultClient.Do(req) |
| 787 | - | 789 | + | require.NoError(t, err) | |
| 788 | - | handler.ServeHTTP(rec, req) | 790 | + | defer resp.Body.Close() |
| 791 | + | body, err := io.ReadAll(resp.Body) | |||
| 792 | + | require.NoError(t, err) | |||
| 789 | 793 | ||||
| 790 | - | assert.Equal(t, http.StatusInternalServerError, rec.Code) | 794 | + | assert.Equal(t, http.StatusInternalServerError, resp.StatusCode) |
| 791 | - | assert.Contains(t, rec.Body.String(), "route removed but runtime cleanup failed") | 795 | + | assert.Contains(t, string(body), "route removed but runtime cleanup failed") |
| 792 | } | 796 | } | ||
| 793 | 797 | ||||
| 794 | func TestHandler_RoutesDelete_ReconcilesRuntimeAndReturnsCleanupReport(t *testing.T) { | 798 | func TestHandler_RoutesDelete_ReconcilesRuntimeAndReturnsCleanupReport(t *testing.T) { | ||
| @@ -820,16 +824,17 @@ func TestHandler_RoutesDelete_ReconcilesRuntimeAndReturnsCleanupReport(t *testin | |||||
| 820 | d.ContainerSvc = containerSvc | 824 | d.ContainerSvc = containerSvc | ||
| 821 | d.SecretSvc = secretSvc | 825 | d.SecretSvc = secretSvc | ||
| 822 | }) | 826 | }) | ||
| 827 | + | server := newScopedTestServer(t, handler, "admin:routes:write") | |||
| 823 | 828 | ||||
| 824 | - | req := httptest.NewRequest("DELETE", "/admin/routes/app.example.com", nil) | 829 | + | req, err := http.NewRequest(http.MethodDelete, server.URL+"/admin/routes/app.example.com", nil) |
| 825 | - | req = req.WithContext(ctxWithScopes("admin:routes:write")) | 830 | + | require.NoError(t, err) |
| 826 | - | rec := httptest.NewRecorder() | 831 | + | respHTTP, err := http.DefaultClient.Do(req) |
| 827 | - | 832 | + | require.NoError(t, err) | |
| 828 | - | handler.ServeHTTP(rec, req) | 833 | + | defer respHTTP.Body.Close() |
| 829 | 834 | ||||
| 830 | - | require.Equal(t, http.StatusOK, rec.Code) | 835 | + | require.Equal(t, http.StatusOK, respHTTP.StatusCode) |
| 831 | var resp dto.RouteDeleteResponse | 836 | var resp dto.RouteDeleteResponse | ||
| 832 | - | require.NoError(t, json.NewDecoder(rec.Body).Decode(&resp)) | 837 | + | require.NoError(t, json.NewDecoder(respHTTP.Body).Decode(&resp)) |
| 833 | assert.Equal(t, "removed", resp.Status) | 838 | assert.Equal(t, "removed", resp.Status) | ||
| 834 | require.NotNil(t, resp.Cleanup) | 839 | require.NotNil(t, resp.Cleanup) | ||
| 835 | assert.Equal(t, "app.example.com", resp.Cleanup.Domain) | 840 | assert.Equal(t, "app.example.com", resp.Cleanup.Domain) | ||