detached 1290c78

Merge pull request #200 from bnema/feat/deletion-cleanup-lifecycle

feat(cleanup): reconcile removed runtime entities
commit
1290c78385689a2a48bc154a35eab9e21db38bd1
author
bnema · · 2026-05-22 04:15 UTC
committer
GitHub · · 2026-05-22 04:15 UTC
merge of
4106bb1 + 33a66ec · diff against the first parent
signature
GPG UnverifiedB5690EEEBB952194 The key is unknown
29 files · +5128 -41
M .mockery.yaml +1 -0
@@ -52,3 +52,4 @@ packages:
5252 github.com/bnema/gordon/internal/adapters/in/cli:
5353 interfaces:
5454 pushImageOps:
55+ ControlPlane:
M docs/cli/attachments.md +67 -0
@@ -24,6 +24,8 @@ The CLI will warn you if you try to add an attachment without network isolation
2424 | `add` | Add an attachment to a domain or network group |
2525 | `push` | Build or push attachment images to the Gordon registry |
2626 | `remove` | Remove an attachment from a domain or network group |
27+| `orphans` | List running attachment containers no longer configured |
28+| `prune` | Dry-run or stop orphaned attachment containers while preserving volumes |
2729
2830 ### Alias
2931
@@ -93,6 +95,71 @@ gordon attachments list --json
9395
9496 ---
9597
98+## gordon attachments orphans
99+
100+List running attachment containers that are no longer configured.
101+
102+```bash
103+gordon attachments orphans
104+gordon attachments orphans --json
105+```
106+
107+### Output
108+
109+| Container ID | Name | Image | Status | Owner |
110+|--------------|------|-------|--------|-------|
111+| `pg123` | `postgres` | `postgres:16` | `running` | `myapp.example.com` |
112+
113+### JSON Output
114+
115+```json
116+{
117+ "attachments": [
118+ {
119+ "container_id": "pg123",
120+ "name": "postgres",
121+ "image": "postgres:16",
122+ "status": "running",
123+ "owner": "myapp.example.com"
124+ }
125+ ]
126+}
127+```
128+
129+## gordon attachments prune
130+
131+Lists orphaned attachments by default (dry-run). Use `--stop` to stop and remove the orphaned containers while preserving attachment volumes/data.
132+
133+```bash
134+gordon attachments prune # dry-run/report only
135+gordon attachments prune --stop # stop/remove orphaned containers, preserve volumes
136+```
137+
138+### Output
139+
140+```text
141+Orphaned attachments
142+ postgres -> postgres:16
143+Hint: orphaned attachments preserved; rerun with --stop to stop/remove containers while preserving volumes
144+```
145+
146+### Output with `--stop`
147+
148+```text
149+Removed orphaned attachment containers:
150+ postgres
151+Hint: attachment volumes and data were preserved
152+```
153+
154+### Options
155+
156+| Option | Description |
157+|--------|-------------|
158+| `--stop` | Stop and remove orphaned attachment containers while preserving volumes/data |
159+| `--json` | Output the cleanup report as JSON |
160+| `--remote, -r` | Remote name or URL (e.g., prod, https://gordon.mydomain.com) |
161+| `--token` | Authentication token for remote |
162+
96163 ## gordon attachments add
97164
98165 Add an attachment to a domain or network group.
M docs/cli/routes.md +91 -0
@@ -275,10 +275,92 @@ gordon routes show myapp.example.com --remote https://gordon.mydomain.com --toke
275275
276276 ---
277277
278+## gordon routes diagnose
279+
280+Diagnose route configuration, runtime state, preserved volumes, and orphaned attachment containers.
281+
282+```bash
283+gordon routes diagnose <domain>
284+gordon routes diagnose myapp.example.com --json
285+```
286+
287+Use this after route deletion or failed deployment to see whether runtime state still exists and which safe cleanup commands to run next.
288+
289+### Output
290+
291+```text
292+Route diagnosis: myapp.example.com
293+Image: myapp:latest
294+Container: running abc123def456
295+Preserved volume: gordon-myapp-example-com-data
296+Orphaned attachment: postgres
297+Hint: persistent volumes are preserved by default
298+Hint: run 'gordon attachments prune --stop' to stop orphaned attachment containers while preserving volumes
299+```
300+
301+### JSON Output
302+
303+```json
304+{
305+ "domain": "myapp.example.com",
306+ "configured": true,
307+ "route": {"domain": "myapp.example.com", "image": "myapp:latest", "https": true},
308+ "runtime": {"domain": "myapp.example.com", "container_id": "abc123def456", "container_status": "running"},
309+ "volumes": [{"name": "gordon-myapp-example-com-data", "in_use": false}],
310+ "orphaned_attachments": [{"container_id": "pg123", "name": "postgres", "image": "postgres:16", "status": "running", "owner": "myapp.example.com"}],
311+ "hints": ["persistent volumes are preserved by default"]
312+}
313+```
314+
315+### Next Steps
316+
317+```bash
318+gordon routes purge myapp.example.com # dry-run retained resources
319+gordon routes purge myapp.example.com --attachments --force
320+```
321+
322+---
323+
324+## gordon routes purge
325+
326+Review retained resources for a route and execute supported explicit cleanup actions only when forced.
327+
328+```bash
329+gordon routes purge myapp.example.com # dry-run
330+gordon routes purge myapp.example.com --attachments --force
331+gordon routes purge myapp.example.com --volumes # report preserved volumes for manual review
332+```
333+
334+### Dry-run Output
335+
336+```text
337+Purge dry-run: myapp.example.com
338+Preserved attachments:
339+ postgres (running)
340+Purge candidate: route_container abc123def456
341+Preserved volume: gordon-myapp-example-com-data
342+Hint: dry-run only; add --force with explicit category flags to execute supported purge actions
343+```
344+
345+### Forced Attachment Cleanup Output
346+
347+```text
348+Purge completed: myapp.example.com
349+Removed containers:
350+ postgres
351+Hint: attachment volumes and data were preserved
352+```
353+
354+Purge is intentionally conservative. Volumes are reported and preserved unless a dedicated volume deletion flow supports safe targeted deletion.
355+
356+---
357+
278358 ## gordon routes remove
279359
280360 Remove a route.
281361
362+Route removal is safe by default. Gordon removes the route from configuration and reconciles the active route container so it is no longer served or restarted by Gordon's monitor. Stateful data is preserved. Volumes and attachment data are not deleted unless a separate explicit purge flow requests destructive cleanup.
363+
282364 ```bash
283365 gordon routes remove <domain>
284366 gordon routes remove myapp.example.com
@@ -305,6 +387,15 @@ gordon routes remove myapp.example.com
305387
306388 # Remote (override)
307389 gordon routes remove myapp.example.com --remote https://gordon.mydomain.com --token $TOKEN
390+
391+# Typical output
392+Route removed: myapp.example.com
393+
394+Removed containers:
395+ gordon-myapp.example.com (abc123def456)
396+
397+Preserved attachments:
398+ postgres (running)
308399 ```
309400
310401 ---
M docs/concepts.md +22 -0
@@ -59,6 +59,28 @@ New Container: [══════════════════
5959 ↑ start ↑ traffic routed
6060 ```
6161
62+## Deletion and Cleanup Lifecycle
63+
64+Gordon separates configuration removal from destructive data cleanup:
65+
66+- **configured** — an entity exists in Gordon configuration, such as a route in `gordon.toml`.
67+- **active** — an entity has runtime state, such as a running Gordon-managed container.
68+- **preserved** — state intentionally kept after configuration removal, such as volumes or attachment data.
69+- **orphaned** — runtime state no longer referenced by configuration and requiring follow-up cleanup or diagnosis.
70+- **purged** — state explicitly deleted by a destructive cleanup command.
71+- **retained** — preserved state that Gordon reports so operators can decide whether to keep or purge it later.
72+
73+Safe deletion is the default. Removing a route reconciles active route containers so the app is no longer served or restarted by Gordon's monitor. Stateful data such as volumes and attachment data is preserved unless a later purge command explicitly requests deletion.
74+
75+Cleanup reports use additive-only JSON fields so humans and automation can rely on stable keys while Gordon adds more details over time.
76+
77+Current runtime cleanup capabilities are intentionally conservative:
78+
79+- Gordon can identify managed route and attachment containers by labels such as `gordon.managed`, `gordon.route`, and `gordon.attachment`.
80+- Gordon can stop and remove containers.
81+- Existing volume attribution is limited for older volumes; Gordon may need naming heuristics when labels do not identify owner, mount path, or category.
82+- Updating runtime restart policy and relabeling existing volumes are runtime-dependent capabilities and should be treated as best-effort when added.
83+
6284 ## Routes
6385
6486 Routes map domains to container images:
M internal/adapters/dto/admin_route.go +2 -1
@@ -50,5 +50,6 @@ type RoutesByImageResponse struct {
5050
5151 // RouteDeleteResponse represents a route removal response.
5252 type RouteDeleteResponse struct {
53- Status string `json:"status"`
53+ Status string `json:"status"`
54+ Cleanup *CleanupReport `json:"cleanup,omitempty"`
5455 }
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+}
M internal/adapters/in/cli/attachments.go +127 -0
@@ -9,6 +9,7 @@ import (
99 "github.com/bnema/gordon/internal/adapters/in/cli/remote"
1010 "github.com/bnema/gordon/internal/adapters/in/cli/ui/components"
1111 "github.com/bnema/gordon/internal/adapters/in/cli/ui/styles"
12+ "github.com/bnema/gordon/internal/domain"
1213
1314 "github.com/spf13/cobra"
1415 )
@@ -32,6 +33,8 @@ the local Gordon configuration.`,
3233 cmd.AddCommand(newAttachmentsListCmd())
3334 cmd.AddCommand(newAttachmentsAddCmd())
3435 cmd.AddCommand(newAttachmentsRemoveCmd())
36+ cmd.AddCommand(newAttachmentsOrphansCmd())
37+ cmd.AddCommand(newAttachmentsPruneCmd())
3538 cmd.AddCommand(newAttachmentsPushCmd())
3639
3740 return cmd
@@ -277,6 +280,130 @@ func runAttachmentsListLocal(ctx context.Context, cfgPath string, target string,
277280 return nil
278281 }
279282
283+func newAttachmentsOrphansCmd() *cobra.Command {
284+ var jsonOut bool
285+ cmd := &cobra.Command{
286+ Use: "orphans",
287+ Short: "List orphaned attachment containers",
288+ Args: cobra.NoArgs,
289+ RunE: func(cmd *cobra.Command, args []string) error {
290+ handle, err := resolveControlPlane(configPath)
291+ if err != nil {
292+ return err
293+ }
294+ defer handle.close()
295+ lister, ok := handle.plane.(interface {
296+ ListOrphanedAttachments(context.Context) ([]domain.CleanupAttachment, error)
297+ })
298+ if !ok {
299+ return fmt.Errorf("attachment orphan listing is unavailable")
300+ }
301+ attachments, err := lister.ListOrphanedAttachments(cmd.Context())
302+ if err != nil {
303+ return fmt.Errorf("failed to list orphaned attachments: %w", err)
304+ }
305+ if jsonOut {
306+ return writeJSON(cmd.OutOrStdout(), map[string]any{"attachments": attachments})
307+ }
308+ return writeAttachmentOrphansText(cmd.OutOrStdout(), attachments)
309+ },
310+ }
311+ cmd.Flags().BoolVar(&jsonOut, "json", false, "Output as JSON")
312+ return cmd
313+}
314+
315+func newAttachmentsPruneCmd() *cobra.Command {
316+ var jsonOut bool
317+ var stop bool
318+ cmd := &cobra.Command{
319+ Use: "prune",
320+ Short: "Review or stop orphaned attachment containers",
321+ Args: cobra.NoArgs,
322+ RunE: func(cmd *cobra.Command, args []string) error {
323+ handle, err := resolveControlPlane(configPath)
324+ if err != nil {
325+ return err
326+ }
327+ defer handle.close()
328+ cleaner, ok := handle.plane.(interface {
329+ CleanupOrphanedAttachments(context.Context, string, bool) (*domain.CleanupReport, error)
330+ })
331+ if !ok {
332+ return fmt.Errorf("attachment orphan cleanup is unavailable")
333+ }
334+ report, err := cleaner.CleanupOrphanedAttachments(cmd.Context(), "", stop)
335+ if err != nil {
336+ return fmt.Errorf("failed to cleanup orphaned attachments: %w", err)
337+ }
338+ if jsonOut {
339+ return writeJSON(cmd.OutOrStdout(), report)
340+ }
341+ return writeAttachmentPruneText(cmd.OutOrStdout(), report, stop)
342+ },
343+ }
344+ cmd.Flags().BoolVar(&jsonOut, "json", false, "Output as JSON")
345+ cmd.Flags().BoolVar(&stop, "stop", false, "Stop and remove orphaned attachment containers while preserving volumes")
346+ return cmd
347+}
348+
349+func writeAttachmentOrphansText(out io.Writer, attachments []domain.CleanupAttachment) error {
350+ if len(attachments) == 0 {
351+ return cliWriteLine(out, styles.Theme.Muted.Render("No orphaned attachments found"))
352+ }
353+ if err := cliWriteLine(out, cliRenderTitle("Orphaned attachments")); err != nil {
354+ return err
355+ }
356+ for _, a := range attachments {
357+ label := a.Name
358+ if label == "" {
359+ label = a.ContainerID
360+ }
361+ if a.Image != "" {
362+ label += " -> " + a.Image
363+ }
364+ if err := cliWriteLine(out, " "+label); err != nil {
365+ return err
366+ }
367+ }
368+ return nil
369+}
370+
371+func writeAttachmentPruneText(out io.Writer, report *domain.CleanupReport, stop bool) error {
372+ if report == nil {
373+ return nil
374+ }
375+ if !stop {
376+ if err := writeAttachmentOrphansText(out, report.PreservedAttachments); err != nil {
377+ return err
378+ }
379+ return writeCleanupMessages(out, report.Warnings, report.Hints)
380+ }
381+ if len(report.RemovedContainers) == 0 {
382+ if err := cliWriteLine(out, styles.Theme.Muted.Render("No orphaned attachments removed")); err != nil {
383+ return err
384+ }
385+ } else {
386+ if err := cliWriteLine(out, cliRenderSuccess("Removed orphaned attachment containers:")); err != nil {
387+ return err
388+ }
389+ for _, c := range report.RemovedContainers {
390+ label := c.Name
391+ if label == "" {
392+ label = c.ID
393+ }
394+ if err := cliWriteLine(out, " "+label); err != nil {
395+ return err
396+ }
397+ }
398+ }
399+ for _, failure := range report.PartialFailures {
400+ if err := cliWriteLine(out, cliRenderWarning(fmt.Sprintf("%s %s failed: %s", failure.Action, failure.Name, failure.Error))); err != nil {
401+ return err
402+ }
403+ }
404+ return writeCleanupMessages(out, report.Warnings, report.Hints)
405+}
406+
280407 // newAttachmentsAddCmd creates the attachments add command.
281408 func newAttachmentsAddCmd() *cobra.Command {
282409 cmd := &cobra.Command{
M internal/adapters/in/cli/attachments_push_test.go +8 -0
@@ -81,6 +81,14 @@ func (c *attachmentPushTestControlPlane) GetAttachmentsConfig(context.Context, s
8181 panic("unexpected call")
8282 }
8383
84+func (c *attachmentPushTestControlPlane) ListOrphanedAttachments(context.Context) ([]domain.CleanupAttachment, error) {
85+ panic("unexpected call")
86+}
87+
88+func (c *attachmentPushTestControlPlane) CleanupOrphanedAttachments(context.Context, string, bool) (*domain.CleanupReport, error) {
89+ panic("unexpected call")
90+}
91+
8492 func (c *attachmentPushTestControlPlane) FindAttachmentTargetsByImage(ctx context.Context, imageName string) ([]string, error) {
8593 if c.findAttachmentTargets != nil {
8694 return c.findAttachmentTargets(ctx, imageName)
M internal/adapters/in/cli/controlplane.go +2 -0
@@ -30,6 +30,8 @@ type ControlPlane interface {
3030
3131 GetAllAttachmentsConfig(ctx context.Context) (map[string][]string, error)
3232 GetAttachmentsConfig(ctx context.Context, domainOrGroup string) ([]string, error)
33+ ListOrphanedAttachments(ctx context.Context) ([]domain.CleanupAttachment, error)
34+ CleanupOrphanedAttachments(ctx context.Context, owner string, stop bool) (*domain.CleanupReport, error)
3335 FindAttachmentTargetsByImage(ctx context.Context, imageName string) ([]string, error)
3436 AddAttachment(ctx context.Context, domainOrGroup, image string) error
3537 RemoveAttachment(ctx context.Context, domainOrGroup, image string) error
M internal/adapters/in/cli/controlplane_local.go +42 -3
@@ -130,10 +130,27 @@ func (l *localControlPlane) UpdateRoute(ctx context.Context, route domain.Route)
130130 }
131131
132132 func (l *localControlPlane) RemoveRoute(ctx context.Context, routeDomain string) error {
133+ _, err := l.RemoveRouteWithCleanup(ctx, routeDomain)
134+ return err
135+}
136+
137+func (l *localControlPlane) RemoveRouteWithCleanup(ctx context.Context, routeDomain string) (*dto.RouteDeleteResponse, error) {
133138 if l.configSvc == nil {
134- return fmt.Errorf("local config service unavailable")
139+ return nil, fmt.Errorf("local config service unavailable")
140+ }
141+ if err := l.configSvc.RemoveRoute(ctx, routeDomain); err != nil && !errors.Is(err, domain.ErrRouteNotFound) {
142+ return nil, fmt.Errorf("remove route: %w", err)
143+ }
144+ resp := &dto.RouteDeleteResponse{Status: "removed"}
145+ if l.containerSvc == nil {
146+ return resp, nil
147+ }
148+ report, err := l.containerSvc.ReconcileRemovedRoute(ctx, routeDomain)
149+ if err != nil {
150+ return nil, fmt.Errorf("failed to cleanup removed route runtime state: %w", err)
135151 }
136- return l.configSvc.RemoveRoute(ctx, routeDomain)
152+ resp.Cleanup = dto.CleanupReportFromDomain(report)
153+ return resp, nil
137154 }
138155
139156 func (l *localControlPlane) Bootstrap(ctx context.Context, req dto.BootstrapRequest) (*dto.BootstrapResponse, error) {
@@ -239,7 +256,7 @@ func (l *localControlPlane) ListSecretsWithAttachments(ctx context.Context, secr
239256
240257 keys, attachments, err := l.secretSvc.ListKeysWithAttachments(ctx, secretDomain)
241258 if err != nil {
242- return nil, err
259+ return nil, fmt.Errorf("list secret keys with attachments: %w", err)
243260 }
244261
245262 result := &remote.SecretsListResult{
@@ -284,6 +301,28 @@ func (l *localControlPlane) DeleteAttachmentSecret(ctx context.Context, domainNa
284301 return l.secretSvc.DeleteAttachment(ctx, domainName, service, key)
285302 }
286303
304+func (l *localControlPlane) ListOrphanedAttachments(ctx context.Context) ([]domain.CleanupAttachment, error) {
305+ if l.containerSvc == nil {
306+ return nil, fmt.Errorf("local container service unavailable")
307+ }
308+ attachments, err := l.containerSvc.ListOrphanedAttachments(ctx)
309+ if err != nil {
310+ return nil, fmt.Errorf("list orphaned attachments: %w", err)
311+ }
312+ return attachments, nil
313+}
314+
315+func (l *localControlPlane) CleanupOrphanedAttachments(ctx context.Context, owner string, stop bool) (*domain.CleanupReport, error) {
316+ if l.containerSvc == nil {
317+ return nil, fmt.Errorf("local container service unavailable")
318+ }
319+ report, err := l.containerSvc.CleanupOrphanedAttachments(ctx, owner, stop)
320+ if err != nil {
321+ return nil, fmt.Errorf("cleanup orphaned attachments: %w", err)
322+ }
323+ return report, nil
324+}
325+
287326 func (l *localControlPlane) GetAllAttachmentsConfig(ctx context.Context) (map[string][]string, error) {
288327 if l.configSvc == nil {
289328 return nil, fmt.Errorf("local config service unavailable")
M internal/adapters/in/cli/controlplane_local_test.go +39 -0
@@ -8,6 +8,7 @@ import (
88 "github.com/bnema/gordon/internal/adapters/dto"
99 inmocks "github.com/bnema/gordon/internal/boundaries/in/mocks"
1010 "github.com/bnema/gordon/internal/domain"
11+ "github.com/stretchr/testify/assert"
1112 "github.com/stretchr/testify/mock"
1213 "github.com/stretchr/testify/require"
1314 )
@@ -110,6 +111,28 @@ func TestLocalControlPlane_ListTags(t *testing.T) {
110111 require.Equal(t, []string{"v1.0.0", "latest"}, tags)
111112 }
112113
114+func TestLocalControlPlane_RemoveRoutePersistsConfigThenReconcilesRuntime(t *testing.T) {
115+ t.Parallel()
116+
117+ configSvc := inmocks.NewMockConfigService(t)
118+ containerSvc := inmocks.NewMockContainerService(t)
119+ report := &domain.CleanupReport{
120+ Domain: "app.local",
121+ RemovedContainers: []domain.CleanupContainer{{
122+ ID: "container-1",
123+ Name: "gordon-app.local",
124+ }},
125+ }
126+
127+ removeCall := configSvc.EXPECT().RemoveRoute(mock.Anything, "app.local").Return(nil).Once()
128+ cleanupCall := containerSvc.EXPECT().ReconcileRemovedRoute(mock.Anything, "app.local").Return(report, nil).Once()
129+ mock.InOrder(removeCall, cleanupCall)
130+
131+ cp := &localControlPlane{configSvc: configSvc, containerSvc: containerSvc}
132+ err := cp.RemoveRoute(context.Background(), "app.local")
133+ require.NoError(t, err)
134+}
135+
113136 func TestLocalControlPlane_RestartWithAttachments(t *testing.T) {
114137 t.Parallel()
115138
@@ -192,3 +215,19 @@ func TestLocalControlPlane_GetContainerLogs(t *testing.T) {
192215 require.NoError(t, err)
193216 require.Equal(t, []string{"line1", "line2"}, lines)
194217 }
218+
219+func TestLocalControlPlane_RemoveRouteReconcilesRuntimeWhenRouteAlreadyMissing(t *testing.T) {
220+ ctx := context.Background()
221+ configSvc := inmocks.NewMockConfigService(t)
222+ containerSvc := inmocks.NewMockContainerService(t)
223+ report := &domain.CleanupReport{Domain: "app.local"}
224+
225+ configSvc.EXPECT().RemoveRoute(mock.Anything, "app.local").Return(domain.ErrRouteNotFound).Once()
226+ containerSvc.EXPECT().ReconcileRemovedRoute(mock.Anything, "app.local").Return(report, nil).Once()
227+
228+ cp := &localControlPlane{configSvc: configSvc, containerSvc: containerSvc}
229+ resp, err := cp.RemoveRouteWithCleanup(ctx, "app.local")
230+ require.NoError(t, err)
231+ require.NotNil(t, resp)
232+ assert.Equal(t, dto.CleanupReportFromDomain(report), resp.Cleanup)
233+}
M internal/adapters/in/cli/controlplane_remote.go +14 -1
@@ -41,7 +41,12 @@ func (r *remoteControlPlane) UpdateRoute(ctx context.Context, route domain.Route
4141 }
4242
4343 func (r *remoteControlPlane) RemoveRoute(ctx context.Context, routeDomain string) error {
44- return r.client.RemoveRoute(ctx, routeDomain)
44+ _, err := r.RemoveRouteWithCleanup(ctx, routeDomain)
45+ return err
46+}
47+
48+func (r *remoteControlPlane) RemoveRouteWithCleanup(ctx context.Context, routeDomain string) (*dto.RouteDeleteResponse, error) {
49+ return r.client.RemoveRouteWithCleanup(ctx, routeDomain)
4550 }
4651
4752 func (r *remoteControlPlane) Bootstrap(ctx context.Context, req dto.BootstrapRequest) (*dto.BootstrapResponse, error) {
@@ -68,6 +73,14 @@ func (r *remoteControlPlane) DeleteAttachmentSecret(ctx context.Context, domainN
6873 return r.client.DeleteAttachmentSecret(ctx, domainName, service, key)
6974 }
7075
76+func (r *remoteControlPlane) ListOrphanedAttachments(ctx context.Context) ([]domain.CleanupAttachment, error) {
77+ return r.client.ListOrphanedAttachments(ctx)
78+}
79+
80+func (r *remoteControlPlane) CleanupOrphanedAttachments(ctx context.Context, owner string, stop bool) (*domain.CleanupReport, error) {
81+ return r.client.CleanupOrphanedAttachments(ctx, owner, stop)
82+}
83+
7184 func (r *remoteControlPlane) GetAllAttachmentsConfig(ctx context.Context) (map[string][]string, error) {
7285 return r.client.GetAllAttachmentsConfig(ctx)
7386 }
A internal/adapters/in/cli/mocks/mock_control_plane.go +2760 -0
@@ -0,0 +1,2760 @@
1+// Code generated by mockery; DO NOT EDIT.
2+// github.com/vektra/mockery
3+// template: testify
4+
5+package mocks
6+
7+import (
8+ "context"
9+
10+ "github.com/bnema/gordon/internal/adapters/dto"
11+ "github.com/bnema/gordon/internal/adapters/in/cli/remote"
12+ "github.com/bnema/gordon/internal/domain"
13+ mock "github.com/stretchr/testify/mock"
14+)
15+
16+// NewMockControlPlane creates a new instance of MockControlPlane. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
17+// The first argument is typically a *testing.T value.
18+func NewMockControlPlane(t interface {
19+ mock.TestingT
20+ Cleanup(func())
21+}) *MockControlPlane {
22+ mock := &MockControlPlane{}
23+ mock.Mock.Test(t)
24+
25+ t.Cleanup(func() { mock.AssertExpectations(t) })
26+
27+ return mock
28+}
29+
30+// MockControlPlane is an autogenerated mock type for the ControlPlane type
31+type MockControlPlane struct {
32+ mock.Mock
33+}
34+
35+type MockControlPlane_Expecter struct {
36+ mock *mock.Mock
37+}
38+
39+func (_m *MockControlPlane) EXPECT() *MockControlPlane_Expecter {
40+ return &MockControlPlane_Expecter{mock: &_m.Mock}
41+}
42+
43+// AddAttachment provides a mock function for the type MockControlPlane
44+func (_mock *MockControlPlane) AddAttachment(ctx context.Context, domainOrGroup string, image string) error {
45+ ret := _mock.Called(ctx, domainOrGroup, image)
46+
47+ if len(ret) == 0 {
48+ panic("no return value specified for AddAttachment")
49+ }
50+
51+ var r0 error
52+ if returnFunc, ok := ret.Get(0).(func(context.Context, string, string) error); ok {
53+ r0 = returnFunc(ctx, domainOrGroup, image)
54+ } else {
55+ r0 = ret.Error(0)
56+ }
57+ return r0
58+}
59+
60+// MockControlPlane_AddAttachment_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'AddAttachment'
61+type MockControlPlane_AddAttachment_Call struct {
62+ *mock.Call
63+}
64+
65+// AddAttachment is a helper method to define mock.On call
66+// - ctx context.Context
67+// - domainOrGroup string
68+// - image string
69+func (_e *MockControlPlane_Expecter) AddAttachment(ctx interface{}, domainOrGroup interface{}, image interface{}) *MockControlPlane_AddAttachment_Call {
70+ return &MockControlPlane_AddAttachment_Call{Call: _e.mock.On("AddAttachment", ctx, domainOrGroup, image)}
71+}
72+
73+func (_c *MockControlPlane_AddAttachment_Call) Run(run func(ctx context.Context, domainOrGroup string, image string)) *MockControlPlane_AddAttachment_Call {
74+ _c.Call.Run(func(args mock.Arguments) {
75+ var arg0 context.Context
76+ if args[0] != nil {
77+ arg0 = args[0].(context.Context)
78+ }
79+ var arg1 string
80+ if args[1] != nil {
81+ arg1 = args[1].(string)
82+ }
83+ var arg2 string
84+ if args[2] != nil {
85+ arg2 = args[2].(string)
86+ }
87+ run(
88+ arg0,
89+ arg1,
90+ arg2,
91+ )
92+ })
93+ return _c
94+}
95+
96+func (_c *MockControlPlane_AddAttachment_Call) Return(err error) *MockControlPlane_AddAttachment_Call {
97+ _c.Call.Return(err)
98+ return _c
99+}
100+
101+func (_c *MockControlPlane_AddAttachment_Call) RunAndReturn(run func(ctx context.Context, domainOrGroup string, image string) error) *MockControlPlane_AddAttachment_Call {
102+ _c.Call.Return(run)
103+ return _c
104+}
105+
106+// AddAutoRouteAllowedDomain provides a mock function for the type MockControlPlane
107+func (_mock *MockControlPlane) AddAutoRouteAllowedDomain(ctx context.Context, pattern string) error {
108+ ret := _mock.Called(ctx, pattern)
109+
110+ if len(ret) == 0 {
111+ panic("no return value specified for AddAutoRouteAllowedDomain")
112+ }
113+
114+ var r0 error
115+ if returnFunc, ok := ret.Get(0).(func(context.Context, string) error); ok {
116+ r0 = returnFunc(ctx, pattern)
117+ } else {
118+ r0 = ret.Error(0)
119+ }
120+ return r0
121+}
122+
123+// MockControlPlane_AddAutoRouteAllowedDomain_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'AddAutoRouteAllowedDomain'
124+type MockControlPlane_AddAutoRouteAllowedDomain_Call struct {
125+ *mock.Call
126+}
127+
128+// AddAutoRouteAllowedDomain is a helper method to define mock.On call
129+// - ctx context.Context
130+// - pattern string
131+func (_e *MockControlPlane_Expecter) AddAutoRouteAllowedDomain(ctx interface{}, pattern interface{}) *MockControlPlane_AddAutoRouteAllowedDomain_Call {
132+ return &MockControlPlane_AddAutoRouteAllowedDomain_Call{Call: _e.mock.On("AddAutoRouteAllowedDomain", ctx, pattern)}
133+}
134+
135+func (_c *MockControlPlane_AddAutoRouteAllowedDomain_Call) Run(run func(ctx context.Context, pattern string)) *MockControlPlane_AddAutoRouteAllowedDomain_Call {
136+ _c.Call.Run(func(args mock.Arguments) {
137+ var arg0 context.Context
138+ if args[0] != nil {
139+ arg0 = args[0].(context.Context)
140+ }
141+ var arg1 string
142+ if args[1] != nil {
143+ arg1 = args[1].(string)
144+ }
145+ run(
146+ arg0,
147+ arg1,
148+ )
149+ })
150+ return _c
151+}
152+
153+func (_c *MockControlPlane_AddAutoRouteAllowedDomain_Call) Return(err error) *MockControlPlane_AddAutoRouteAllowedDomain_Call {
154+ _c.Call.Return(err)
155+ return _c
156+}
157+
158+func (_c *MockControlPlane_AddAutoRouteAllowedDomain_Call) RunAndReturn(run func(ctx context.Context, pattern string) error) *MockControlPlane_AddAutoRouteAllowedDomain_Call {
159+ _c.Call.Return(run)
160+ return _c
161+}
162+
163+// AddRoute provides a mock function for the type MockControlPlane
164+func (_mock *MockControlPlane) AddRoute(ctx context.Context, route domain.Route) error {
165+ ret := _mock.Called(ctx, route)
166+
167+ if len(ret) == 0 {
168+ panic("no return value specified for AddRoute")
169+ }
170+
171+ var r0 error
172+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.Route) error); ok {
173+ r0 = returnFunc(ctx, route)
174+ } else {
175+ r0 = ret.Error(0)
176+ }
177+ return r0
178+}
179+
180+// MockControlPlane_AddRoute_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'AddRoute'
181+type MockControlPlane_AddRoute_Call struct {
182+ *mock.Call
183+}
184+
185+// AddRoute is a helper method to define mock.On call
186+// - ctx context.Context
187+// - route domain.Route
188+func (_e *MockControlPlane_Expecter) AddRoute(ctx interface{}, route interface{}) *MockControlPlane_AddRoute_Call {
189+ return &MockControlPlane_AddRoute_Call{Call: _e.mock.On("AddRoute", ctx, route)}
190+}
191+
192+func (_c *MockControlPlane_AddRoute_Call) Run(run func(ctx context.Context, route domain.Route)) *MockControlPlane_AddRoute_Call {
193+ _c.Call.Run(func(args mock.Arguments) {
194+ var arg0 context.Context
195+ if args[0] != nil {
196+ arg0 = args[0].(context.Context)
197+ }
198+ var arg1 domain.Route
199+ if args[1] != nil {
200+ arg1 = args[1].(domain.Route)
201+ }
202+ run(
203+ arg0,
204+ arg1,
205+ )
206+ })
207+ return _c
208+}
209+
210+func (_c *MockControlPlane_AddRoute_Call) Return(err error) *MockControlPlane_AddRoute_Call {
211+ _c.Call.Return(err)
212+ return _c
213+}
214+
215+func (_c *MockControlPlane_AddRoute_Call) RunAndReturn(run func(ctx context.Context, route domain.Route) error) *MockControlPlane_AddRoute_Call {
216+ _c.Call.Return(run)
217+ return _c
218+}
219+
220+// BackupStatus provides a mock function for the type MockControlPlane
221+func (_mock *MockControlPlane) BackupStatus(ctx context.Context) ([]dto.BackupJob, error) {
222+ ret := _mock.Called(ctx)
223+
224+ if len(ret) == 0 {
225+ panic("no return value specified for BackupStatus")
226+ }
227+
228+ var r0 []dto.BackupJob
229+ var r1 error
230+ if returnFunc, ok := ret.Get(0).(func(context.Context) ([]dto.BackupJob, error)); ok {
231+ return returnFunc(ctx)
232+ }
233+ if returnFunc, ok := ret.Get(0).(func(context.Context) []dto.BackupJob); ok {
234+ r0 = returnFunc(ctx)
235+ } else {
236+ if ret.Get(0) != nil {
237+ r0 = ret.Get(0).([]dto.BackupJob)
238+ }
239+ }
240+ if returnFunc, ok := ret.Get(1).(func(context.Context) error); ok {
241+ r1 = returnFunc(ctx)
242+ } else {
243+ r1 = ret.Error(1)
244+ }
245+ return r0, r1
246+}
247+
248+// MockControlPlane_BackupStatus_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'BackupStatus'
249+type MockControlPlane_BackupStatus_Call struct {
250+ *mock.Call
251+}
252+
253+// BackupStatus is a helper method to define mock.On call
254+// - ctx context.Context
255+func (_e *MockControlPlane_Expecter) BackupStatus(ctx interface{}) *MockControlPlane_BackupStatus_Call {
256+ return &MockControlPlane_BackupStatus_Call{Call: _e.mock.On("BackupStatus", ctx)}
257+}
258+
259+func (_c *MockControlPlane_BackupStatus_Call) Run(run func(ctx context.Context)) *MockControlPlane_BackupStatus_Call {
260+ _c.Call.Run(func(args mock.Arguments) {
261+ var arg0 context.Context
262+ if args[0] != nil {
263+ arg0 = args[0].(context.Context)
264+ }
265+ run(
266+ arg0,
267+ )
268+ })
269+ return _c
270+}
271+
272+func (_c *MockControlPlane_BackupStatus_Call) Return(backupJobs []dto.BackupJob, err error) *MockControlPlane_BackupStatus_Call {
273+ _c.Call.Return(backupJobs, err)
274+ return _c
275+}
276+
277+func (_c *MockControlPlane_BackupStatus_Call) RunAndReturn(run func(ctx context.Context) ([]dto.BackupJob, error)) *MockControlPlane_BackupStatus_Call {
278+ _c.Call.Return(run)
279+ return _c
280+}
281+
282+// Bootstrap provides a mock function for the type MockControlPlane
283+func (_mock *MockControlPlane) Bootstrap(ctx context.Context, req dto.BootstrapRequest) (*dto.BootstrapResponse, error) {
284+ ret := _mock.Called(ctx, req)
285+
286+ if len(ret) == 0 {
287+ panic("no return value specified for Bootstrap")
288+ }
289+
290+ var r0 *dto.BootstrapResponse
291+ var r1 error
292+ if returnFunc, ok := ret.Get(0).(func(context.Context, dto.BootstrapRequest) (*dto.BootstrapResponse, error)); ok {
293+ return returnFunc(ctx, req)
294+ }
295+ if returnFunc, ok := ret.Get(0).(func(context.Context, dto.BootstrapRequest) *dto.BootstrapResponse); ok {
296+ r0 = returnFunc(ctx, req)
297+ } else {
298+ if ret.Get(0) != nil {
299+ r0 = ret.Get(0).(*dto.BootstrapResponse)
300+ }
301+ }
302+ if returnFunc, ok := ret.Get(1).(func(context.Context, dto.BootstrapRequest) error); ok {
303+ r1 = returnFunc(ctx, req)
304+ } else {
305+ r1 = ret.Error(1)
306+ }
307+ return r0, r1
308+}
309+
310+// MockControlPlane_Bootstrap_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Bootstrap'
311+type MockControlPlane_Bootstrap_Call struct {
312+ *mock.Call
313+}
314+
315+// Bootstrap is a helper method to define mock.On call
316+// - ctx context.Context
317+// - req dto.BootstrapRequest
318+func (_e *MockControlPlane_Expecter) Bootstrap(ctx interface{}, req interface{}) *MockControlPlane_Bootstrap_Call {
319+ return &MockControlPlane_Bootstrap_Call{Call: _e.mock.On("Bootstrap", ctx, req)}
320+}
321+
322+func (_c *MockControlPlane_Bootstrap_Call) Run(run func(ctx context.Context, req dto.BootstrapRequest)) *MockControlPlane_Bootstrap_Call {
323+ _c.Call.Run(func(args mock.Arguments) {
324+ var arg0 context.Context
325+ if args[0] != nil {
326+ arg0 = args[0].(context.Context)
327+ }
328+ var arg1 dto.BootstrapRequest
329+ if args[1] != nil {
330+ arg1 = args[1].(dto.BootstrapRequest)
331+ }
332+ run(
333+ arg0,
334+ arg1,
335+ )
336+ })
337+ return _c
338+}
339+
340+func (_c *MockControlPlane_Bootstrap_Call) Return(bootstrapResponse *dto.BootstrapResponse, err error) *MockControlPlane_Bootstrap_Call {
341+ _c.Call.Return(bootstrapResponse, err)
342+ return _c
343+}
344+
345+func (_c *MockControlPlane_Bootstrap_Call) RunAndReturn(run func(ctx context.Context, req dto.BootstrapRequest) (*dto.BootstrapResponse, error)) *MockControlPlane_Bootstrap_Call {
346+ _c.Call.Return(run)
347+ return _c
348+}
349+
350+// CleanupOrphanedAttachments provides a mock function for the type MockControlPlane
351+func (_mock *MockControlPlane) CleanupOrphanedAttachments(ctx context.Context, owner string, stop bool) (*domain.CleanupReport, error) {
352+ ret := _mock.Called(ctx, owner, stop)
353+
354+ if len(ret) == 0 {
355+ panic("no return value specified for CleanupOrphanedAttachments")
356+ }
357+
358+ var r0 *domain.CleanupReport
359+ var r1 error
360+ if returnFunc, ok := ret.Get(0).(func(context.Context, string, bool) (*domain.CleanupReport, error)); ok {
361+ return returnFunc(ctx, owner, stop)
362+ }
363+ if returnFunc, ok := ret.Get(0).(func(context.Context, string, bool) *domain.CleanupReport); ok {
364+ r0 = returnFunc(ctx, owner, stop)
365+ } else {
366+ if ret.Get(0) != nil {
367+ r0 = ret.Get(0).(*domain.CleanupReport)
368+ }
369+ }
370+ if returnFunc, ok := ret.Get(1).(func(context.Context, string, bool) error); ok {
371+ r1 = returnFunc(ctx, owner, stop)
372+ } else {
373+ r1 = ret.Error(1)
374+ }
375+ return r0, r1
376+}
377+
378+// MockControlPlane_CleanupOrphanedAttachments_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CleanupOrphanedAttachments'
379+type MockControlPlane_CleanupOrphanedAttachments_Call struct {
380+ *mock.Call
381+}
382+
383+// CleanupOrphanedAttachments is a helper method to define mock.On call
384+// - ctx context.Context
385+// - owner string
386+// - stop bool
387+func (_e *MockControlPlane_Expecter) CleanupOrphanedAttachments(ctx interface{}, owner interface{}, stop interface{}) *MockControlPlane_CleanupOrphanedAttachments_Call {
388+ return &MockControlPlane_CleanupOrphanedAttachments_Call{Call: _e.mock.On("CleanupOrphanedAttachments", ctx, owner, stop)}
389+}
390+
391+func (_c *MockControlPlane_CleanupOrphanedAttachments_Call) Run(run func(ctx context.Context, owner string, stop bool)) *MockControlPlane_CleanupOrphanedAttachments_Call {
392+ _c.Call.Run(func(args mock.Arguments) {
393+ var arg0 context.Context
394+ if args[0] != nil {
395+ arg0 = args[0].(context.Context)
396+ }
397+ var arg1 string
398+ if args[1] != nil {
399+ arg1 = args[1].(string)
400+ }
401+ var arg2 bool
402+ if args[2] != nil {
403+ arg2 = args[2].(bool)
404+ }
405+ run(
406+ arg0,
407+ arg1,
408+ arg2,
409+ )
410+ })
411+ return _c
412+}
413+
414+func (_c *MockControlPlane_CleanupOrphanedAttachments_Call) Return(cleanupReport *domain.CleanupReport, err error) *MockControlPlane_CleanupOrphanedAttachments_Call {
415+ _c.Call.Return(cleanupReport, err)
416+ return _c
417+}
418+
419+func (_c *MockControlPlane_CleanupOrphanedAttachments_Call) RunAndReturn(run func(ctx context.Context, owner string, stop bool) (*domain.CleanupReport, error)) *MockControlPlane_CleanupOrphanedAttachments_Call {
420+ _c.Call.Return(run)
421+ return _c
422+}
423+
424+// DeleteAttachmentSecret provides a mock function for the type MockControlPlane
425+func (_mock *MockControlPlane) DeleteAttachmentSecret(ctx context.Context, domain1 string, service string, key string) error {
426+ ret := _mock.Called(ctx, domain1, service, key)
427+
428+ if len(ret) == 0 {
429+ panic("no return value specified for DeleteAttachmentSecret")
430+ }
431+
432+ var r0 error
433+ if returnFunc, ok := ret.Get(0).(func(context.Context, string, string, string) error); ok {
434+ r0 = returnFunc(ctx, domain1, service, key)
435+ } else {
436+ r0 = ret.Error(0)
437+ }
438+ return r0
439+}
440+
441+// MockControlPlane_DeleteAttachmentSecret_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'DeleteAttachmentSecret'
442+type MockControlPlane_DeleteAttachmentSecret_Call struct {
443+ *mock.Call
444+}
445+
446+// DeleteAttachmentSecret is a helper method to define mock.On call
447+// - ctx context.Context
448+// - domain1 string
449+// - service string
450+// - key string
451+func (_e *MockControlPlane_Expecter) DeleteAttachmentSecret(ctx interface{}, domain1 interface{}, service interface{}, key interface{}) *MockControlPlane_DeleteAttachmentSecret_Call {
452+ return &MockControlPlane_DeleteAttachmentSecret_Call{Call: _e.mock.On("DeleteAttachmentSecret", ctx, domain1, service, key)}
453+}
454+
455+func (_c *MockControlPlane_DeleteAttachmentSecret_Call) Run(run func(ctx context.Context, domain1 string, service string, key string)) *MockControlPlane_DeleteAttachmentSecret_Call {
456+ _c.Call.Run(func(args mock.Arguments) {
457+ var arg0 context.Context
458+ if args[0] != nil {
459+ arg0 = args[0].(context.Context)
460+ }
461+ var arg1 string
462+ if args[1] != nil {
463+ arg1 = args[1].(string)
464+ }
465+ var arg2 string
466+ if args[2] != nil {
467+ arg2 = args[2].(string)
468+ }
469+ var arg3 string
470+ if args[3] != nil {
471+ arg3 = args[3].(string)
472+ }
473+ run(
474+ arg0,
475+ arg1,
476+ arg2,
477+ arg3,
478+ )
479+ })
480+ return _c
481+}
482+
483+func (_c *MockControlPlane_DeleteAttachmentSecret_Call) Return(err error) *MockControlPlane_DeleteAttachmentSecret_Call {
484+ _c.Call.Return(err)
485+ return _c
486+}
487+
488+func (_c *MockControlPlane_DeleteAttachmentSecret_Call) RunAndReturn(run func(ctx context.Context, domain1 string, service string, key string) error) *MockControlPlane_DeleteAttachmentSecret_Call {
489+ _c.Call.Return(run)
490+ return _c
491+}
492+
493+// DeleteSecret provides a mock function for the type MockControlPlane
494+func (_mock *MockControlPlane) DeleteSecret(ctx context.Context, secretDomain string, key string) error {
495+ ret := _mock.Called(ctx, secretDomain, key)
496+
497+ if len(ret) == 0 {
498+ panic("no return value specified for DeleteSecret")
499+ }
500+
501+ var r0 error
502+ if returnFunc, ok := ret.Get(0).(func(context.Context, string, string) error); ok {
503+ r0 = returnFunc(ctx, secretDomain, key)
504+ } else {
505+ r0 = ret.Error(0)
506+ }
507+ return r0
508+}
509+
510+// MockControlPlane_DeleteSecret_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'DeleteSecret'
511+type MockControlPlane_DeleteSecret_Call struct {
512+ *mock.Call
513+}
514+
515+// DeleteSecret is a helper method to define mock.On call
516+// - ctx context.Context
517+// - secretDomain string
518+// - key string
519+func (_e *MockControlPlane_Expecter) DeleteSecret(ctx interface{}, secretDomain interface{}, key interface{}) *MockControlPlane_DeleteSecret_Call {
520+ return &MockControlPlane_DeleteSecret_Call{Call: _e.mock.On("DeleteSecret", ctx, secretDomain, key)}
521+}
522+
523+func (_c *MockControlPlane_DeleteSecret_Call) Run(run func(ctx context.Context, secretDomain string, key string)) *MockControlPlane_DeleteSecret_Call {
524+ _c.Call.Run(func(args mock.Arguments) {
525+ var arg0 context.Context
526+ if args[0] != nil {
527+ arg0 = args[0].(context.Context)
528+ }
529+ var arg1 string
530+ if args[1] != nil {
531+ arg1 = args[1].(string)
532+ }
533+ var arg2 string
534+ if args[2] != nil {
535+ arg2 = args[2].(string)
536+ }
537+ run(
538+ arg0,
539+ arg1,
540+ arg2,
541+ )
542+ })
543+ return _c
544+}
545+
546+func (_c *MockControlPlane_DeleteSecret_Call) Return(err error) *MockControlPlane_DeleteSecret_Call {
547+ _c.Call.Return(err)
548+ return _c
549+}
550+
551+func (_c *MockControlPlane_DeleteSecret_Call) RunAndReturn(run func(ctx context.Context, secretDomain string, key string) error) *MockControlPlane_DeleteSecret_Call {
552+ _c.Call.Return(run)
553+ return _c
554+}
555+
556+// Deploy provides a mock function for the type MockControlPlane
557+func (_mock *MockControlPlane) Deploy(ctx context.Context, deployDomain string) (*remote.DeployResult, error) {
558+ ret := _mock.Called(ctx, deployDomain)
559+
560+ if len(ret) == 0 {
561+ panic("no return value specified for Deploy")
562+ }
563+
564+ var r0 *remote.DeployResult
565+ var r1 error
566+ if returnFunc, ok := ret.Get(0).(func(context.Context, string) (*remote.DeployResult, error)); ok {
567+ return returnFunc(ctx, deployDomain)
568+ }
569+ if returnFunc, ok := ret.Get(0).(func(context.Context, string) *remote.DeployResult); ok {
570+ r0 = returnFunc(ctx, deployDomain)
571+ } else {
572+ if ret.Get(0) != nil {
573+ r0 = ret.Get(0).(*remote.DeployResult)
574+ }
575+ }
576+ if returnFunc, ok := ret.Get(1).(func(context.Context, string) error); ok {
577+ r1 = returnFunc(ctx, deployDomain)
578+ } else {
579+ r1 = ret.Error(1)
580+ }
581+ return r0, r1
582+}
583+
584+// MockControlPlane_Deploy_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Deploy'
585+type MockControlPlane_Deploy_Call struct {
586+ *mock.Call
587+}
588+
589+// Deploy is a helper method to define mock.On call
590+// - ctx context.Context
591+// - deployDomain string
592+func (_e *MockControlPlane_Expecter) Deploy(ctx interface{}, deployDomain interface{}) *MockControlPlane_Deploy_Call {
593+ return &MockControlPlane_Deploy_Call{Call: _e.mock.On("Deploy", ctx, deployDomain)}
594+}
595+
596+func (_c *MockControlPlane_Deploy_Call) Run(run func(ctx context.Context, deployDomain string)) *MockControlPlane_Deploy_Call {
597+ _c.Call.Run(func(args mock.Arguments) {
598+ var arg0 context.Context
599+ if args[0] != nil {
600+ arg0 = args[0].(context.Context)
601+ }
602+ var arg1 string
603+ if args[1] != nil {
604+ arg1 = args[1].(string)
605+ }
606+ run(
607+ arg0,
608+ arg1,
609+ )
610+ })
611+ return _c
612+}
613+
614+func (_c *MockControlPlane_Deploy_Call) Return(deployResult *remote.DeployResult, err error) *MockControlPlane_Deploy_Call {
615+ _c.Call.Return(deployResult, err)
616+ return _c
617+}
618+
619+func (_c *MockControlPlane_Deploy_Call) RunAndReturn(run func(ctx context.Context, deployDomain string) (*remote.DeployResult, error)) *MockControlPlane_Deploy_Call {
620+ _c.Call.Return(run)
621+ return _c
622+}
623+
624+// DeployIntent provides a mock function for the type MockControlPlane
625+func (_mock *MockControlPlane) DeployIntent(ctx context.Context, imageName string) error {
626+ ret := _mock.Called(ctx, imageName)
627+
628+ if len(ret) == 0 {
629+ panic("no return value specified for DeployIntent")
630+ }
631+
632+ var r0 error
633+ if returnFunc, ok := ret.Get(0).(func(context.Context, string) error); ok {
634+ r0 = returnFunc(ctx, imageName)
635+ } else {
636+ r0 = ret.Error(0)
637+ }
638+ return r0
639+}
640+
641+// MockControlPlane_DeployIntent_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'DeployIntent'
642+type MockControlPlane_DeployIntent_Call struct {
643+ *mock.Call
644+}
645+
646+// DeployIntent is a helper method to define mock.On call
647+// - ctx context.Context
648+// - imageName string
649+func (_e *MockControlPlane_Expecter) DeployIntent(ctx interface{}, imageName interface{}) *MockControlPlane_DeployIntent_Call {
650+ return &MockControlPlane_DeployIntent_Call{Call: _e.mock.On("DeployIntent", ctx, imageName)}
651+}
652+
653+func (_c *MockControlPlane_DeployIntent_Call) Run(run func(ctx context.Context, imageName string)) *MockControlPlane_DeployIntent_Call {
654+ _c.Call.Run(func(args mock.Arguments) {
655+ var arg0 context.Context
656+ if args[0] != nil {
657+ arg0 = args[0].(context.Context)
658+ }
659+ var arg1 string
660+ if args[1] != nil {
661+ arg1 = args[1].(string)
662+ }
663+ run(
664+ arg0,
665+ arg1,
666+ )
667+ })
668+ return _c
669+}
670+
671+func (_c *MockControlPlane_DeployIntent_Call) Return(err error) *MockControlPlane_DeployIntent_Call {
672+ _c.Call.Return(err)
673+ return _c
674+}
675+
676+func (_c *MockControlPlane_DeployIntent_Call) RunAndReturn(run func(ctx context.Context, imageName string) error) *MockControlPlane_DeployIntent_Call {
677+ _c.Call.Return(run)
678+ return _c
679+}
680+
681+// DetectDatabases provides a mock function for the type MockControlPlane
682+func (_mock *MockControlPlane) DetectDatabases(ctx context.Context, backupDomain string) ([]dto.DatabaseInfo, error) {
683+ ret := _mock.Called(ctx, backupDomain)
684+
685+ if len(ret) == 0 {
686+ panic("no return value specified for DetectDatabases")
687+ }
688+
689+ var r0 []dto.DatabaseInfo
690+ var r1 error
691+ if returnFunc, ok := ret.Get(0).(func(context.Context, string) ([]dto.DatabaseInfo, error)); ok {
692+ return returnFunc(ctx, backupDomain)
693+ }
694+ if returnFunc, ok := ret.Get(0).(func(context.Context, string) []dto.DatabaseInfo); ok {
695+ r0 = returnFunc(ctx, backupDomain)
696+ } else {
697+ if ret.Get(0) != nil {
698+ r0 = ret.Get(0).([]dto.DatabaseInfo)
699+ }
700+ }
701+ if returnFunc, ok := ret.Get(1).(func(context.Context, string) error); ok {
702+ r1 = returnFunc(ctx, backupDomain)
703+ } else {
704+ r1 = ret.Error(1)
705+ }
706+ return r0, r1
707+}
708+
709+// MockControlPlane_DetectDatabases_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'DetectDatabases'
710+type MockControlPlane_DetectDatabases_Call struct {
711+ *mock.Call
712+}
713+
714+// DetectDatabases is a helper method to define mock.On call
715+// - ctx context.Context
716+// - backupDomain string
717+func (_e *MockControlPlane_Expecter) DetectDatabases(ctx interface{}, backupDomain interface{}) *MockControlPlane_DetectDatabases_Call {
718+ return &MockControlPlane_DetectDatabases_Call{Call: _e.mock.On("DetectDatabases", ctx, backupDomain)}
719+}
720+
721+func (_c *MockControlPlane_DetectDatabases_Call) Run(run func(ctx context.Context, backupDomain string)) *MockControlPlane_DetectDatabases_Call {
722+ _c.Call.Run(func(args mock.Arguments) {
723+ var arg0 context.Context
724+ if args[0] != nil {
725+ arg0 = args[0].(context.Context)
726+ }
727+ var arg1 string
728+ if args[1] != nil {
729+ arg1 = args[1].(string)
730+ }
731+ run(
732+ arg0,
733+ arg1,
734+ )
735+ })
736+ return _c
737+}
738+
739+func (_c *MockControlPlane_DetectDatabases_Call) Return(databaseInfos []dto.DatabaseInfo, err error) *MockControlPlane_DetectDatabases_Call {
740+ _c.Call.Return(databaseInfos, err)
741+ return _c
742+}
743+
744+func (_c *MockControlPlane_DetectDatabases_Call) RunAndReturn(run func(ctx context.Context, backupDomain string) ([]dto.DatabaseInfo, error)) *MockControlPlane_DetectDatabases_Call {
745+ _c.Call.Return(run)
746+ return _c
747+}
748+
749+// FindAttachmentTargetsByImage provides a mock function for the type MockControlPlane
750+func (_mock *MockControlPlane) FindAttachmentTargetsByImage(ctx context.Context, imageName string) ([]string, error) {
751+ ret := _mock.Called(ctx, imageName)
752+
753+ if len(ret) == 0 {
754+ panic("no return value specified for FindAttachmentTargetsByImage")
755+ }
756+
757+ var r0 []string
758+ var r1 error
759+ if returnFunc, ok := ret.Get(0).(func(context.Context, string) ([]string, error)); ok {
760+ return returnFunc(ctx, imageName)
761+ }
762+ if returnFunc, ok := ret.Get(0).(func(context.Context, string) []string); ok {
763+ r0 = returnFunc(ctx, imageName)
764+ } else {
765+ if ret.Get(0) != nil {
766+ r0 = ret.Get(0).([]string)
767+ }
768+ }
769+ if returnFunc, ok := ret.Get(1).(func(context.Context, string) error); ok {
770+ r1 = returnFunc(ctx, imageName)
771+ } else {
772+ r1 = ret.Error(1)
773+ }
774+ return r0, r1
775+}
776+
777+// MockControlPlane_FindAttachmentTargetsByImage_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'FindAttachmentTargetsByImage'
778+type MockControlPlane_FindAttachmentTargetsByImage_Call struct {
779+ *mock.Call
780+}
781+
782+// FindAttachmentTargetsByImage is a helper method to define mock.On call
783+// - ctx context.Context
784+// - imageName string
785+func (_e *MockControlPlane_Expecter) FindAttachmentTargetsByImage(ctx interface{}, imageName interface{}) *MockControlPlane_FindAttachmentTargetsByImage_Call {
786+ return &MockControlPlane_FindAttachmentTargetsByImage_Call{Call: _e.mock.On("FindAttachmentTargetsByImage", ctx, imageName)}
787+}
788+
789+func (_c *MockControlPlane_FindAttachmentTargetsByImage_Call) Run(run func(ctx context.Context, imageName string)) *MockControlPlane_FindAttachmentTargetsByImage_Call {
790+ _c.Call.Run(func(args mock.Arguments) {
791+ var arg0 context.Context
792+ if args[0] != nil {
793+ arg0 = args[0].(context.Context)
794+ }
795+ var arg1 string
796+ if args[1] != nil {
797+ arg1 = args[1].(string)
798+ }
799+ run(
800+ arg0,
801+ arg1,
802+ )
803+ })
804+ return _c
805+}
806+
807+func (_c *MockControlPlane_FindAttachmentTargetsByImage_Call) Return(strings []string, err error) *MockControlPlane_FindAttachmentTargetsByImage_Call {
808+ _c.Call.Return(strings, err)
809+ return _c
810+}
811+
812+func (_c *MockControlPlane_FindAttachmentTargetsByImage_Call) RunAndReturn(run func(ctx context.Context, imageName string) ([]string, error)) *MockControlPlane_FindAttachmentTargetsByImage_Call {
813+ _c.Call.Return(run)
814+ return _c
815+}
816+
817+// FindRoutesByImage provides a mock function for the type MockControlPlane
818+func (_mock *MockControlPlane) FindRoutesByImage(ctx context.Context, imageName string) ([]domain.Route, error) {
819+ ret := _mock.Called(ctx, imageName)
820+
821+ if len(ret) == 0 {
822+ panic("no return value specified for FindRoutesByImage")
823+ }
824+
825+ var r0 []domain.Route
826+ var r1 error
827+ if returnFunc, ok := ret.Get(0).(func(context.Context, string) ([]domain.Route, error)); ok {
828+ return returnFunc(ctx, imageName)
829+ }
830+ if returnFunc, ok := ret.Get(0).(func(context.Context, string) []domain.Route); ok {
831+ r0 = returnFunc(ctx, imageName)
832+ } else {
833+ if ret.Get(0) != nil {
834+ r0 = ret.Get(0).([]domain.Route)
835+ }
836+ }
837+ if returnFunc, ok := ret.Get(1).(func(context.Context, string) error); ok {
838+ r1 = returnFunc(ctx, imageName)
839+ } else {
840+ r1 = ret.Error(1)
841+ }
842+ return r0, r1
843+}
844+
845+// MockControlPlane_FindRoutesByImage_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'FindRoutesByImage'
846+type MockControlPlane_FindRoutesByImage_Call struct {
847+ *mock.Call
848+}
849+
850+// FindRoutesByImage is a helper method to define mock.On call
851+// - ctx context.Context
852+// - imageName string
853+func (_e *MockControlPlane_Expecter) FindRoutesByImage(ctx interface{}, imageName interface{}) *MockControlPlane_FindRoutesByImage_Call {
854+ return &MockControlPlane_FindRoutesByImage_Call{Call: _e.mock.On("FindRoutesByImage", ctx, imageName)}
855+}
856+
857+func (_c *MockControlPlane_FindRoutesByImage_Call) Run(run func(ctx context.Context, imageName string)) *MockControlPlane_FindRoutesByImage_Call {
858+ _c.Call.Run(func(args mock.Arguments) {
859+ var arg0 context.Context
860+ if args[0] != nil {
861+ arg0 = args[0].(context.Context)
862+ }
863+ var arg1 string
864+ if args[1] != nil {
865+ arg1 = args[1].(string)
866+ }
867+ run(
868+ arg0,
869+ arg1,
870+ )
871+ })
872+ return _c
873+}
874+
875+func (_c *MockControlPlane_FindRoutesByImage_Call) Return(routes []domain.Route, err error) *MockControlPlane_FindRoutesByImage_Call {
876+ _c.Call.Return(routes, err)
877+ return _c
878+}
879+
880+func (_c *MockControlPlane_FindRoutesByImage_Call) RunAndReturn(run func(ctx context.Context, imageName string) ([]domain.Route, error)) *MockControlPlane_FindRoutesByImage_Call {
881+ _c.Call.Return(run)
882+ return _c
883+}
884+
885+// GetAllAttachmentsConfig provides a mock function for the type MockControlPlane
886+func (_mock *MockControlPlane) GetAllAttachmentsConfig(ctx context.Context) (map[string][]string, error) {
887+ ret := _mock.Called(ctx)
888+
889+ if len(ret) == 0 {
890+ panic("no return value specified for GetAllAttachmentsConfig")
891+ }
892+
893+ var r0 map[string][]string
894+ var r1 error
895+ if returnFunc, ok := ret.Get(0).(func(context.Context) (map[string][]string, error)); ok {
896+ return returnFunc(ctx)
897+ }
898+ if returnFunc, ok := ret.Get(0).(func(context.Context) map[string][]string); ok {
899+ r0 = returnFunc(ctx)
900+ } else {
901+ if ret.Get(0) != nil {
902+ r0 = ret.Get(0).(map[string][]string)
903+ }
904+ }
905+ if returnFunc, ok := ret.Get(1).(func(context.Context) error); ok {
906+ r1 = returnFunc(ctx)
907+ } else {
908+ r1 = ret.Error(1)
909+ }
910+ return r0, r1
911+}
912+
913+// MockControlPlane_GetAllAttachmentsConfig_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetAllAttachmentsConfig'
914+type MockControlPlane_GetAllAttachmentsConfig_Call struct {
915+ *mock.Call
916+}
917+
918+// GetAllAttachmentsConfig is a helper method to define mock.On call
919+// - ctx context.Context
920+func (_e *MockControlPlane_Expecter) GetAllAttachmentsConfig(ctx interface{}) *MockControlPlane_GetAllAttachmentsConfig_Call {
921+ return &MockControlPlane_GetAllAttachmentsConfig_Call{Call: _e.mock.On("GetAllAttachmentsConfig", ctx)}
922+}
923+
924+func (_c *MockControlPlane_GetAllAttachmentsConfig_Call) Run(run func(ctx context.Context)) *MockControlPlane_GetAllAttachmentsConfig_Call {
925+ _c.Call.Run(func(args mock.Arguments) {
926+ var arg0 context.Context
927+ if args[0] != nil {
928+ arg0 = args[0].(context.Context)
929+ }
930+ run(
931+ arg0,
932+ )
933+ })
934+ return _c
935+}
936+
937+func (_c *MockControlPlane_GetAllAttachmentsConfig_Call) Return(stringToStrings map[string][]string, err error) *MockControlPlane_GetAllAttachmentsConfig_Call {
938+ _c.Call.Return(stringToStrings, err)
939+ return _c
940+}
941+
942+func (_c *MockControlPlane_GetAllAttachmentsConfig_Call) RunAndReturn(run func(ctx context.Context) (map[string][]string, error)) *MockControlPlane_GetAllAttachmentsConfig_Call {
943+ _c.Call.Return(run)
944+ return _c
945+}
946+
947+// GetAttachmentsConfig provides a mock function for the type MockControlPlane
948+func (_mock *MockControlPlane) GetAttachmentsConfig(ctx context.Context, domainOrGroup string) ([]string, error) {
949+ ret := _mock.Called(ctx, domainOrGroup)
950+
951+ if len(ret) == 0 {
952+ panic("no return value specified for GetAttachmentsConfig")
953+ }
954+
955+ var r0 []string
956+ var r1 error
957+ if returnFunc, ok := ret.Get(0).(func(context.Context, string) ([]string, error)); ok {
958+ return returnFunc(ctx, domainOrGroup)
959+ }
960+ if returnFunc, ok := ret.Get(0).(func(context.Context, string) []string); ok {
961+ r0 = returnFunc(ctx, domainOrGroup)
962+ } else {
963+ if ret.Get(0) != nil {
964+ r0 = ret.Get(0).([]string)
965+ }
966+ }
967+ if returnFunc, ok := ret.Get(1).(func(context.Context, string) error); ok {
968+ r1 = returnFunc(ctx, domainOrGroup)
969+ } else {
970+ r1 = ret.Error(1)
971+ }
972+ return r0, r1
973+}
974+
975+// MockControlPlane_GetAttachmentsConfig_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetAttachmentsConfig'
976+type MockControlPlane_GetAttachmentsConfig_Call struct {
977+ *mock.Call
978+}
979+
980+// GetAttachmentsConfig is a helper method to define mock.On call
981+// - ctx context.Context
982+// - domainOrGroup string
983+func (_e *MockControlPlane_Expecter) GetAttachmentsConfig(ctx interface{}, domainOrGroup interface{}) *MockControlPlane_GetAttachmentsConfig_Call {
984+ return &MockControlPlane_GetAttachmentsConfig_Call{Call: _e.mock.On("GetAttachmentsConfig", ctx, domainOrGroup)}
985+}
986+
987+func (_c *MockControlPlane_GetAttachmentsConfig_Call) Run(run func(ctx context.Context, domainOrGroup string)) *MockControlPlane_GetAttachmentsConfig_Call {
988+ _c.Call.Run(func(args mock.Arguments) {
989+ var arg0 context.Context
990+ if args[0] != nil {
991+ arg0 = args[0].(context.Context)
992+ }
993+ var arg1 string
994+ if args[1] != nil {
995+ arg1 = args[1].(string)
996+ }
997+ run(
998+ arg0,
999+ arg1,
1000+ )
1001+ })
1002+ return _c
1003+}
1004+
1005+func (_c *MockControlPlane_GetAttachmentsConfig_Call) Return(strings []string, err error) *MockControlPlane_GetAttachmentsConfig_Call {
1006+ _c.Call.Return(strings, err)
1007+ return _c
1008+}
1009+
1010+func (_c *MockControlPlane_GetAttachmentsConfig_Call) RunAndReturn(run func(ctx context.Context, domainOrGroup string) ([]string, error)) *MockControlPlane_GetAttachmentsConfig_Call {
1011+ _c.Call.Return(run)
1012+ return _c
1013+}
1014+
1015+// GetAutoRouteAllowedDomains provides a mock function for the type MockControlPlane
1016+func (_mock *MockControlPlane) GetAutoRouteAllowedDomains(ctx context.Context) ([]string, error) {
1017+ ret := _mock.Called(ctx)
1018+
1019+ if len(ret) == 0 {
1020+ panic("no return value specified for GetAutoRouteAllowedDomains")
1021+ }
1022+
1023+ var r0 []string
1024+ var r1 error
1025+ if returnFunc, ok := ret.Get(0).(func(context.Context) ([]string, error)); ok {
1026+ return returnFunc(ctx)
1027+ }
1028+ if returnFunc, ok := ret.Get(0).(func(context.Context) []string); ok {
1029+ r0 = returnFunc(ctx)
1030+ } else {
1031+ if ret.Get(0) != nil {
1032+ r0 = ret.Get(0).([]string)
1033+ }
1034+ }
1035+ if returnFunc, ok := ret.Get(1).(func(context.Context) error); ok {
1036+ r1 = returnFunc(ctx)
1037+ } else {
1038+ r1 = ret.Error(1)
1039+ }
1040+ return r0, r1
1041+}
1042+
1043+// MockControlPlane_GetAutoRouteAllowedDomains_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetAutoRouteAllowedDomains'
1044+type MockControlPlane_GetAutoRouteAllowedDomains_Call struct {
1045+ *mock.Call
1046+}
1047+
1048+// GetAutoRouteAllowedDomains is a helper method to define mock.On call
1049+// - ctx context.Context
1050+func (_e *MockControlPlane_Expecter) GetAutoRouteAllowedDomains(ctx interface{}) *MockControlPlane_GetAutoRouteAllowedDomains_Call {
1051+ return &MockControlPlane_GetAutoRouteAllowedDomains_Call{Call: _e.mock.On("GetAutoRouteAllowedDomains", ctx)}
1052+}
1053+
1054+func (_c *MockControlPlane_GetAutoRouteAllowedDomains_Call) Run(run func(ctx context.Context)) *MockControlPlane_GetAutoRouteAllowedDomains_Call {
1055+ _c.Call.Run(func(args mock.Arguments) {
1056+ var arg0 context.Context
1057+ if args[0] != nil {
1058+ arg0 = args[0].(context.Context)
1059+ }
1060+ run(
1061+ arg0,
1062+ )
1063+ })
1064+ return _c
1065+}
1066+
1067+func (_c *MockControlPlane_GetAutoRouteAllowedDomains_Call) Return(strings []string, err error) *MockControlPlane_GetAutoRouteAllowedDomains_Call {
1068+ _c.Call.Return(strings, err)
1069+ return _c
1070+}
1071+
1072+func (_c *MockControlPlane_GetAutoRouteAllowedDomains_Call) RunAndReturn(run func(ctx context.Context) ([]string, error)) *MockControlPlane_GetAutoRouteAllowedDomains_Call {
1073+ _c.Call.Return(run)
1074+ return _c
1075+}
1076+
1077+// GetConfig provides a mock function for the type MockControlPlane
1078+func (_mock *MockControlPlane) GetConfig(ctx context.Context) (*remote.Config, error) {
1079+ ret := _mock.Called(ctx)
1080+
1081+ if len(ret) == 0 {
1082+ panic("no return value specified for GetConfig")
1083+ }
1084+
1085+ var r0 *remote.Config
1086+ var r1 error
1087+ if returnFunc, ok := ret.Get(0).(func(context.Context) (*remote.Config, error)); ok {
1088+ return returnFunc(ctx)
1089+ }
1090+ if returnFunc, ok := ret.Get(0).(func(context.Context) *remote.Config); ok {
1091+ r0 = returnFunc(ctx)
1092+ } else {
1093+ if ret.Get(0) != nil {
1094+ r0 = ret.Get(0).(*remote.Config)
1095+ }
1096+ }
1097+ if returnFunc, ok := ret.Get(1).(func(context.Context) error); ok {
1098+ r1 = returnFunc(ctx)
1099+ } else {
1100+ r1 = ret.Error(1)
1101+ }
1102+ return r0, r1
1103+}
1104+
1105+// MockControlPlane_GetConfig_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetConfig'
1106+type MockControlPlane_GetConfig_Call struct {
1107+ *mock.Call
1108+}
1109+
1110+// GetConfig is a helper method to define mock.On call
1111+// - ctx context.Context
1112+func (_e *MockControlPlane_Expecter) GetConfig(ctx interface{}) *MockControlPlane_GetConfig_Call {
1113+ return &MockControlPlane_GetConfig_Call{Call: _e.mock.On("GetConfig", ctx)}
1114+}
1115+
1116+func (_c *MockControlPlane_GetConfig_Call) Run(run func(ctx context.Context)) *MockControlPlane_GetConfig_Call {
1117+ _c.Call.Run(func(args mock.Arguments) {
1118+ var arg0 context.Context
1119+ if args[0] != nil {
1120+ arg0 = args[0].(context.Context)
1121+ }
1122+ run(
1123+ arg0,
1124+ )
1125+ })
1126+ return _c
1127+}
1128+
1129+func (_c *MockControlPlane_GetConfig_Call) Return(config *remote.Config, err error) *MockControlPlane_GetConfig_Call {
1130+ _c.Call.Return(config, err)
1131+ return _c
1132+}
1133+
1134+func (_c *MockControlPlane_GetConfig_Call) RunAndReturn(run func(ctx context.Context) (*remote.Config, error)) *MockControlPlane_GetConfig_Call {
1135+ _c.Call.Return(run)
1136+ return _c
1137+}
1138+
1139+// GetContainerLogs provides a mock function for the type MockControlPlane
1140+func (_mock *MockControlPlane) GetContainerLogs(ctx context.Context, logDomain string, lines int) ([]string, error) {
1141+ ret := _mock.Called(ctx, logDomain, lines)
1142+
1143+ if len(ret) == 0 {
1144+ panic("no return value specified for GetContainerLogs")
1145+ }
1146+
1147+ var r0 []string
1148+ var r1 error
1149+ if returnFunc, ok := ret.Get(0).(func(context.Context, string, int) ([]string, error)); ok {
1150+ return returnFunc(ctx, logDomain, lines)
1151+ }
1152+ if returnFunc, ok := ret.Get(0).(func(context.Context, string, int) []string); ok {
1153+ r0 = returnFunc(ctx, logDomain, lines)
1154+ } else {
1155+ if ret.Get(0) != nil {
1156+ r0 = ret.Get(0).([]string)
1157+ }
1158+ }
1159+ if returnFunc, ok := ret.Get(1).(func(context.Context, string, int) error); ok {
1160+ r1 = returnFunc(ctx, logDomain, lines)
1161+ } else {
1162+ r1 = ret.Error(1)
1163+ }
1164+ return r0, r1
1165+}
1166+
1167+// MockControlPlane_GetContainerLogs_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetContainerLogs'
1168+type MockControlPlane_GetContainerLogs_Call struct {
1169+ *mock.Call
1170+}
1171+
1172+// GetContainerLogs is a helper method to define mock.On call
1173+// - ctx context.Context
1174+// - logDomain string
1175+// - lines int
1176+func (_e *MockControlPlane_Expecter) GetContainerLogs(ctx interface{}, logDomain interface{}, lines interface{}) *MockControlPlane_GetContainerLogs_Call {
1177+ return &MockControlPlane_GetContainerLogs_Call{Call: _e.mock.On("GetContainerLogs", ctx, logDomain, lines)}
1178+}
1179+
1180+func (_c *MockControlPlane_GetContainerLogs_Call) Run(run func(ctx context.Context, logDomain string, lines int)) *MockControlPlane_GetContainerLogs_Call {
1181+ _c.Call.Run(func(args mock.Arguments) {
1182+ var arg0 context.Context
1183+ if args[0] != nil {
1184+ arg0 = args[0].(context.Context)
1185+ }
1186+ var arg1 string
1187+ if args[1] != nil {
1188+ arg1 = args[1].(string)
1189+ }
1190+ var arg2 int
1191+ if args[2] != nil {
1192+ arg2 = args[2].(int)
1193+ }
1194+ run(
1195+ arg0,
1196+ arg1,
1197+ arg2,
1198+ )
1199+ })
1200+ return _c
1201+}
1202+
1203+func (_c *MockControlPlane_GetContainerLogs_Call) Return(strings []string, err error) *MockControlPlane_GetContainerLogs_Call {
1204+ _c.Call.Return(strings, err)
1205+ return _c
1206+}
1207+
1208+func (_c *MockControlPlane_GetContainerLogs_Call) RunAndReturn(run func(ctx context.Context, logDomain string, lines int) ([]string, error)) *MockControlPlane_GetContainerLogs_Call {
1209+ _c.Call.Return(run)
1210+ return _c
1211+}
1212+
1213+// GetHealth provides a mock function for the type MockControlPlane
1214+func (_mock *MockControlPlane) GetHealth(ctx context.Context) (map[string]*remote.RouteHealth, error) {
1215+ ret := _mock.Called(ctx)
1216+
1217+ if len(ret) == 0 {
1218+ panic("no return value specified for GetHealth")
1219+ }
1220+
1221+ var r0 map[string]*remote.RouteHealth
1222+ var r1 error
1223+ if returnFunc, ok := ret.Get(0).(func(context.Context) (map[string]*remote.RouteHealth, error)); ok {
1224+ return returnFunc(ctx)
1225+ }
1226+ if returnFunc, ok := ret.Get(0).(func(context.Context) map[string]*remote.RouteHealth); ok {
1227+ r0 = returnFunc(ctx)
1228+ } else {
1229+ if ret.Get(0) != nil {
1230+ r0 = ret.Get(0).(map[string]*remote.RouteHealth)
1231+ }
1232+ }
1233+ if returnFunc, ok := ret.Get(1).(func(context.Context) error); ok {
1234+ r1 = returnFunc(ctx)
1235+ } else {
1236+ r1 = ret.Error(1)
1237+ }
1238+ return r0, r1
1239+}
1240+
1241+// MockControlPlane_GetHealth_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetHealth'
1242+type MockControlPlane_GetHealth_Call struct {
1243+ *mock.Call
1244+}
1245+
1246+// GetHealth is a helper method to define mock.On call
1247+// - ctx context.Context
1248+func (_e *MockControlPlane_Expecter) GetHealth(ctx interface{}) *MockControlPlane_GetHealth_Call {
1249+ return &MockControlPlane_GetHealth_Call{Call: _e.mock.On("GetHealth", ctx)}
1250+}
1251+
1252+func (_c *MockControlPlane_GetHealth_Call) Run(run func(ctx context.Context)) *MockControlPlane_GetHealth_Call {
1253+ _c.Call.Run(func(args mock.Arguments) {
1254+ var arg0 context.Context
1255+ if args[0] != nil {
1256+ arg0 = args[0].(context.Context)
1257+ }
1258+ run(
1259+ arg0,
1260+ )
1261+ })
1262+ return _c
1263+}
1264+
1265+func (_c *MockControlPlane_GetHealth_Call) Return(stringToRouteHealth map[string]*remote.RouteHealth, err error) *MockControlPlane_GetHealth_Call {
1266+ _c.Call.Return(stringToRouteHealth, err)
1267+ return _c
1268+}
1269+
1270+func (_c *MockControlPlane_GetHealth_Call) RunAndReturn(run func(ctx context.Context) (map[string]*remote.RouteHealth, error)) *MockControlPlane_GetHealth_Call {
1271+ _c.Call.Return(run)
1272+ return _c
1273+}
1274+
1275+// GetProcessLogs provides a mock function for the type MockControlPlane
1276+func (_mock *MockControlPlane) GetProcessLogs(ctx context.Context, lines int) ([]string, error) {
1277+ ret := _mock.Called(ctx, lines)
1278+
1279+ if len(ret) == 0 {
1280+ panic("no return value specified for GetProcessLogs")
1281+ }
1282+
1283+ var r0 []string
1284+ var r1 error
1285+ if returnFunc, ok := ret.Get(0).(func(context.Context, int) ([]string, error)); ok {
1286+ return returnFunc(ctx, lines)
1287+ }
1288+ if returnFunc, ok := ret.Get(0).(func(context.Context, int) []string); ok {
1289+ r0 = returnFunc(ctx, lines)
1290+ } else {
1291+ if ret.Get(0) != nil {
1292+ r0 = ret.Get(0).([]string)
1293+ }
1294+ }
1295+ if returnFunc, ok := ret.Get(1).(func(context.Context, int) error); ok {
1296+ r1 = returnFunc(ctx, lines)
1297+ } else {
1298+ r1 = ret.Error(1)
1299+ }
1300+ return r0, r1
1301+}
1302+
1303+// MockControlPlane_GetProcessLogs_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetProcessLogs'
1304+type MockControlPlane_GetProcessLogs_Call struct {
1305+ *mock.Call
1306+}
1307+
1308+// GetProcessLogs is a helper method to define mock.On call
1309+// - ctx context.Context
1310+// - lines int
1311+func (_e *MockControlPlane_Expecter) GetProcessLogs(ctx interface{}, lines interface{}) *MockControlPlane_GetProcessLogs_Call {
1312+ return &MockControlPlane_GetProcessLogs_Call{Call: _e.mock.On("GetProcessLogs", ctx, lines)}
1313+}
1314+
1315+func (_c *MockControlPlane_GetProcessLogs_Call) Run(run func(ctx context.Context, lines int)) *MockControlPlane_GetProcessLogs_Call {
1316+ _c.Call.Run(func(args mock.Arguments) {
1317+ var arg0 context.Context
1318+ if args[0] != nil {
1319+ arg0 = args[0].(context.Context)
1320+ }
1321+ var arg1 int
1322+ if args[1] != nil {
1323+ arg1 = args[1].(int)
1324+ }
1325+ run(
1326+ arg0,
1327+ arg1,
1328+ )
1329+ })
1330+ return _c
1331+}
1332+
1333+func (_c *MockControlPlane_GetProcessLogs_Call) Return(strings []string, err error) *MockControlPlane_GetProcessLogs_Call {
1334+ _c.Call.Return(strings, err)
1335+ return _c
1336+}
1337+
1338+func (_c *MockControlPlane_GetProcessLogs_Call) RunAndReturn(run func(ctx context.Context, lines int) ([]string, error)) *MockControlPlane_GetProcessLogs_Call {
1339+ _c.Call.Return(run)
1340+ return _c
1341+}
1342+
1343+// GetRoute provides a mock function for the type MockControlPlane
1344+func (_mock *MockControlPlane) GetRoute(ctx context.Context, routeDomain string) (*domain.Route, error) {
1345+ ret := _mock.Called(ctx, routeDomain)
1346+
1347+ if len(ret) == 0 {
1348+ panic("no return value specified for GetRoute")
1349+ }
1350+
1351+ var r0 *domain.Route
1352+ var r1 error
1353+ if returnFunc, ok := ret.Get(0).(func(context.Context, string) (*domain.Route, error)); ok {
1354+ return returnFunc(ctx, routeDomain)
1355+ }
1356+ if returnFunc, ok := ret.Get(0).(func(context.Context, string) *domain.Route); ok {
1357+ r0 = returnFunc(ctx, routeDomain)
1358+ } else {
1359+ if ret.Get(0) != nil {
1360+ r0 = ret.Get(0).(*domain.Route)
1361+ }
1362+ }
1363+ if returnFunc, ok := ret.Get(1).(func(context.Context, string) error); ok {
1364+ r1 = returnFunc(ctx, routeDomain)
1365+ } else {
1366+ r1 = ret.Error(1)
1367+ }
1368+ return r0, r1
1369+}
1370+
1371+// MockControlPlane_GetRoute_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetRoute'
1372+type MockControlPlane_GetRoute_Call struct {
1373+ *mock.Call
1374+}
1375+
1376+// GetRoute is a helper method to define mock.On call
1377+// - ctx context.Context
1378+// - routeDomain string
1379+func (_e *MockControlPlane_Expecter) GetRoute(ctx interface{}, routeDomain interface{}) *MockControlPlane_GetRoute_Call {
1380+ return &MockControlPlane_GetRoute_Call{Call: _e.mock.On("GetRoute", ctx, routeDomain)}
1381+}
1382+
1383+func (_c *MockControlPlane_GetRoute_Call) Run(run func(ctx context.Context, routeDomain string)) *MockControlPlane_GetRoute_Call {
1384+ _c.Call.Run(func(args mock.Arguments) {
1385+ var arg0 context.Context
1386+ if args[0] != nil {
1387+ arg0 = args[0].(context.Context)
1388+ }
1389+ var arg1 string
1390+ if args[1] != nil {
1391+ arg1 = args[1].(string)
1392+ }
1393+ run(
1394+ arg0,
1395+ arg1,
1396+ )
1397+ })
1398+ return _c
1399+}
1400+
1401+func (_c *MockControlPlane_GetRoute_Call) Return(route *domain.Route, err error) *MockControlPlane_GetRoute_Call {
1402+ _c.Call.Return(route, err)
1403+ return _c
1404+}
1405+
1406+func (_c *MockControlPlane_GetRoute_Call) RunAndReturn(run func(ctx context.Context, routeDomain string) (*domain.Route, error)) *MockControlPlane_GetRoute_Call {
1407+ _c.Call.Return(run)
1408+ return _c
1409+}
1410+
1411+// GetStatus provides a mock function for the type MockControlPlane
1412+func (_mock *MockControlPlane) GetStatus(ctx context.Context) (*remote.Status, error) {
1413+ ret := _mock.Called(ctx)
1414+
1415+ if len(ret) == 0 {
1416+ panic("no return value specified for GetStatus")
1417+ }
1418+
1419+ var r0 *remote.Status
1420+ var r1 error
1421+ if returnFunc, ok := ret.Get(0).(func(context.Context) (*remote.Status, error)); ok {
1422+ return returnFunc(ctx)
1423+ }
1424+ if returnFunc, ok := ret.Get(0).(func(context.Context) *remote.Status); ok {
1425+ r0 = returnFunc(ctx)
1426+ } else {
1427+ if ret.Get(0) != nil {
1428+ r0 = ret.Get(0).(*remote.Status)
1429+ }
1430+ }
1431+ if returnFunc, ok := ret.Get(1).(func(context.Context) error); ok {
1432+ r1 = returnFunc(ctx)
1433+ } else {
1434+ r1 = ret.Error(1)
1435+ }
1436+ return r0, r1
1437+}
1438+
1439+// MockControlPlane_GetStatus_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetStatus'
1440+type MockControlPlane_GetStatus_Call struct {
1441+ *mock.Call
1442+}
1443+
1444+// GetStatus is a helper method to define mock.On call
1445+// - ctx context.Context
1446+func (_e *MockControlPlane_Expecter) GetStatus(ctx interface{}) *MockControlPlane_GetStatus_Call {
1447+ return &MockControlPlane_GetStatus_Call{Call: _e.mock.On("GetStatus", ctx)}
1448+}
1449+
1450+func (_c *MockControlPlane_GetStatus_Call) Run(run func(ctx context.Context)) *MockControlPlane_GetStatus_Call {
1451+ _c.Call.Run(func(args mock.Arguments) {
1452+ var arg0 context.Context
1453+ if args[0] != nil {
1454+ arg0 = args[0].(context.Context)
1455+ }
1456+ run(
1457+ arg0,
1458+ )
1459+ })
1460+ return _c
1461+}
1462+
1463+func (_c *MockControlPlane_GetStatus_Call) Return(status *remote.Status, err error) *MockControlPlane_GetStatus_Call {
1464+ _c.Call.Return(status, err)
1465+ return _c
1466+}
1467+
1468+func (_c *MockControlPlane_GetStatus_Call) RunAndReturn(run func(ctx context.Context) (*remote.Status, error)) *MockControlPlane_GetStatus_Call {
1469+ _c.Call.Return(run)
1470+ return _c
1471+}
1472+
1473+// GetTLSStatus provides a mock function for the type MockControlPlane
1474+func (_mock *MockControlPlane) GetTLSStatus(ctx context.Context) (*dto.TLSStatusResponse, error) {
1475+ ret := _mock.Called(ctx)
1476+
1477+ if len(ret) == 0 {
1478+ panic("no return value specified for GetTLSStatus")
1479+ }
1480+
1481+ var r0 *dto.TLSStatusResponse
1482+ var r1 error
1483+ if returnFunc, ok := ret.Get(0).(func(context.Context) (*dto.TLSStatusResponse, error)); ok {
1484+ return returnFunc(ctx)
1485+ }
1486+ if returnFunc, ok := ret.Get(0).(func(context.Context) *dto.TLSStatusResponse); ok {
1487+ r0 = returnFunc(ctx)
1488+ } else {
1489+ if ret.Get(0) != nil {
1490+ r0 = ret.Get(0).(*dto.TLSStatusResponse)
1491+ }
1492+ }
1493+ if returnFunc, ok := ret.Get(1).(func(context.Context) error); ok {
1494+ r1 = returnFunc(ctx)
1495+ } else {
1496+ r1 = ret.Error(1)
1497+ }
1498+ return r0, r1
1499+}
1500+
1501+// MockControlPlane_GetTLSStatus_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetTLSStatus'
1502+type MockControlPlane_GetTLSStatus_Call struct {
1503+ *mock.Call
1504+}
1505+
1506+// GetTLSStatus is a helper method to define mock.On call
1507+// - ctx context.Context
1508+func (_e *MockControlPlane_Expecter) GetTLSStatus(ctx interface{}) *MockControlPlane_GetTLSStatus_Call {
1509+ return &MockControlPlane_GetTLSStatus_Call{Call: _e.mock.On("GetTLSStatus", ctx)}
1510+}
1511+
1512+func (_c *MockControlPlane_GetTLSStatus_Call) Run(run func(ctx context.Context)) *MockControlPlane_GetTLSStatus_Call {
1513+ _c.Call.Run(func(args mock.Arguments) {
1514+ var arg0 context.Context
1515+ if args[0] != nil {
1516+ arg0 = args[0].(context.Context)
1517+ }
1518+ run(
1519+ arg0,
1520+ )
1521+ })
1522+ return _c
1523+}
1524+
1525+func (_c *MockControlPlane_GetTLSStatus_Call) Return(tLSStatusResponse *dto.TLSStatusResponse, err error) *MockControlPlane_GetTLSStatus_Call {
1526+ _c.Call.Return(tLSStatusResponse, err)
1527+ return _c
1528+}
1529+
1530+func (_c *MockControlPlane_GetTLSStatus_Call) RunAndReturn(run func(ctx context.Context) (*dto.TLSStatusResponse, error)) *MockControlPlane_GetTLSStatus_Call {
1531+ _c.Call.Return(run)
1532+ return _c
1533+}
1534+
1535+// ListBackups provides a mock function for the type MockControlPlane
1536+func (_mock *MockControlPlane) ListBackups(ctx context.Context, backupDomain string) ([]dto.BackupJob, error) {
1537+ ret := _mock.Called(ctx, backupDomain)
1538+
1539+ if len(ret) == 0 {
1540+ panic("no return value specified for ListBackups")
1541+ }
1542+
1543+ var r0 []dto.BackupJob
1544+ var r1 error
1545+ if returnFunc, ok := ret.Get(0).(func(context.Context, string) ([]dto.BackupJob, error)); ok {
1546+ return returnFunc(ctx, backupDomain)
1547+ }
1548+ if returnFunc, ok := ret.Get(0).(func(context.Context, string) []dto.BackupJob); ok {
1549+ r0 = returnFunc(ctx, backupDomain)
1550+ } else {
1551+ if ret.Get(0) != nil {
1552+ r0 = ret.Get(0).([]dto.BackupJob)
1553+ }
1554+ }
1555+ if returnFunc, ok := ret.Get(1).(func(context.Context, string) error); ok {
1556+ r1 = returnFunc(ctx, backupDomain)
1557+ } else {
1558+ r1 = ret.Error(1)
1559+ }
1560+ return r0, r1
1561+}
1562+
1563+// MockControlPlane_ListBackups_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ListBackups'
1564+type MockControlPlane_ListBackups_Call struct {
1565+ *mock.Call
1566+}
1567+
1568+// ListBackups is a helper method to define mock.On call
1569+// - ctx context.Context
1570+// - backupDomain string
1571+func (_e *MockControlPlane_Expecter) ListBackups(ctx interface{}, backupDomain interface{}) *MockControlPlane_ListBackups_Call {
1572+ return &MockControlPlane_ListBackups_Call{Call: _e.mock.On("ListBackups", ctx, backupDomain)}
1573+}
1574+
1575+func (_c *MockControlPlane_ListBackups_Call) Run(run func(ctx context.Context, backupDomain string)) *MockControlPlane_ListBackups_Call {
1576+ _c.Call.Run(func(args mock.Arguments) {
1577+ var arg0 context.Context
1578+ if args[0] != nil {
1579+ arg0 = args[0].(context.Context)
1580+ }
1581+ var arg1 string
1582+ if args[1] != nil {
1583+ arg1 = args[1].(string)
1584+ }
1585+ run(
1586+ arg0,
1587+ arg1,
1588+ )
1589+ })
1590+ return _c
1591+}
1592+
1593+func (_c *MockControlPlane_ListBackups_Call) Return(backupJobs []dto.BackupJob, err error) *MockControlPlane_ListBackups_Call {
1594+ _c.Call.Return(backupJobs, err)
1595+ return _c
1596+}
1597+
1598+func (_c *MockControlPlane_ListBackups_Call) RunAndReturn(run func(ctx context.Context, backupDomain string) ([]dto.BackupJob, error)) *MockControlPlane_ListBackups_Call {
1599+ _c.Call.Return(run)
1600+ return _c
1601+}
1602+
1603+// ListNetworks provides a mock function for the type MockControlPlane
1604+func (_mock *MockControlPlane) ListNetworks(ctx context.Context) ([]*domain.NetworkInfo, error) {
1605+ ret := _mock.Called(ctx)
1606+
1607+ if len(ret) == 0 {
1608+ panic("no return value specified for ListNetworks")
1609+ }
1610+
1611+ var r0 []*domain.NetworkInfo
1612+ var r1 error
1613+ if returnFunc, ok := ret.Get(0).(func(context.Context) ([]*domain.NetworkInfo, error)); ok {
1614+ return returnFunc(ctx)
1615+ }
1616+ if returnFunc, ok := ret.Get(0).(func(context.Context) []*domain.NetworkInfo); ok {
1617+ r0 = returnFunc(ctx)
1618+ } else {
1619+ if ret.Get(0) != nil {
1620+ r0 = ret.Get(0).([]*domain.NetworkInfo)
1621+ }
1622+ }
1623+ if returnFunc, ok := ret.Get(1).(func(context.Context) error); ok {
1624+ r1 = returnFunc(ctx)
1625+ } else {
1626+ r1 = ret.Error(1)
1627+ }
1628+ return r0, r1
1629+}
1630+
1631+// MockControlPlane_ListNetworks_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ListNetworks'
1632+type MockControlPlane_ListNetworks_Call struct {
1633+ *mock.Call
1634+}
1635+
1636+// ListNetworks is a helper method to define mock.On call
1637+// - ctx context.Context
1638+func (_e *MockControlPlane_Expecter) ListNetworks(ctx interface{}) *MockControlPlane_ListNetworks_Call {
1639+ return &MockControlPlane_ListNetworks_Call{Call: _e.mock.On("ListNetworks", ctx)}
1640+}
1641+
1642+func (_c *MockControlPlane_ListNetworks_Call) Run(run func(ctx context.Context)) *MockControlPlane_ListNetworks_Call {
1643+ _c.Call.Run(func(args mock.Arguments) {
1644+ var arg0 context.Context
1645+ if args[0] != nil {
1646+ arg0 = args[0].(context.Context)
1647+ }
1648+ run(
1649+ arg0,
1650+ )
1651+ })
1652+ return _c
1653+}
1654+
1655+func (_c *MockControlPlane_ListNetworks_Call) Return(networkInfos []*domain.NetworkInfo, err error) *MockControlPlane_ListNetworks_Call {
1656+ _c.Call.Return(networkInfos, err)
1657+ return _c
1658+}
1659+
1660+func (_c *MockControlPlane_ListNetworks_Call) RunAndReturn(run func(ctx context.Context) ([]*domain.NetworkInfo, error)) *MockControlPlane_ListNetworks_Call {
1661+ _c.Call.Return(run)
1662+ return _c
1663+}
1664+
1665+// ListOrphanedAttachments provides a mock function for the type MockControlPlane
1666+func (_mock *MockControlPlane) ListOrphanedAttachments(ctx context.Context) ([]domain.CleanupAttachment, error) {
1667+ ret := _mock.Called(ctx)
1668+
1669+ if len(ret) == 0 {
1670+ panic("no return value specified for ListOrphanedAttachments")
1671+ }
1672+
1673+ var r0 []domain.CleanupAttachment
1674+ var r1 error
1675+ if returnFunc, ok := ret.Get(0).(func(context.Context) ([]domain.CleanupAttachment, error)); ok {
1676+ return returnFunc(ctx)
1677+ }
1678+ if returnFunc, ok := ret.Get(0).(func(context.Context) []domain.CleanupAttachment); ok {
1679+ r0 = returnFunc(ctx)
1680+ } else {
1681+ if ret.Get(0) != nil {
1682+ r0 = ret.Get(0).([]domain.CleanupAttachment)
1683+ }
1684+ }
1685+ if returnFunc, ok := ret.Get(1).(func(context.Context) error); ok {
1686+ r1 = returnFunc(ctx)
1687+ } else {
1688+ r1 = ret.Error(1)
1689+ }
1690+ return r0, r1
1691+}
1692+
1693+// MockControlPlane_ListOrphanedAttachments_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ListOrphanedAttachments'
1694+type MockControlPlane_ListOrphanedAttachments_Call struct {
1695+ *mock.Call
1696+}
1697+
1698+// ListOrphanedAttachments is a helper method to define mock.On call
1699+// - ctx context.Context
1700+func (_e *MockControlPlane_Expecter) ListOrphanedAttachments(ctx interface{}) *MockControlPlane_ListOrphanedAttachments_Call {
1701+ return &MockControlPlane_ListOrphanedAttachments_Call{Call: _e.mock.On("ListOrphanedAttachments", ctx)}
1702+}
1703+
1704+func (_c *MockControlPlane_ListOrphanedAttachments_Call) Run(run func(ctx context.Context)) *MockControlPlane_ListOrphanedAttachments_Call {
1705+ _c.Call.Run(func(args mock.Arguments) {
1706+ var arg0 context.Context
1707+ if args[0] != nil {
1708+ arg0 = args[0].(context.Context)
1709+ }
1710+ run(
1711+ arg0,
1712+ )
1713+ })
1714+ return _c
1715+}
1716+
1717+func (_c *MockControlPlane_ListOrphanedAttachments_Call) Return(cleanupAttachments []domain.CleanupAttachment, err error) *MockControlPlane_ListOrphanedAttachments_Call {
1718+ _c.Call.Return(cleanupAttachments, err)
1719+ return _c
1720+}
1721+
1722+func (_c *MockControlPlane_ListOrphanedAttachments_Call) RunAndReturn(run func(ctx context.Context) ([]domain.CleanupAttachment, error)) *MockControlPlane_ListOrphanedAttachments_Call {
1723+ _c.Call.Return(run)
1724+ return _c
1725+}
1726+
1727+// ListRoutesWithDetails provides a mock function for the type MockControlPlane
1728+func (_mock *MockControlPlane) ListRoutesWithDetails(ctx context.Context) ([]remote.RouteInfo, error) {
1729+ ret := _mock.Called(ctx)
1730+
1731+ if len(ret) == 0 {
1732+ panic("no return value specified for ListRoutesWithDetails")
1733+ }
1734+
1735+ var r0 []remote.RouteInfo
1736+ var r1 error
1737+ if returnFunc, ok := ret.Get(0).(func(context.Context) ([]remote.RouteInfo, error)); ok {
1738+ return returnFunc(ctx)
1739+ }
1740+ if returnFunc, ok := ret.Get(0).(func(context.Context) []remote.RouteInfo); ok {
1741+ r0 = returnFunc(ctx)
1742+ } else {
1743+ if ret.Get(0) != nil {
1744+ r0 = ret.Get(0).([]remote.RouteInfo)
1745+ }
1746+ }
1747+ if returnFunc, ok := ret.Get(1).(func(context.Context) error); ok {
1748+ r1 = returnFunc(ctx)
1749+ } else {
1750+ r1 = ret.Error(1)
1751+ }
1752+ return r0, r1
1753+}
1754+
1755+// MockControlPlane_ListRoutesWithDetails_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ListRoutesWithDetails'
1756+type MockControlPlane_ListRoutesWithDetails_Call struct {
1757+ *mock.Call
1758+}
1759+
1760+// ListRoutesWithDetails is a helper method to define mock.On call
1761+// - ctx context.Context
1762+func (_e *MockControlPlane_Expecter) ListRoutesWithDetails(ctx interface{}) *MockControlPlane_ListRoutesWithDetails_Call {
1763+ return &MockControlPlane_ListRoutesWithDetails_Call{Call: _e.mock.On("ListRoutesWithDetails", ctx)}
1764+}
1765+
1766+func (_c *MockControlPlane_ListRoutesWithDetails_Call) Run(run func(ctx context.Context)) *MockControlPlane_ListRoutesWithDetails_Call {
1767+ _c.Call.Run(func(args mock.Arguments) {
1768+ var arg0 context.Context
1769+ if args[0] != nil {
1770+ arg0 = args[0].(context.Context)
1771+ }
1772+ run(
1773+ arg0,
1774+ )
1775+ })
1776+ return _c
1777+}
1778+
1779+func (_c *MockControlPlane_ListRoutesWithDetails_Call) Return(vs []remote.RouteInfo, err error) *MockControlPlane_ListRoutesWithDetails_Call {
1780+ _c.Call.Return(vs, err)
1781+ return _c
1782+}
1783+
1784+func (_c *MockControlPlane_ListRoutesWithDetails_Call) RunAndReturn(run func(ctx context.Context) ([]remote.RouteInfo, error)) *MockControlPlane_ListRoutesWithDetails_Call {
1785+ _c.Call.Return(run)
1786+ return _c
1787+}
1788+
1789+// ListSecretsWithAttachments provides a mock function for the type MockControlPlane
1790+func (_mock *MockControlPlane) ListSecretsWithAttachments(ctx context.Context, secretDomain string) (*remote.SecretsListResult, error) {
1791+ ret := _mock.Called(ctx, secretDomain)
1792+
1793+ if len(ret) == 0 {
1794+ panic("no return value specified for ListSecretsWithAttachments")
1795+ }
1796+
1797+ var r0 *remote.SecretsListResult
1798+ var r1 error
1799+ if returnFunc, ok := ret.Get(0).(func(context.Context, string) (*remote.SecretsListResult, error)); ok {
1800+ return returnFunc(ctx, secretDomain)
1801+ }
1802+ if returnFunc, ok := ret.Get(0).(func(context.Context, string) *remote.SecretsListResult); ok {
1803+ r0 = returnFunc(ctx, secretDomain)
1804+ } else {
1805+ if ret.Get(0) != nil {
1806+ r0 = ret.Get(0).(*remote.SecretsListResult)
1807+ }
1808+ }
1809+ if returnFunc, ok := ret.Get(1).(func(context.Context, string) error); ok {
1810+ r1 = returnFunc(ctx, secretDomain)
1811+ } else {
1812+ r1 = ret.Error(1)
1813+ }
1814+ return r0, r1
1815+}
1816+
1817+// MockControlPlane_ListSecretsWithAttachments_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ListSecretsWithAttachments'
1818+type MockControlPlane_ListSecretsWithAttachments_Call struct {
1819+ *mock.Call
1820+}
1821+
1822+// ListSecretsWithAttachments is a helper method to define mock.On call
1823+// - ctx context.Context
1824+// - secretDomain string
1825+func (_e *MockControlPlane_Expecter) ListSecretsWithAttachments(ctx interface{}, secretDomain interface{}) *MockControlPlane_ListSecretsWithAttachments_Call {
1826+ return &MockControlPlane_ListSecretsWithAttachments_Call{Call: _e.mock.On("ListSecretsWithAttachments", ctx, secretDomain)}
1827+}
1828+
1829+func (_c *MockControlPlane_ListSecretsWithAttachments_Call) Run(run func(ctx context.Context, secretDomain string)) *MockControlPlane_ListSecretsWithAttachments_Call {
1830+ _c.Call.Run(func(args mock.Arguments) {
1831+ var arg0 context.Context
1832+ if args[0] != nil {
1833+ arg0 = args[0].(context.Context)
1834+ }
1835+ var arg1 string
1836+ if args[1] != nil {
1837+ arg1 = args[1].(string)
1838+ }
1839+ run(
1840+ arg0,
1841+ arg1,
1842+ )
1843+ })
1844+ return _c
1845+}
1846+
1847+func (_c *MockControlPlane_ListSecretsWithAttachments_Call) Return(secretsListResult *remote.SecretsListResult, err error) *MockControlPlane_ListSecretsWithAttachments_Call {
1848+ _c.Call.Return(secretsListResult, err)
1849+ return _c
1850+}
1851+
1852+func (_c *MockControlPlane_ListSecretsWithAttachments_Call) RunAndReturn(run func(ctx context.Context, secretDomain string) (*remote.SecretsListResult, error)) *MockControlPlane_ListSecretsWithAttachments_Call {
1853+ _c.Call.Return(run)
1854+ return _c
1855+}
1856+
1857+// ListTags provides a mock function for the type MockControlPlane
1858+func (_mock *MockControlPlane) ListTags(ctx context.Context, repository string) ([]string, error) {
1859+ ret := _mock.Called(ctx, repository)
1860+
1861+ if len(ret) == 0 {
1862+ panic("no return value specified for ListTags")
1863+ }
1864+
1865+ var r0 []string
1866+ var r1 error
1867+ if returnFunc, ok := ret.Get(0).(func(context.Context, string) ([]string, error)); ok {
1868+ return returnFunc(ctx, repository)
1869+ }
1870+ if returnFunc, ok := ret.Get(0).(func(context.Context, string) []string); ok {
1871+ r0 = returnFunc(ctx, repository)
1872+ } else {
1873+ if ret.Get(0) != nil {
1874+ r0 = ret.Get(0).([]string)
1875+ }
1876+ }
1877+ if returnFunc, ok := ret.Get(1).(func(context.Context, string) error); ok {
1878+ r1 = returnFunc(ctx, repository)
1879+ } else {
1880+ r1 = ret.Error(1)
1881+ }
1882+ return r0, r1
1883+}
1884+
1885+// MockControlPlane_ListTags_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ListTags'
1886+type MockControlPlane_ListTags_Call struct {
1887+ *mock.Call
1888+}
1889+
1890+// ListTags is a helper method to define mock.On call
1891+// - ctx context.Context
1892+// - repository string
1893+func (_e *MockControlPlane_Expecter) ListTags(ctx interface{}, repository interface{}) *MockControlPlane_ListTags_Call {
1894+ return &MockControlPlane_ListTags_Call{Call: _e.mock.On("ListTags", ctx, repository)}
1895+}
1896+
1897+func (_c *MockControlPlane_ListTags_Call) Run(run func(ctx context.Context, repository string)) *MockControlPlane_ListTags_Call {
1898+ _c.Call.Run(func(args mock.Arguments) {
1899+ var arg0 context.Context
1900+ if args[0] != nil {
1901+ arg0 = args[0].(context.Context)
1902+ }
1903+ var arg1 string
1904+ if args[1] != nil {
1905+ arg1 = args[1].(string)
1906+ }
1907+ run(
1908+ arg0,
1909+ arg1,
1910+ )
1911+ })
1912+ return _c
1913+}
1914+
1915+func (_c *MockControlPlane_ListTags_Call) Return(strings []string, err error) *MockControlPlane_ListTags_Call {
1916+ _c.Call.Return(strings, err)
1917+ return _c
1918+}
1919+
1920+func (_c *MockControlPlane_ListTags_Call) RunAndReturn(run func(ctx context.Context, repository string) ([]string, error)) *MockControlPlane_ListTags_Call {
1921+ _c.Call.Return(run)
1922+ return _c
1923+}
1924+
1925+// ListVolumes provides a mock function for the type MockControlPlane
1926+func (_mock *MockControlPlane) ListVolumes(ctx context.Context) ([]dto.Volume, error) {
1927+ ret := _mock.Called(ctx)
1928+
1929+ if len(ret) == 0 {
1930+ panic("no return value specified for ListVolumes")
1931+ }
1932+
1933+ var r0 []dto.Volume
1934+ var r1 error
1935+ if returnFunc, ok := ret.Get(0).(func(context.Context) ([]dto.Volume, error)); ok {
1936+ return returnFunc(ctx)
1937+ }
1938+ if returnFunc, ok := ret.Get(0).(func(context.Context) []dto.Volume); ok {
1939+ r0 = returnFunc(ctx)
1940+ } else {
1941+ if ret.Get(0) != nil {
1942+ r0 = ret.Get(0).([]dto.Volume)
1943+ }
1944+ }
1945+ if returnFunc, ok := ret.Get(1).(func(context.Context) error); ok {
1946+ r1 = returnFunc(ctx)
1947+ } else {
1948+ r1 = ret.Error(1)
1949+ }
1950+ return r0, r1
1951+}
1952+
1953+// MockControlPlane_ListVolumes_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ListVolumes'
1954+type MockControlPlane_ListVolumes_Call struct {
1955+ *mock.Call
1956+}
1957+
1958+// ListVolumes is a helper method to define mock.On call
1959+// - ctx context.Context
1960+func (_e *MockControlPlane_Expecter) ListVolumes(ctx interface{}) *MockControlPlane_ListVolumes_Call {
1961+ return &MockControlPlane_ListVolumes_Call{Call: _e.mock.On("ListVolumes", ctx)}
1962+}
1963+
1964+func (_c *MockControlPlane_ListVolumes_Call) Run(run func(ctx context.Context)) *MockControlPlane_ListVolumes_Call {
1965+ _c.Call.Run(func(args mock.Arguments) {
1966+ var arg0 context.Context
1967+ if args[0] != nil {
1968+ arg0 = args[0].(context.Context)
1969+ }
1970+ run(
1971+ arg0,
1972+ )
1973+ })
1974+ return _c
1975+}
1976+
1977+func (_c *MockControlPlane_ListVolumes_Call) Return(volumes []dto.Volume, err error) *MockControlPlane_ListVolumes_Call {
1978+ _c.Call.Return(volumes, err)
1979+ return _c
1980+}
1981+
1982+func (_c *MockControlPlane_ListVolumes_Call) RunAndReturn(run func(ctx context.Context) ([]dto.Volume, error)) *MockControlPlane_ListVolumes_Call {
1983+ _c.Call.Return(run)
1984+ return _c
1985+}
1986+
1987+// PruneVolumes provides a mock function for the type MockControlPlane
1988+func (_mock *MockControlPlane) PruneVolumes(ctx context.Context, req dto.VolumePruneRequest) (*dto.VolumePruneResponse, error) {
1989+ ret := _mock.Called(ctx, req)
1990+
1991+ if len(ret) == 0 {
1992+ panic("no return value specified for PruneVolumes")
1993+ }
1994+
1995+ var r0 *dto.VolumePruneResponse
1996+ var r1 error
1997+ if returnFunc, ok := ret.Get(0).(func(context.Context, dto.VolumePruneRequest) (*dto.VolumePruneResponse, error)); ok {
1998+ return returnFunc(ctx, req)
1999+ }
2000+ if returnFunc, ok := ret.Get(0).(func(context.Context, dto.VolumePruneRequest) *dto.VolumePruneResponse); ok {
2001+ r0 = returnFunc(ctx, req)
2002+ } else {
2003+ if ret.Get(0) != nil {
2004+ r0 = ret.Get(0).(*dto.VolumePruneResponse)
2005+ }
2006+ }
2007+ if returnFunc, ok := ret.Get(1).(func(context.Context, dto.VolumePruneRequest) error); ok {
2008+ r1 = returnFunc(ctx, req)
2009+ } else {
2010+ r1 = ret.Error(1)
2011+ }
2012+ return r0, r1
2013+}
2014+
2015+// MockControlPlane_PruneVolumes_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'PruneVolumes'
2016+type MockControlPlane_PruneVolumes_Call struct {
2017+ *mock.Call
2018+}
2019+
2020+// PruneVolumes is a helper method to define mock.On call
2021+// - ctx context.Context
2022+// - req dto.VolumePruneRequest
2023+func (_e *MockControlPlane_Expecter) PruneVolumes(ctx interface{}, req interface{}) *MockControlPlane_PruneVolumes_Call {
2024+ return &MockControlPlane_PruneVolumes_Call{Call: _e.mock.On("PruneVolumes", ctx, req)}
2025+}
2026+
2027+func (_c *MockControlPlane_PruneVolumes_Call) Run(run func(ctx context.Context, req dto.VolumePruneRequest)) *MockControlPlane_PruneVolumes_Call {
2028+ _c.Call.Run(func(args mock.Arguments) {
2029+ var arg0 context.Context
2030+ if args[0] != nil {
2031+ arg0 = args[0].(context.Context)
2032+ }
2033+ var arg1 dto.VolumePruneRequest
2034+ if args[1] != nil {
2035+ arg1 = args[1].(dto.VolumePruneRequest)
2036+ }
2037+ run(
2038+ arg0,
2039+ arg1,
2040+ )
2041+ })
2042+ return _c
2043+}
2044+
2045+func (_c *MockControlPlane_PruneVolumes_Call) Return(volumePruneResponse *dto.VolumePruneResponse, err error) *MockControlPlane_PruneVolumes_Call {
2046+ _c.Call.Return(volumePruneResponse, err)
2047+ return _c
2048+}
2049+
2050+func (_c *MockControlPlane_PruneVolumes_Call) RunAndReturn(run func(ctx context.Context, req dto.VolumePruneRequest) (*dto.VolumePruneResponse, error)) *MockControlPlane_PruneVolumes_Call {
2051+ _c.Call.Return(run)
2052+ return _c
2053+}
2054+
2055+// Reload provides a mock function for the type MockControlPlane
2056+func (_mock *MockControlPlane) Reload(ctx context.Context) error {
2057+ ret := _mock.Called(ctx)
2058+
2059+ if len(ret) == 0 {
2060+ panic("no return value specified for Reload")
2061+ }
2062+
2063+ var r0 error
2064+ if returnFunc, ok := ret.Get(0).(func(context.Context) error); ok {
2065+ r0 = returnFunc(ctx)
2066+ } else {
2067+ r0 = ret.Error(0)
2068+ }
2069+ return r0
2070+}
2071+
2072+// MockControlPlane_Reload_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Reload'
2073+type MockControlPlane_Reload_Call struct {
2074+ *mock.Call
2075+}
2076+
2077+// Reload is a helper method to define mock.On call
2078+// - ctx context.Context
2079+func (_e *MockControlPlane_Expecter) Reload(ctx interface{}) *MockControlPlane_Reload_Call {
2080+ return &MockControlPlane_Reload_Call{Call: _e.mock.On("Reload", ctx)}
2081+}
2082+
2083+func (_c *MockControlPlane_Reload_Call) Run(run func(ctx context.Context)) *MockControlPlane_Reload_Call {
2084+ _c.Call.Run(func(args mock.Arguments) {
2085+ var arg0 context.Context
2086+ if args[0] != nil {
2087+ arg0 = args[0].(context.Context)
2088+ }
2089+ run(
2090+ arg0,
2091+ )
2092+ })
2093+ return _c
2094+}
2095+
2096+func (_c *MockControlPlane_Reload_Call) Return(err error) *MockControlPlane_Reload_Call {
2097+ _c.Call.Return(err)
2098+ return _c
2099+}
2100+
2101+func (_c *MockControlPlane_Reload_Call) RunAndReturn(run func(ctx context.Context) error) *MockControlPlane_Reload_Call {
2102+ _c.Call.Return(run)
2103+ return _c
2104+}
2105+
2106+// RemoveAttachment provides a mock function for the type MockControlPlane
2107+func (_mock *MockControlPlane) RemoveAttachment(ctx context.Context, domainOrGroup string, image string) error {
2108+ ret := _mock.Called(ctx, domainOrGroup, image)
2109+
2110+ if len(ret) == 0 {
2111+ panic("no return value specified for RemoveAttachment")
2112+ }
2113+
2114+ var r0 error
2115+ if returnFunc, ok := ret.Get(0).(func(context.Context, string, string) error); ok {
2116+ r0 = returnFunc(ctx, domainOrGroup, image)
2117+ } else {
2118+ r0 = ret.Error(0)
2119+ }
2120+ return r0
2121+}
2122+
2123+// MockControlPlane_RemoveAttachment_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'RemoveAttachment'
2124+type MockControlPlane_RemoveAttachment_Call struct {
2125+ *mock.Call
2126+}
2127+
2128+// RemoveAttachment is a helper method to define mock.On call
2129+// - ctx context.Context
2130+// - domainOrGroup string
2131+// - image string
2132+func (_e *MockControlPlane_Expecter) RemoveAttachment(ctx interface{}, domainOrGroup interface{}, image interface{}) *MockControlPlane_RemoveAttachment_Call {
2133+ return &MockControlPlane_RemoveAttachment_Call{Call: _e.mock.On("RemoveAttachment", ctx, domainOrGroup, image)}
2134+}
2135+
2136+func (_c *MockControlPlane_RemoveAttachment_Call) Run(run func(ctx context.Context, domainOrGroup string, image string)) *MockControlPlane_RemoveAttachment_Call {
2137+ _c.Call.Run(func(args mock.Arguments) {
2138+ var arg0 context.Context
2139+ if args[0] != nil {
2140+ arg0 = args[0].(context.Context)
2141+ }
2142+ var arg1 string
2143+ if args[1] != nil {
2144+ arg1 = args[1].(string)
2145+ }
2146+ var arg2 string
2147+ if args[2] != nil {
2148+ arg2 = args[2].(string)
2149+ }
2150+ run(
2151+ arg0,
2152+ arg1,
2153+ arg2,
2154+ )
2155+ })
2156+ return _c
2157+}
2158+
2159+func (_c *MockControlPlane_RemoveAttachment_Call) Return(err error) *MockControlPlane_RemoveAttachment_Call {
2160+ _c.Call.Return(err)
2161+ return _c
2162+}
2163+
2164+func (_c *MockControlPlane_RemoveAttachment_Call) RunAndReturn(run func(ctx context.Context, domainOrGroup string, image string) error) *MockControlPlane_RemoveAttachment_Call {
2165+ _c.Call.Return(run)
2166+ return _c
2167+}
2168+
2169+// RemoveAutoRouteAllowedDomain provides a mock function for the type MockControlPlane
2170+func (_mock *MockControlPlane) RemoveAutoRouteAllowedDomain(ctx context.Context, pattern string) error {
2171+ ret := _mock.Called(ctx, pattern)
2172+
2173+ if len(ret) == 0 {
2174+ panic("no return value specified for RemoveAutoRouteAllowedDomain")
2175+ }
2176+
2177+ var r0 error
2178+ if returnFunc, ok := ret.Get(0).(func(context.Context, string) error); ok {
2179+ r0 = returnFunc(ctx, pattern)
2180+ } else {
2181+ r0 = ret.Error(0)
2182+ }
2183+ return r0
2184+}
2185+
2186+// MockControlPlane_RemoveAutoRouteAllowedDomain_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'RemoveAutoRouteAllowedDomain'
2187+type MockControlPlane_RemoveAutoRouteAllowedDomain_Call struct {
2188+ *mock.Call
2189+}
2190+
2191+// RemoveAutoRouteAllowedDomain is a helper method to define mock.On call
2192+// - ctx context.Context
2193+// - pattern string
2194+func (_e *MockControlPlane_Expecter) RemoveAutoRouteAllowedDomain(ctx interface{}, pattern interface{}) *MockControlPlane_RemoveAutoRouteAllowedDomain_Call {
2195+ return &MockControlPlane_RemoveAutoRouteAllowedDomain_Call{Call: _e.mock.On("RemoveAutoRouteAllowedDomain", ctx, pattern)}
2196+}
2197+
2198+func (_c *MockControlPlane_RemoveAutoRouteAllowedDomain_Call) Run(run func(ctx context.Context, pattern string)) *MockControlPlane_RemoveAutoRouteAllowedDomain_Call {
2199+ _c.Call.Run(func(args mock.Arguments) {
2200+ var arg0 context.Context
2201+ if args[0] != nil {
2202+ arg0 = args[0].(context.Context)
2203+ }
2204+ var arg1 string
2205+ if args[1] != nil {
2206+ arg1 = args[1].(string)
2207+ }
2208+ run(
2209+ arg0,
2210+ arg1,
2211+ )
2212+ })
2213+ return _c
2214+}
2215+
2216+func (_c *MockControlPlane_RemoveAutoRouteAllowedDomain_Call) Return(err error) *MockControlPlane_RemoveAutoRouteAllowedDomain_Call {
2217+ _c.Call.Return(err)
2218+ return _c
2219+}
2220+
2221+func (_c *MockControlPlane_RemoveAutoRouteAllowedDomain_Call) RunAndReturn(run func(ctx context.Context, pattern string) error) *MockControlPlane_RemoveAutoRouteAllowedDomain_Call {
2222+ _c.Call.Return(run)
2223+ return _c
2224+}
2225+
2226+// RemoveRoute provides a mock function for the type MockControlPlane
2227+func (_mock *MockControlPlane) RemoveRoute(ctx context.Context, routeDomain string) error {
2228+ ret := _mock.Called(ctx, routeDomain)
2229+
2230+ if len(ret) == 0 {
2231+ panic("no return value specified for RemoveRoute")
2232+ }
2233+
2234+ var r0 error
2235+ if returnFunc, ok := ret.Get(0).(func(context.Context, string) error); ok {
2236+ r0 = returnFunc(ctx, routeDomain)
2237+ } else {
2238+ r0 = ret.Error(0)
2239+ }
2240+ return r0
2241+}
2242+
2243+// MockControlPlane_RemoveRoute_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'RemoveRoute'
2244+type MockControlPlane_RemoveRoute_Call struct {
2245+ *mock.Call
2246+}
2247+
2248+// RemoveRoute is a helper method to define mock.On call
2249+// - ctx context.Context
2250+// - routeDomain string
2251+func (_e *MockControlPlane_Expecter) RemoveRoute(ctx interface{}, routeDomain interface{}) *MockControlPlane_RemoveRoute_Call {
2252+ return &MockControlPlane_RemoveRoute_Call{Call: _e.mock.On("RemoveRoute", ctx, routeDomain)}
2253+}
2254+
2255+func (_c *MockControlPlane_RemoveRoute_Call) Run(run func(ctx context.Context, routeDomain string)) *MockControlPlane_RemoveRoute_Call {
2256+ _c.Call.Run(func(args mock.Arguments) {
2257+ var arg0 context.Context
2258+ if args[0] != nil {
2259+ arg0 = args[0].(context.Context)
2260+ }
2261+ var arg1 string
2262+ if args[1] != nil {
2263+ arg1 = args[1].(string)
2264+ }
2265+ run(
2266+ arg0,
2267+ arg1,
2268+ )
2269+ })
2270+ return _c
2271+}
2272+
2273+func (_c *MockControlPlane_RemoveRoute_Call) Return(err error) *MockControlPlane_RemoveRoute_Call {
2274+ _c.Call.Return(err)
2275+ return _c
2276+}
2277+
2278+func (_c *MockControlPlane_RemoveRoute_Call) RunAndReturn(run func(ctx context.Context, routeDomain string) error) *MockControlPlane_RemoveRoute_Call {
2279+ _c.Call.Return(run)
2280+ return _c
2281+}
2282+
2283+// Restart provides a mock function for the type MockControlPlane
2284+func (_mock *MockControlPlane) Restart(ctx context.Context, restartDomain string, withAttachments bool) (*remote.RestartResult, error) {
2285+ ret := _mock.Called(ctx, restartDomain, withAttachments)
2286+
2287+ if len(ret) == 0 {
2288+ panic("no return value specified for Restart")
2289+ }
2290+
2291+ var r0 *remote.RestartResult
2292+ var r1 error
2293+ if returnFunc, ok := ret.Get(0).(func(context.Context, string, bool) (*remote.RestartResult, error)); ok {
2294+ return returnFunc(ctx, restartDomain, withAttachments)
2295+ }
2296+ if returnFunc, ok := ret.Get(0).(func(context.Context, string, bool) *remote.RestartResult); ok {
2297+ r0 = returnFunc(ctx, restartDomain, withAttachments)
2298+ } else {
2299+ if ret.Get(0) != nil {
2300+ r0 = ret.Get(0).(*remote.RestartResult)
2301+ }
2302+ }
2303+ if returnFunc, ok := ret.Get(1).(func(context.Context, string, bool) error); ok {
2304+ r1 = returnFunc(ctx, restartDomain, withAttachments)
2305+ } else {
2306+ r1 = ret.Error(1)
2307+ }
2308+ return r0, r1
2309+}
2310+
2311+// MockControlPlane_Restart_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Restart'
2312+type MockControlPlane_Restart_Call struct {
2313+ *mock.Call
2314+}
2315+
2316+// Restart is a helper method to define mock.On call
2317+// - ctx context.Context
2318+// - restartDomain string
2319+// - withAttachments bool
2320+func (_e *MockControlPlane_Expecter) Restart(ctx interface{}, restartDomain interface{}, withAttachments interface{}) *MockControlPlane_Restart_Call {
2321+ return &MockControlPlane_Restart_Call{Call: _e.mock.On("Restart", ctx, restartDomain, withAttachments)}
2322+}
2323+
2324+func (_c *MockControlPlane_Restart_Call) Run(run func(ctx context.Context, restartDomain string, withAttachments bool)) *MockControlPlane_Restart_Call {
2325+ _c.Call.Run(func(args mock.Arguments) {
2326+ var arg0 context.Context
2327+ if args[0] != nil {
2328+ arg0 = args[0].(context.Context)
2329+ }
2330+ var arg1 string
2331+ if args[1] != nil {
2332+ arg1 = args[1].(string)
2333+ }
2334+ var arg2 bool
2335+ if args[2] != nil {
2336+ arg2 = args[2].(bool)
2337+ }
2338+ run(
2339+ arg0,
2340+ arg1,
2341+ arg2,
2342+ )
2343+ })
2344+ return _c
2345+}
2346+
2347+func (_c *MockControlPlane_Restart_Call) Return(restartResult *remote.RestartResult, err error) *MockControlPlane_Restart_Call {
2348+ _c.Call.Return(restartResult, err)
2349+ return _c
2350+}
2351+
2352+func (_c *MockControlPlane_Restart_Call) RunAndReturn(run func(ctx context.Context, restartDomain string, withAttachments bool) (*remote.RestartResult, error)) *MockControlPlane_Restart_Call {
2353+ _c.Call.Return(run)
2354+ return _c
2355+}
2356+
2357+// RunBackup provides a mock function for the type MockControlPlane
2358+func (_mock *MockControlPlane) RunBackup(ctx context.Context, backupDomain string, dbName string) (*dto.BackupRunResponse, error) {
2359+ ret := _mock.Called(ctx, backupDomain, dbName)
2360+
2361+ if len(ret) == 0 {
2362+ panic("no return value specified for RunBackup")
2363+ }
2364+
2365+ var r0 *dto.BackupRunResponse
2366+ var r1 error
2367+ if returnFunc, ok := ret.Get(0).(func(context.Context, string, string) (*dto.BackupRunResponse, error)); ok {
2368+ return returnFunc(ctx, backupDomain, dbName)
2369+ }
2370+ if returnFunc, ok := ret.Get(0).(func(context.Context, string, string) *dto.BackupRunResponse); ok {
2371+ r0 = returnFunc(ctx, backupDomain, dbName)
2372+ } else {
2373+ if ret.Get(0) != nil {
2374+ r0 = ret.Get(0).(*dto.BackupRunResponse)
2375+ }
2376+ }
2377+ if returnFunc, ok := ret.Get(1).(func(context.Context, string, string) error); ok {
2378+ r1 = returnFunc(ctx, backupDomain, dbName)
2379+ } else {
2380+ r1 = ret.Error(1)
2381+ }
2382+ return r0, r1
2383+}
2384+
2385+// MockControlPlane_RunBackup_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'RunBackup'
2386+type MockControlPlane_RunBackup_Call struct {
2387+ *mock.Call
2388+}
2389+
2390+// RunBackup is a helper method to define mock.On call
2391+// - ctx context.Context
2392+// - backupDomain string
2393+// - dbName string
2394+func (_e *MockControlPlane_Expecter) RunBackup(ctx interface{}, backupDomain interface{}, dbName interface{}) *MockControlPlane_RunBackup_Call {
2395+ return &MockControlPlane_RunBackup_Call{Call: _e.mock.On("RunBackup", ctx, backupDomain, dbName)}
2396+}
2397+
2398+func (_c *MockControlPlane_RunBackup_Call) Run(run func(ctx context.Context, backupDomain string, dbName string)) *MockControlPlane_RunBackup_Call {
2399+ _c.Call.Run(func(args mock.Arguments) {
2400+ var arg0 context.Context
2401+ if args[0] != nil {
2402+ arg0 = args[0].(context.Context)
2403+ }
2404+ var arg1 string
2405+ if args[1] != nil {
2406+ arg1 = args[1].(string)
2407+ }
2408+ var arg2 string
2409+ if args[2] != nil {
2410+ arg2 = args[2].(string)
2411+ }
2412+ run(
2413+ arg0,
2414+ arg1,
2415+ arg2,
2416+ )
2417+ })
2418+ return _c
2419+}
2420+
2421+func (_c *MockControlPlane_RunBackup_Call) Return(backupRunResponse *dto.BackupRunResponse, err error) *MockControlPlane_RunBackup_Call {
2422+ _c.Call.Return(backupRunResponse, err)
2423+ return _c
2424+}
2425+
2426+func (_c *MockControlPlane_RunBackup_Call) RunAndReturn(run func(ctx context.Context, backupDomain string, dbName string) (*dto.BackupRunResponse, error)) *MockControlPlane_RunBackup_Call {
2427+ _c.Call.Return(run)
2428+ return _c
2429+}
2430+
2431+// SetAttachmentSecrets provides a mock function for the type MockControlPlane
2432+func (_mock *MockControlPlane) SetAttachmentSecrets(ctx context.Context, domain1 string, service string, secrets map[string]string) error {
2433+ ret := _mock.Called(ctx, domain1, service, secrets)
2434+
2435+ if len(ret) == 0 {
2436+ panic("no return value specified for SetAttachmentSecrets")
2437+ }
2438+
2439+ var r0 error
2440+ if returnFunc, ok := ret.Get(0).(func(context.Context, string, string, map[string]string) error); ok {
2441+ r0 = returnFunc(ctx, domain1, service, secrets)
2442+ } else {
2443+ r0 = ret.Error(0)
2444+ }
2445+ return r0
2446+}
2447+
2448+// MockControlPlane_SetAttachmentSecrets_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SetAttachmentSecrets'
2449+type MockControlPlane_SetAttachmentSecrets_Call struct {
2450+ *mock.Call
2451+}
2452+
2453+// SetAttachmentSecrets is a helper method to define mock.On call
2454+// - ctx context.Context
2455+// - domain1 string
2456+// - service string
2457+// - secrets map[string]string
2458+func (_e *MockControlPlane_Expecter) SetAttachmentSecrets(ctx interface{}, domain1 interface{}, service interface{}, secrets interface{}) *MockControlPlane_SetAttachmentSecrets_Call {
2459+ return &MockControlPlane_SetAttachmentSecrets_Call{Call: _e.mock.On("SetAttachmentSecrets", ctx, domain1, service, secrets)}
2460+}
2461+
2462+func (_c *MockControlPlane_SetAttachmentSecrets_Call) Run(run func(ctx context.Context, domain1 string, service string, secrets map[string]string)) *MockControlPlane_SetAttachmentSecrets_Call {
2463+ _c.Call.Run(func(args mock.Arguments) {
2464+ var arg0 context.Context
2465+ if args[0] != nil {
2466+ arg0 = args[0].(context.Context)
2467+ }
2468+ var arg1 string
2469+ if args[1] != nil {
2470+ arg1 = args[1].(string)
2471+ }
2472+ var arg2 string
2473+ if args[2] != nil {
2474+ arg2 = args[2].(string)
2475+ }
2476+ var arg3 map[string]string
2477+ if args[3] != nil {
2478+ arg3 = args[3].(map[string]string)
2479+ }
2480+ run(
2481+ arg0,
2482+ arg1,
2483+ arg2,
2484+ arg3,
2485+ )
2486+ })
2487+ return _c
2488+}
2489+
2490+func (_c *MockControlPlane_SetAttachmentSecrets_Call) Return(err error) *MockControlPlane_SetAttachmentSecrets_Call {
2491+ _c.Call.Return(err)
2492+ return _c
2493+}
2494+
2495+func (_c *MockControlPlane_SetAttachmentSecrets_Call) RunAndReturn(run func(ctx context.Context, domain1 string, service string, secrets map[string]string) error) *MockControlPlane_SetAttachmentSecrets_Call {
2496+ _c.Call.Return(run)
2497+ return _c
2498+}
2499+
2500+// SetSecrets provides a mock function for the type MockControlPlane
2501+func (_mock *MockControlPlane) SetSecrets(ctx context.Context, secretDomain string, secrets map[string]string) error {
2502+ ret := _mock.Called(ctx, secretDomain, secrets)
2503+
2504+ if len(ret) == 0 {
2505+ panic("no return value specified for SetSecrets")
2506+ }
2507+
2508+ var r0 error
2509+ if returnFunc, ok := ret.Get(0).(func(context.Context, string, map[string]string) error); ok {
2510+ r0 = returnFunc(ctx, secretDomain, secrets)
2511+ } else {
2512+ r0 = ret.Error(0)
2513+ }
2514+ return r0
2515+}
2516+
2517+// MockControlPlane_SetSecrets_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SetSecrets'
2518+type MockControlPlane_SetSecrets_Call struct {
2519+ *mock.Call
2520+}
2521+
2522+// SetSecrets is a helper method to define mock.On call
2523+// - ctx context.Context
2524+// - secretDomain string
2525+// - secrets map[string]string
2526+func (_e *MockControlPlane_Expecter) SetSecrets(ctx interface{}, secretDomain interface{}, secrets interface{}) *MockControlPlane_SetSecrets_Call {
2527+ return &MockControlPlane_SetSecrets_Call{Call: _e.mock.On("SetSecrets", ctx, secretDomain, secrets)}
2528+}
2529+
2530+func (_c *MockControlPlane_SetSecrets_Call) Run(run func(ctx context.Context, secretDomain string, secrets map[string]string)) *MockControlPlane_SetSecrets_Call {
2531+ _c.Call.Run(func(args mock.Arguments) {
2532+ var arg0 context.Context
2533+ if args[0] != nil {
2534+ arg0 = args[0].(context.Context)
2535+ }
2536+ var arg1 string
2537+ if args[1] != nil {
2538+ arg1 = args[1].(string)
2539+ }
2540+ var arg2 map[string]string
2541+ if args[2] != nil {
2542+ arg2 = args[2].(map[string]string)
2543+ }
2544+ run(
2545+ arg0,
2546+ arg1,
2547+ arg2,
2548+ )
2549+ })
2550+ return _c
2551+}
2552+
2553+func (_c *MockControlPlane_SetSecrets_Call) Return(err error) *MockControlPlane_SetSecrets_Call {
2554+ _c.Call.Return(err)
2555+ return _c
2556+}
2557+
2558+func (_c *MockControlPlane_SetSecrets_Call) RunAndReturn(run func(ctx context.Context, secretDomain string, secrets map[string]string) error) *MockControlPlane_SetSecrets_Call {
2559+ _c.Call.Return(run)
2560+ return _c
2561+}
2562+
2563+// StreamContainerLogs provides a mock function for the type MockControlPlane
2564+func (_mock *MockControlPlane) StreamContainerLogs(ctx context.Context, logDomain string, lines int) (<-chan string, error) {
2565+ ret := _mock.Called(ctx, logDomain, lines)
2566+
2567+ if len(ret) == 0 {
2568+ panic("no return value specified for StreamContainerLogs")
2569+ }
2570+
2571+ var r0 <-chan string
2572+ var r1 error
2573+ if returnFunc, ok := ret.Get(0).(func(context.Context, string, int) (<-chan string, error)); ok {
2574+ return returnFunc(ctx, logDomain, lines)
2575+ }
2576+ if returnFunc, ok := ret.Get(0).(func(context.Context, string, int) <-chan string); ok {
2577+ r0 = returnFunc(ctx, logDomain, lines)
2578+ } else {
2579+ if ret.Get(0) != nil {
2580+ r0 = ret.Get(0).(<-chan string)
2581+ }
2582+ }
2583+ if returnFunc, ok := ret.Get(1).(func(context.Context, string, int) error); ok {
2584+ r1 = returnFunc(ctx, logDomain, lines)
2585+ } else {
2586+ r1 = ret.Error(1)
2587+ }
2588+ return r0, r1
2589+}
2590+
2591+// MockControlPlane_StreamContainerLogs_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'StreamContainerLogs'
2592+type MockControlPlane_StreamContainerLogs_Call struct {
2593+ *mock.Call
2594+}
2595+
2596+// StreamContainerLogs is a helper method to define mock.On call
2597+// - ctx context.Context
2598+// - logDomain string
2599+// - lines int
2600+func (_e *MockControlPlane_Expecter) StreamContainerLogs(ctx interface{}, logDomain interface{}, lines interface{}) *MockControlPlane_StreamContainerLogs_Call {
2601+ return &MockControlPlane_StreamContainerLogs_Call{Call: _e.mock.On("StreamContainerLogs", ctx, logDomain, lines)}
2602+}
2603+
2604+func (_c *MockControlPlane_StreamContainerLogs_Call) Run(run func(ctx context.Context, logDomain string, lines int)) *MockControlPlane_StreamContainerLogs_Call {
2605+ _c.Call.Run(func(args mock.Arguments) {
2606+ var arg0 context.Context
2607+ if args[0] != nil {
2608+ arg0 = args[0].(context.Context)
2609+ }
2610+ var arg1 string
2611+ if args[1] != nil {
2612+ arg1 = args[1].(string)
2613+ }
2614+ var arg2 int
2615+ if args[2] != nil {
2616+ arg2 = args[2].(int)
2617+ }
2618+ run(
2619+ arg0,
2620+ arg1,
2621+ arg2,
2622+ )
2623+ })
2624+ return _c
2625+}
2626+
2627+func (_c *MockControlPlane_StreamContainerLogs_Call) Return(stringCh <-chan string, err error) *MockControlPlane_StreamContainerLogs_Call {
2628+ _c.Call.Return(stringCh, err)
2629+ return _c
2630+}
2631+
2632+func (_c *MockControlPlane_StreamContainerLogs_Call) RunAndReturn(run func(ctx context.Context, logDomain string, lines int) (<-chan string, error)) *MockControlPlane_StreamContainerLogs_Call {
2633+ _c.Call.Return(run)
2634+ return _c
2635+}
2636+
2637+// StreamProcessLogs provides a mock function for the type MockControlPlane
2638+func (_mock *MockControlPlane) StreamProcessLogs(ctx context.Context, lines int) (<-chan string, error) {
2639+ ret := _mock.Called(ctx, lines)
2640+
2641+ if len(ret) == 0 {
2642+ panic("no return value specified for StreamProcessLogs")
2643+ }
2644+
2645+ var r0 <-chan string
2646+ var r1 error
2647+ if returnFunc, ok := ret.Get(0).(func(context.Context, int) (<-chan string, error)); ok {
2648+ return returnFunc(ctx, lines)
2649+ }
2650+ if returnFunc, ok := ret.Get(0).(func(context.Context, int) <-chan string); ok {
2651+ r0 = returnFunc(ctx, lines)
2652+ } else {
2653+ if ret.Get(0) != nil {
2654+ r0 = ret.Get(0).(<-chan string)
2655+ }
2656+ }
2657+ if returnFunc, ok := ret.Get(1).(func(context.Context, int) error); ok {
2658+ r1 = returnFunc(ctx, lines)
2659+ } else {
2660+ r1 = ret.Error(1)
2661+ }
2662+ return r0, r1
2663+}
2664+
2665+// MockControlPlane_StreamProcessLogs_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'StreamProcessLogs'
2666+type MockControlPlane_StreamProcessLogs_Call struct {
2667+ *mock.Call
2668+}
2669+
2670+// StreamProcessLogs is a helper method to define mock.On call
2671+// - ctx context.Context
2672+// - lines int
2673+func (_e *MockControlPlane_Expecter) StreamProcessLogs(ctx interface{}, lines interface{}) *MockControlPlane_StreamProcessLogs_Call {
2674+ return &MockControlPlane_StreamProcessLogs_Call{Call: _e.mock.On("StreamProcessLogs", ctx, lines)}
2675+}
2676+
2677+func (_c *MockControlPlane_StreamProcessLogs_Call) Run(run func(ctx context.Context, lines int)) *MockControlPlane_StreamProcessLogs_Call {
2678+ _c.Call.Run(func(args mock.Arguments) {
2679+ var arg0 context.Context
2680+ if args[0] != nil {
2681+ arg0 = args[0].(context.Context)
2682+ }
2683+ var arg1 int
2684+ if args[1] != nil {
2685+ arg1 = args[1].(int)
2686+ }
2687+ run(
2688+ arg0,
2689+ arg1,
2690+ )
2691+ })
2692+ return _c
2693+}
2694+
2695+func (_c *MockControlPlane_StreamProcessLogs_Call) Return(stringCh <-chan string, err error) *MockControlPlane_StreamProcessLogs_Call {
2696+ _c.Call.Return(stringCh, err)
2697+ return _c
2698+}
2699+
2700+func (_c *MockControlPlane_StreamProcessLogs_Call) RunAndReturn(run func(ctx context.Context, lines int) (<-chan string, error)) *MockControlPlane_StreamProcessLogs_Call {
2701+ _c.Call.Return(run)
2702+ return _c
2703+}
2704+
2705+// UpdateRoute provides a mock function for the type MockControlPlane
2706+func (_mock *MockControlPlane) UpdateRoute(ctx context.Context, route domain.Route) error {
2707+ ret := _mock.Called(ctx, route)
2708+
2709+ if len(ret) == 0 {
2710+ panic("no return value specified for UpdateRoute")
2711+ }
2712+
2713+ var r0 error
2714+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.Route) error); ok {
2715+ r0 = returnFunc(ctx, route)
2716+ } else {
2717+ r0 = ret.Error(0)
2718+ }
2719+ return r0
2720+}
2721+
2722+// MockControlPlane_UpdateRoute_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UpdateRoute'
2723+type MockControlPlane_UpdateRoute_Call struct {
2724+ *mock.Call
2725+}
2726+
2727+// UpdateRoute is a helper method to define mock.On call
2728+// - ctx context.Context
2729+// - route domain.Route
2730+func (_e *MockControlPlane_Expecter) UpdateRoute(ctx interface{}, route interface{}) *MockControlPlane_UpdateRoute_Call {
2731+ return &MockControlPlane_UpdateRoute_Call{Call: _e.mock.On("UpdateRoute", ctx, route)}
2732+}
2733+
2734+func (_c *MockControlPlane_UpdateRoute_Call) Run(run func(ctx context.Context, route domain.Route)) *MockControlPlane_UpdateRoute_Call {
2735+ _c.Call.Run(func(args mock.Arguments) {
2736+ var arg0 context.Context
2737+ if args[0] != nil {
2738+ arg0 = args[0].(context.Context)
2739+ }
2740+ var arg1 domain.Route
2741+ if args[1] != nil {
2742+ arg1 = args[1].(domain.Route)
2743+ }
2744+ run(
2745+ arg0,
2746+ arg1,
2747+ )
2748+ })
2749+ return _c
2750+}
2751+
2752+func (_c *MockControlPlane_UpdateRoute_Call) Return(err error) *MockControlPlane_UpdateRoute_Call {
2753+ _c.Call.Return(err)
2754+ return _c
2755+}
2756+
2757+func (_c *MockControlPlane_UpdateRoute_Call) RunAndReturn(run func(ctx context.Context, route domain.Route) error) *MockControlPlane_UpdateRoute_Call {
2758+ _c.Call.Return(run)
2759+ return _c
2760+}
M internal/adapters/in/cli/push_test.go +8 -0
@@ -90,6 +90,14 @@ func (c *resolveFromImageTestControlPlane) GetAttachmentsConfig(context.Context,
9090 panic("unexpected call")
9191 }
9292
93+func (c *resolveFromImageTestControlPlane) ListOrphanedAttachments(context.Context) ([]domain.CleanupAttachment, error) {
94+ panic("unexpected call")
95+}
96+
97+func (c *resolveFromImageTestControlPlane) CleanupOrphanedAttachments(context.Context, string, bool) (*domain.CleanupReport, error) {
98+ panic("unexpected call")
99+}
100+
93101 func (c *resolveFromImageTestControlPlane) FindAttachmentTargetsByImage(context.Context, string) ([]string, error) {
94102 panic("unexpected call")
95103 }
M internal/adapters/in/cli/remote/client.go +51 -2
@@ -548,11 +548,21 @@ func (c *Client) UpdateRoute(ctx context.Context, route domain.Route) error {
548548
549549 // RemoveRoute removes a route by domain.
550550 func (c *Client) RemoveRoute(ctx context.Context, routeDomain string) error {
551+ _, err := c.RemoveRouteWithCleanup(ctx, routeDomain)
552+ return err
553+}
554+
555+// RemoveRouteWithCleanup removes a route and returns the server cleanup report.
556+func (c *Client) RemoveRouteWithCleanup(ctx context.Context, routeDomain string) (*dto.RouteDeleteResponse, error) {
551557 resp, err := c.request(ctx, http.MethodDelete, "/routes/"+url.PathEscape(routeDomain), nil)
552558 if err != nil {
553- return err
559+ return nil, fmt.Errorf("delete route %s failed: %w", routeDomain, err)
554560 }
555- return parseResponse(resp, nil)
561+ var result dto.RouteDeleteResponse
562+ if err := parseResponse(resp, &result); err != nil {
563+ return nil, fmt.Errorf("parse delete route %s response: %w", routeDomain, err)
564+ }
565+ return &result, nil
556566 }
557567
558568 func (c *Client) Bootstrap(ctx context.Context, req dto.BootstrapRequest) (*dto.BootstrapResponse, error) {
@@ -1025,6 +1035,45 @@ func (c *Client) StreamContainerLogs(ctx context.Context, logDomain string, line
10251035
10261036 // Attachments Config API
10271037
1038+// ListOrphanedAttachments returns running attachment containers no longer configured.
1039+func (c *Client) ListOrphanedAttachments(ctx context.Context) ([]domain.CleanupAttachment, error) {
1040+ resp, err := c.request(ctx, http.MethodGet, "/attachments/orphans", nil)
1041+ if err != nil {
1042+ return nil, err
1043+ }
1044+ var result struct {
1045+ Attachments []domain.CleanupAttachment `json:"attachments"`
1046+ }
1047+ if err := parseResponse(resp, &result); err != nil {
1048+ return nil, err
1049+ }
1050+ return result.Attachments, nil
1051+}
1052+
1053+// CleanupOrphanedAttachments optionally stops/removes orphaned attachment containers.
1054+func (c *Client) CleanupOrphanedAttachments(ctx context.Context, owner string, stop bool) (*domain.CleanupReport, error) {
1055+ path := "/attachments/prune"
1056+ params := url.Values{}
1057+ if stop {
1058+ params.Set("stop", "true")
1059+ }
1060+ if owner != "" {
1061+ params.Set("owner", owner)
1062+ }
1063+ if encoded := params.Encode(); encoded != "" {
1064+ path += "?" + encoded
1065+ }
1066+ resp, err := c.request(ctx, http.MethodPost, path, nil)
1067+ if err != nil {
1068+ return nil, fmt.Errorf("cleanup orphaned attachments failed: %w", err)
1069+ }
1070+ var result domain.CleanupReport
1071+ if err := parseResponse(resp, &result); err != nil {
1072+ return nil, fmt.Errorf("parse orphaned attachment cleanup response: %w", err)
1073+ }
1074+ return &result, nil
1075+}
1076+
10281077 // GetAllAttachmentsConfig returns all configured attachments.
10291078 func (c *Client) GetAllAttachmentsConfig(ctx context.Context) (map[string][]string, error) {
10301079 resp, err := c.request(ctx, http.MethodGet, "/attachments", nil)
M internal/adapters/in/cli/routes.go +408 -4
@@ -13,6 +13,7 @@ import (
1313
1414 "github.com/spf13/cobra"
1515
16+ "github.com/bnema/gordon/internal/adapters/dto"
1617 "github.com/bnema/gordon/internal/adapters/in/cli/remote"
1718 "github.com/bnema/gordon/internal/adapters/in/cli/ui/components"
1819 "github.com/bnema/gordon/internal/adapters/in/cli/ui/styles"
@@ -127,6 +128,18 @@ type routeStatusSection struct {
127128 Routes []routeStatusItem `json:"routes,omitempty"`
128129 }
129130
131+type routeDiagnosis struct {
132+ Domain string `json:"domain"`
133+ Configured bool `json:"configured"`
134+ Route *domain.Route `json:"route,omitempty"`
135+ Runtime *remote.RouteInfo `json:"runtime,omitempty"`
136+ Health *remote.RouteHealth `json:"health,omitempty"`
137+ Volumes []dto.Volume `json:"volumes,omitempty"`
138+ OrphanedAttachments []domain.CleanupAttachment `json:"orphaned_attachments,omitempty"`
139+ Warnings []string `json:"warnings,omitempty"`
140+ Hints []string `json:"hints,omitempty"`
141+}
142+
130143 type routesListDeps struct {
131144 explicitRemote func() (*remote.ResolvedRemote, bool, error)
132145 loadLocal func(context.Context, string) (routeListSection, error)
@@ -194,6 +207,8 @@ the local Gordon configuration.`,
194207 cmd.AddCommand(newRoutesStatusCmd())
195208
196209 cmd.AddCommand(newRoutesShowCmd())
210+ cmd.AddCommand(newRoutesDiagnoseCmd())
211+ cmd.AddCommand(newRoutesPurgeCmd())
197212 cmd.AddCommand(newRoutesAddCmd())
198213 cmd.AddCommand(newRoutesRemoveCmd())
199214
@@ -1078,7 +1093,287 @@ func writeRouteShowText(out io.Writer, domainName, image, containerStatus string
10781093 return nil
10791094 }
10801095
1081-// newRoutesAddCmd creates the routes add command.
1096+// newRoutesDiagnoseCmd creates the routes diagnose command.
1097+func newRoutesDiagnoseCmd() *cobra.Command {
1098+ var jsonOut bool
1099+ cmd := &cobra.Command{
1100+ Use: "diagnose <domain>",
1101+ Short: "Diagnose route configuration, runtime state, and cleanup leftovers",
1102+ Args: cobra.ExactArgs(1),
1103+ RunE: func(cmd *cobra.Command, args []string) error {
1104+ return runRouteDiagnosis(cmd.Context(), args[0], cmd.OutOrStdout(), jsonOut)
1105+ },
1106+ }
1107+ cmd.Flags().BoolVar(&jsonOut, "json", false, "Output as JSON")
1108+ return cmd
1109+}
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+
1127+func buildRouteDiagnosis(ctx context.Context, cp ControlPlane, domainName string) (*routeDiagnosis, error) {
1128+ diag := &routeDiagnosis{Domain: domainName}
1129+ loadRouteDiagnosisConfig(ctx, cp, domainName, diag)
1130+ loadRouteDiagnosisRuntime(ctx, cp, domainName, diag)
1131+ loadRouteDiagnosisHealth(ctx, cp, domainName, diag)
1132+ loadRouteDiagnosisVolumes(ctx, cp, domainName, diag)
1133+ loadRouteDiagnosisOrphanedAttachments(ctx, cp, domainName, diag)
1134+ finalizeRouteDiagnosis(diag)
1135+ return diag, nil
1136+}
1137+
1138+func loadRouteDiagnosisConfig(ctx context.Context, cp ControlPlane, domainName string, diag *routeDiagnosis) {
1139+ route, err := cp.GetRoute(ctx, domainName)
1140+ if err == nil && route != nil {
1141+ diag.Configured = true
1142+ diag.Route = route
1143+ return
1144+ }
1145+ if err != nil && !errors.Is(err, domain.ErrRouteNotFound) {
1146+ diag.Warnings = append(diag.Warnings, "failed to load route config: "+err.Error())
1147+ }
1148+}
1149+
1150+func loadRouteDiagnosisRuntime(ctx context.Context, cp ControlPlane, domainName string, diag *routeDiagnosis) {
1151+ routes, err := cp.ListRoutesWithDetails(ctx)
1152+ if err != nil {
1153+ diag.Warnings = append(diag.Warnings, "failed to load route runtime details: "+err.Error())
1154+ return
1155+ }
1156+ for _, info := range routes {
1157+ if strings.EqualFold(info.Domain, domainName) {
1158+ copy := info
1159+ diag.Runtime = &copy
1160+ return
1161+ }
1162+ }
1163+}
1164+
1165+func loadRouteDiagnosisHealth(ctx context.Context, cp ControlPlane, domainName string, diag *routeDiagnosis) {
1166+ if health, err := cp.GetHealth(ctx); err == nil && health != nil {
1167+ diag.Health = health[domainName]
1168+ }
1169+}
1170+
1171+func loadRouteDiagnosisVolumes(ctx context.Context, cp ControlPlane, domainName string, diag *routeDiagnosis) {
1172+ if volumes, err := cp.ListVolumes(ctx); err == nil {
1173+ diag.Volumes = volumesForRouteDiagnosis(volumes, domainName)
1174+ }
1175+}
1176+
1177+func loadRouteDiagnosisOrphanedAttachments(ctx context.Context, cp ControlPlane, domainName string, diag *routeDiagnosis) {
1178+ lister, ok := cp.(interface {
1179+ ListOrphanedAttachments(context.Context) ([]domain.CleanupAttachment, error)
1180+ })
1181+ if !ok {
1182+ return
1183+ }
1184+ if attachments, err := lister.ListOrphanedAttachments(ctx); err == nil {
1185+ diag.OrphanedAttachments = attachmentsForRouteDiagnosis(attachments, domainName)
1186+ }
1187+}
1188+
1189+func finalizeRouteDiagnosis(diag *routeDiagnosis) {
1190+ if !diag.Configured && diag.Runtime != nil {
1191+ diag.Warnings = append(diag.Warnings, "route is not configured but runtime state still exists")
1192+ }
1193+ if len(diag.Volumes) > 0 {
1194+ diag.Hints = append(diag.Hints, "persistent volumes are preserved by default")
1195+ }
1196+ if len(diag.OrphanedAttachments) > 0 {
1197+ diag.Hints = append(diag.Hints, "run 'gordon attachments prune --stop' to stop orphaned attachment containers while preserving volumes")
1198+ }
1199+}
1200+
1201+func volumesForRouteDiagnosis(volumes []dto.Volume, domainName string) []dto.Volume {
1202+ needle := strings.ReplaceAll(strings.ToLower(domainName), ".", "-")
1203+ var out []dto.Volume
1204+ for _, volume := range volumes {
1205+ if strings.Contains(strings.ToLower(volume.Name), needle) {
1206+ out = append(out, volume)
1207+ }
1208+ }
1209+ return out
1210+}
1211+
1212+func attachmentsForRouteDiagnosis(attachments []domain.CleanupAttachment, domainName string) []domain.CleanupAttachment {
1213+ var out []domain.CleanupAttachment
1214+ for _, attachment := range attachments {
1215+ if strings.EqualFold(attachment.Owner, domainName) {
1216+ out = append(out, attachment)
1217+ }
1218+ }
1219+ return out
1220+}
1221+
1222+func writeRouteDiagnosisText(out io.Writer, diag *routeDiagnosis) error {
1223+ if err := cliWriteLine(out, cliRenderTitle("Route diagnosis: "+diag.Domain)); err != nil {
1224+ return err
1225+ }
1226+ if err := writeRouteDiagnosisSummary(out, diag); err != nil {
1227+ return err
1228+ }
1229+ if err := writeRouteDiagnosisLeftovers(out, diag); err != nil {
1230+ return err
1231+ }
1232+ return writeCleanupMessages(out, diag.Warnings, diag.Hints)
1233+}
1234+
1235+func writeRouteDiagnosisSummary(out io.Writer, diag *routeDiagnosis) error {
1236+ if diag.Configured && diag.Route != nil {
1237+ if err := cliWriteLine(out, cliRenderMeta("Image:", diag.Route.Image)); err != nil {
1238+ return err
1239+ }
1240+ } else if err := cliWriteLine(out, cliRenderWarning("Route is not configured")); err != nil {
1241+ return err
1242+ }
1243+ if diag.Runtime == nil {
1244+ return nil
1245+ }
1246+ return cliWriteLine(out, cliRenderMeta("Container:", diag.Runtime.ContainerStatus+" "+shortContainerID(diag.Runtime.ContainerID)))
1247+}
1248+
1249+func writeRouteDiagnosisLeftovers(out io.Writer, diag *routeDiagnosis) error {
1250+ for _, volume := range diag.Volumes {
1251+ if err := cliWriteLine(out, cliRenderMeta("Preserved volume:", volume.Name)); err != nil {
1252+ return err
1253+ }
1254+ }
1255+ for _, attachment := range diag.OrphanedAttachments {
1256+ if err := cliWriteLine(out, cliRenderWarning("Orphaned attachment: "+attachment.Name)); err != nil {
1257+ return err
1258+ }
1259+ }
1260+ return nil
1261+}
1262+
1263+func newRoutesPurgeCmd() *cobra.Command {
1264+ var jsonOut bool
1265+ var force bool
1266+ var includeAttachments bool
1267+ var includeVolumes bool
1268+ cmd := &cobra.Command{
1269+ Use: "purge <domain>",
1270+ Short: "Review or explicitly purge retained route resources",
1271+ Args: cobra.ExactArgs(1),
1272+ RunE: func(cmd *cobra.Command, args []string) error {
1273+ return runRoutePurgeCommand(cmd.Context(), args[0], cmd.OutOrStdout(), routePurgeOptions{Force: force, Attachments: includeAttachments, Volumes: includeVolumes}, jsonOut)
1274+ },
1275+ }
1276+ cmd.Flags().BoolVar(&jsonOut, "json", false, "Output as JSON")
1277+ cmd.Flags().BoolVar(&force, "force", false, "Execute purge actions; default is dry-run")
1278+ cmd.Flags().BoolVar(&includeAttachments, "attachments", false, "Include orphaned attachment containers")
1279+ cmd.Flags().BoolVar(&includeVolumes, "volumes", false, "Include preserved volumes in the purge plan")
1280+ return cmd
1281+}
1282+
1283+type routePurgeOptions struct {
1284+ Force bool
1285+ Attachments bool
1286+ Volumes bool
1287+}
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+
1305+func runRoutePurge(ctx context.Context, cp ControlPlane, domainName string, opts routePurgeOptions) (*domain.CleanupReport, error) {
1306+ diag, err := buildRouteDiagnosis(ctx, cp, domainName)
1307+ if err != nil {
1308+ return nil, err
1309+ }
1310+ report := &domain.CleanupReport{Domain: domainName}
1311+ if diag.Runtime != nil && diag.Runtime.ContainerID != "" {
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"})
1313+ }
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+ }
1318+ }
1319+ if !opts.Attachments || !opts.Force {
1320+ report.PreservedAttachments = append(report.PreservedAttachments, diag.OrphanedAttachments...)
1321+ }
1322+ if opts.Attachments && opts.Force {
1323+ cleaner, ok := cp.(interface {
1324+ CleanupOrphanedAttachments(context.Context, string, bool) (*domain.CleanupReport, error)
1325+ })
1326+ if !ok {
1327+ return nil, fmt.Errorf("attachment purge is unavailable")
1328+ }
1329+ attachmentReport, err := cleaner.CleanupOrphanedAttachments(ctx, domainName, true)
1330+ if err != nil {
1331+ return nil, err
1332+ }
1333+ report.RemovedContainers = append(report.RemovedContainers, attachmentReport.RemovedContainers...)
1334+ report.PreservedVolumes = append(report.PreservedVolumes, attachmentReport.PreservedVolumes...)
1335+ report.PreservedAttachments = append(report.PreservedAttachments, attachmentReport.PreservedAttachments...)
1336+ report.OrphanedEntities = append(report.OrphanedEntities, attachmentReport.OrphanedEntities...)
1337+ report.Warnings = append(report.Warnings, attachmentReport.Warnings...)
1338+ report.Hints = append(report.Hints, attachmentReport.Hints...)
1339+ report.PartialFailures = append(report.PartialFailures, attachmentReport.PartialFailures...)
1340+ }
1341+ if opts.Volumes {
1342+ report.Warnings = append(report.Warnings, "volume purge is not executed yet; preserved volumes are reported for manual review")
1343+ }
1344+ if !opts.Force {
1345+ report.Hints = append(report.Hints, "dry-run only; add --force with explicit category flags to execute supported purge actions")
1346+ }
1347+ return report, nil
1348+}
1349+
1350+func writeRoutePurgeText(out io.Writer, report *domain.CleanupReport, force bool) error {
1351+ mode := "Purge dry-run"
1352+ if force {
1353+ mode = "Purge completed"
1354+ }
1355+ if err := cliWriteLine(out, cliRenderTitle(mode+": "+report.Domain)); err != nil {
1356+ return err
1357+ }
1358+ if err := writePreservedAttachments(out, report.PreservedAttachments); err != nil {
1359+ return err
1360+ }
1361+ if err := writeRemovedContainers(out, report.RemovedContainers); err != nil {
1362+ return err
1363+ }
1364+ for _, entity := range report.OrphanedEntities {
1365+ if err := cliWriteLine(out, cliRenderWarning("Purge candidate: "+entity.Kind+" "+entity.ID)); err != nil {
1366+ return err
1367+ }
1368+ }
1369+ for _, volume := range report.PreservedVolumes {
1370+ if err := cliWriteLine(out, cliRenderMeta("Preserved volume:", volume.Name)); err != nil {
1371+ return err
1372+ }
1373+ }
1374+ return writeCleanupMessages(out, report.Warnings, report.Hints)
1375+}
1376+
10821377 func newRoutesAddCmd() *cobra.Command {
10831378 var image string
10841379
@@ -1183,12 +1478,22 @@ Examples:
11831478 return err
11841479 }
11851480 defer handle.close()
1186- if err := handle.plane.RemoveRoute(ctx, routeDomain); err != nil {
1481+ var cleanup *dto.CleanupReport
1482+ if remover, ok := handle.plane.(interface {
1483+ RemoveRouteWithCleanup(context.Context, string) (*dto.RouteDeleteResponse, error)
1484+ }); ok {
1485+ resp, err := remover.RemoveRouteWithCleanup(ctx, routeDomain)
1486+ if err != nil {
1487+ return fmt.Errorf("failed to remove route: %w", err)
1488+ }
1489+ if resp != nil {
1490+ cleanup = resp.Cleanup
1491+ }
1492+ } else if err := handle.plane.RemoveRoute(ctx, routeDomain); err != nil {
11871493 return fmt.Errorf("failed to remove route: %w", err)
11881494 }
11891495
1190- fmt.Println(styles.RenderSuccess(fmt.Sprintf("Route removed: %s", routeDomain)))
1191- return nil
1496+ return writeRouteRemoveText(cmd.OutOrStdout(), routeDomain, cleanup)
11921497 },
11931498 }
11941499
@@ -1197,6 +1502,105 @@ Examples:
11971502 return cmd
11981503 }
11991504
1505+func writeRouteRemoveText(out io.Writer, routeDomain string, cleanup *dto.CleanupReport) error {
1506+ if err := cliWriteLine(out, styles.RenderSuccess(fmt.Sprintf("Route removed: %s", routeDomain))); err != nil {
1507+ return err
1508+ }
1509+ if cleanup == nil {
1510+ return nil
1511+ }
1512+ cleanupReport := cleanupReportDTOToDomain(cleanup)
1513+ if err := writeRemovedContainers(out, cleanupReport.RemovedContainers); err != nil {
1514+ return err
1515+ }
1516+ if err := writePreservedAttachments(out, cleanupReport.PreservedAttachments); err != nil {
1517+ return err
1518+ }
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
1538+}
1539+
1540+func writeRemovedContainers(out io.Writer, containers []domain.CleanupContainer) error {
1541+ if len(containers) == 0 {
1542+ return nil
1543+ }
1544+ if err := cliWriteLine(out, ""); err != nil {
1545+ return err
1546+ }
1547+ if err := cliWriteLine(out, styles.Theme.Bold.Render("Removed containers:")); err != nil {
1548+ return err
1549+ }
1550+ for _, c := range containers {
1551+ label := c.Name
1552+ if label == "" {
1553+ label = c.ID
1554+ }
1555+ if id := shortContainerID(c.ID); id != "" && id != label {
1556+ label = fmt.Sprintf("%s (%s)", label, id)
1557+ }
1558+ if err := cliWriteLine(out, " "+label); err != nil {
1559+ return err
1560+ }
1561+ }
1562+ return nil
1563+}
1564+
1565+func writePreservedAttachments(out io.Writer, attachments []domain.CleanupAttachment) error {
1566+ if len(attachments) == 0 {
1567+ return nil
1568+ }
1569+ if err := cliWriteLine(out, ""); err != nil {
1570+ return err
1571+ }
1572+ if err := cliWriteLine(out, styles.Theme.Bold.Render("Preserved attachments:")); err != nil {
1573+ return err
1574+ }
1575+ for _, a := range attachments {
1576+ label := a.Name
1577+ if label == "" {
1578+ label = a.ContainerID
1579+ }
1580+ if a.Status != "" {
1581+ label = fmt.Sprintf("%s (%s)", label, a.Status)
1582+ }
1583+ if err := cliWriteLine(out, " "+label); err != nil {
1584+ return err
1585+ }
1586+ }
1587+ return nil
1588+}
1589+
1590+func writeCleanupMessages(out io.Writer, warnings, hints []string) error {
1591+ for _, warning := range warnings {
1592+ if err := cliWriteLine(out, cliRenderWarning(warning)); err != nil {
1593+ return err
1594+ }
1595+ }
1596+ for _, hint := range hints {
1597+ if err := cliWriteLine(out, cliRenderMuted("Hint: "+hint)); err != nil {
1598+ return err
1599+ }
1600+ }
1601+ return nil
1602+}
1603+
12001604 // newStatusCmd creates the status command.
12011605 func newStatusCmd() *cobra.Command {
12021606 return &cobra.Command{
M internal/adapters/in/cli/routes_test.go +74 -0
@@ -13,6 +13,8 @@ import (
1313 "sync/atomic"
1414 "testing"
1515
16+ "github.com/bnema/gordon/internal/adapters/dto"
17+ climocks "github.com/bnema/gordon/internal/adapters/in/cli/mocks"
1618 "github.com/bnema/gordon/internal/adapters/in/cli/remote"
1719 "github.com/bnema/gordon/internal/adapters/in/cli/ui/components"
1820 "github.com/bnema/gordon/internal/adapters/in/cli/ui/styles"
@@ -39,6 +41,14 @@ func (c *routesShowTestControlPlane) GetHealth(ctx context.Context) (map[string]
3941 panic("unexpected call")
4042 }
4143
44+func (c *routesShowTestControlPlane) ListOrphanedAttachments(context.Context) ([]domain.CleanupAttachment, error) {
45+ panic("unexpected call")
46+}
47+
48+func (c *routesShowTestControlPlane) CleanupOrphanedAttachments(context.Context, string, bool) (*domain.CleanupReport, error) {
49+ panic("unexpected call")
50+}
51+
4252 func TestTruncateImage(t *testing.T) {
4353 tests := []struct {
4454 name string
@@ -885,6 +895,35 @@ func TestRenderRoutesStatusSections_KeepsLocalErrorWhenAllRemotesFail(t *testing
885895 assert.Contains(t, rendered, "dial tcp timeout")
886896 }
887897
898+func TestWriteRouteRemoveTextShowsCleanupReport(t *testing.T) {
899+ var out bytes.Buffer
900+ report := &domain.CleanupReport{
901+ Domain: "app.example.com",
902+ RemovedContainers: []domain.CleanupContainer{{
903+ ID: "1234567890abcdef",
904+ Name: "gordon-app.example.com",
905+ }},
906+ PreservedAttachments: []domain.CleanupAttachment{{
907+ Name: "postgres",
908+ ContainerID: "attachment-1",
909+ Status: "running",
910+ }},
911+ Warnings: []string{"volumes preserved"},
912+ Hints: []string{"run diagnose"},
913+ }
914+
915+ err := writeRouteRemoveText(&out, "app.example.com", dto.CleanupReportFromDomain(report))
916+ require.NoError(t, err)
917+ rendered := stripANSI(out.String())
918+ assert.Contains(t, rendered, "Route removed: app.example.com")
919+ assert.Contains(t, rendered, "Removed containers:")
920+ assert.Contains(t, rendered, "gordon-app.example.com (1234567890ab)")
921+ assert.Contains(t, rendered, "Preserved attachments:")
922+ assert.Contains(t, rendered, "postgres (running)")
923+ assert.Contains(t, rendered, "volumes preserved")
924+ assert.Contains(t, rendered, "run diagnose")
925+}
926+
888927 func TestBuildRouteStatusTree_DeterministicAcrossInputOrder(t *testing.T) {
889928 routes := []routeStatusItem{
890929 {Domain: "zulu.example", Image: "zulu:v1", Network: "gordon-zulu", ContainerStatus: "running"},
@@ -901,3 +940,38 @@ func TestBuildRouteStatusTree_DeterministicAcrossInputOrder(t *testing.T) {
901940
902941 assert.Equal(t, outA.String(), outB.String())
903942 }
943+
944+func TestAttachmentsForRouteDiagnosisUsesOwner(t *testing.T) {
945+ attachments := []domain.CleanupAttachment{
946+ {Name: "unexpected-name", Owner: "app.example.com", ContainerID: "match"},
947+ {Name: "gordon-other-example-com-postgres", Owner: "other.example.com", ContainerID: "miss"},
948+ }
949+
950+ matched := attachmentsForRouteDiagnosis(attachments, "app.example.com")
951+
952+ require.Len(t, matched, 1)
953+ assert.Equal(t, "match", matched[0].ContainerID)
954+}
955+
956+func TestRunRoutePurge_AttachmentsForceIsScopedToRoute(t *testing.T) {
957+ cp := climocks.NewMockControlPlane(t)
958+ orphanedAttachments := []domain.CleanupAttachment{
959+ {ContainerID: "app-attachment", Owner: "app.example.com", Name: "postgres"},
960+ {ContainerID: "other-attachment", Owner: "other.example.com", Name: "redis"},
961+ }
962+ cp.EXPECT().GetRoute(context.Background(), "app.example.com").Return(&domain.Route{Domain: "app.example.com", Image: "app:latest"}, nil).Once()
963+ cp.EXPECT().ListRoutesWithDetails(context.Background()).Return(nil, nil).Once()
964+ cp.EXPECT().GetHealth(context.Background()).Return(nil, nil).Once()
965+ cp.EXPECT().ListVolumes(context.Background()).Return(nil, nil).Once()
966+ cp.EXPECT().ListOrphanedAttachments(context.Background()).Return(orphanedAttachments, nil).Once()
967+ cp.EXPECT().CleanupOrphanedAttachments(context.Background(), "app.example.com", true).Return(&domain.CleanupReport{
968+ RemovedContainers: []domain.CleanupContainer{{ID: "app-attachment", Name: "postgres"}},
969+ }, nil).Once()
970+
971+ report, err := runRoutePurge(context.Background(), cp, "app.example.com", routePurgeOptions{Force: true, Attachments: true})
972+
973+ require.NoError(t, err)
974+ require.NotNil(t, report)
975+ require.Len(t, report.RemovedContainers, 1)
976+ assert.Equal(t, "app-attachment", report.RemovedContainers[0].ID)
977+}
M internal/adapters/in/cli/tls_test.go +8 -0
@@ -82,6 +82,14 @@ func (c *tlsTestControlPlane) GetAttachmentsConfig(context.Context, string) ([]s
8282 panic("unexpected call")
8383 }
8484
85+func (c *tlsTestControlPlane) ListOrphanedAttachments(context.Context) ([]domain.CleanupAttachment, error) {
86+ panic("unexpected call")
87+}
88+
89+func (c *tlsTestControlPlane) CleanupOrphanedAttachments(context.Context, string, bool) (*domain.CleanupReport, error) {
90+ panic("unexpected call")
91+}
92+
8593 func (c *tlsTestControlPlane) FindAttachmentTargetsByImage(context.Context, string) ([]string, error) {
8694 panic("unexpected call")
8795 }
M internal/adapters/in/http/admin/handler.go +85 -14
@@ -229,16 +229,18 @@ type routeHandler func(w http.ResponseWriter, r *http.Request, path string)
229229 func (h *Handler) matchRoute(path string) (routeHandler, bool) {
230230 // Exact match routes
231231 exactRoutes := map[string]routeHandler{
232- "/networks": func(w http.ResponseWriter, r *http.Request, _ string) { h.handleNetworks(w, r) },
233- "/status": func(w http.ResponseWriter, r *http.Request, _ string) { h.handleStatus(w, r) },
234- "/health": func(w http.ResponseWriter, r *http.Request, _ string) { h.handleHealth(w, r) },
235- "/bootstrap": func(w http.ResponseWriter, r *http.Request, _ string) { h.handleBootstrap(w, r) },
236- "/reload": func(w http.ResponseWriter, r *http.Request, _ string) { h.handleReload(w, r) },
237- "/config": func(w http.ResponseWriter, r *http.Request, _ string) { h.handleConfig(w, r) },
238- "/auth/verify": func(w http.ResponseWriter, r *http.Request, _ string) { h.handleAuthVerify(w, r) },
239- "/volumes": func(w http.ResponseWriter, r *http.Request, _ string) { h.handleListVolumes(w, r) },
240- "/volumes/prune": func(w http.ResponseWriter, r *http.Request, _ string) { h.handlePruneVolumes(w, r) },
241- "/tls/status": func(w http.ResponseWriter, r *http.Request, _ string) { h.handleTLSStatus(w, r) },
232+ "/networks": func(w http.ResponseWriter, r *http.Request, _ string) { h.handleNetworks(w, r) },
233+ "/status": func(w http.ResponseWriter, r *http.Request, _ string) { h.handleStatus(w, r) },
234+ "/health": func(w http.ResponseWriter, r *http.Request, _ string) { h.handleHealth(w, r) },
235+ "/bootstrap": func(w http.ResponseWriter, r *http.Request, _ string) { h.handleBootstrap(w, r) },
236+ "/reload": func(w http.ResponseWriter, r *http.Request, _ string) { h.handleReload(w, r) },
237+ "/config": func(w http.ResponseWriter, r *http.Request, _ string) { h.handleConfig(w, r) },
238+ "/auth/verify": func(w http.ResponseWriter, r *http.Request, _ string) { h.handleAuthVerify(w, r) },
239+ "/volumes": func(w http.ResponseWriter, r *http.Request, _ string) { h.handleListVolumes(w, r) },
240+ "/volumes/prune": func(w http.ResponseWriter, r *http.Request, _ string) { h.handlePruneVolumes(w, r) },
241+ "/attachments/orphans": func(w http.ResponseWriter, r *http.Request, _ string) { h.handleAttachmentOrphans(w, r) },
242+ "/attachments/prune": func(w http.ResponseWriter, r *http.Request, _ string) { h.handleAttachmentPrune(w, r) },
243+ "/tls/status": func(w http.ResponseWriter, r *http.Request, _ string) { h.handleTLSStatus(w, r) },
242244 }
243245 if handler, ok := exactRoutes[path]; ok {
244246 return handler, true
@@ -274,6 +276,62 @@ func (h *Handler) matchRoute(path string) (routeHandler, bool) {
274276 return nil, false
275277 }
276278
279+func (h *Handler) handleAttachmentOrphans(w http.ResponseWriter, r *http.Request) {
280+ ctx := r.Context()
281+ if r.Method != http.MethodGet {
282+ h.sendError(w, http.StatusMethodNotAllowed, "method not allowed")
283+ return
284+ }
285+ if !HasAccess(ctx, domain.AdminResourceConfig, domain.AdminActionRead) {
286+ h.sendError(w, http.StatusForbidden, "insufficient permissions for config:read")
287+ return
288+ }
289+ if h.containerSvc == nil {
290+ log := zerowrap.FromCtx(ctx)
291+ log.Error().Msg("container service not available for orphaned attachment listing")
292+ h.sendError(w, http.StatusInternalServerError, "container service not available")
293+ return
294+ }
295+ attachments, err := h.containerSvc.ListOrphanedAttachments(ctx)
296+ if err != nil {
297+ log := zerowrap.FromCtx(ctx)
298+ log.Error().Err(err).Msg("failed to list orphaned attachments")
299+ h.sendError(w, http.StatusInternalServerError, "failed to list orphaned attachments")
300+ return
301+ }
302+ h.sendJSON(w, http.StatusOK, map[string]any{"attachments": attachments})
303+}
304+
305+func (h *Handler) handleAttachmentPrune(w http.ResponseWriter, r *http.Request) {
306+ ctx := r.Context()
307+ if r.Method != http.MethodPost {
308+ h.sendError(w, http.StatusMethodNotAllowed, "method not allowed")
309+ return
310+ }
311+ if !HasAccess(ctx, domain.AdminResourceConfig, domain.AdminActionWrite) {
312+ h.sendError(w, http.StatusForbidden, "insufficient permissions for config:write")
313+ return
314+ }
315+ if h.containerSvc == nil {
316+ log := zerowrap.FromCtx(ctx)
317+ log.Error().Msg("container service not available for orphaned attachment cleanup")
318+ h.sendError(w, http.StatusInternalServerError, "container service not available")
319+ return
320+ }
321+ stop := r.URL.Query().Get("stop") == "true"
322+ owner := r.URL.Query().Get("owner")
323+ var report *domain.CleanupReport
324+ var err error
325+ report, err = h.containerSvc.CleanupOrphanedAttachments(ctx, owner, stop)
326+ if err != nil {
327+ log := zerowrap.FromCtx(ctx)
328+ log.Error().Err(err).Str("owner", owner).Bool("stop", stop).Msg("failed to cleanup orphaned attachments")
329+ h.sendError(w, http.StatusInternalServerError, "failed to cleanup orphaned attachments")
330+ return
331+ }
332+ h.sendJSON(w, http.StatusOK, report)
333+}
334+
277335 // handleAttachmentsByImage handles GET /admin/attachments/by-image/{image} endpoint.
278336 // Returns all attachment targets associated with the given image name.
279337 func (h *Handler) handleAttachmentsByImage(w http.ResponseWriter, r *http.Request, path string) {
@@ -721,20 +779,33 @@ func (h *Handler) handleRoutesDelete(w http.ResponseWriter, r *http.Request, rou
721779 }
722780
723781 if err := h.configSvc.RemoveRoute(ctx, routeDomain); err != nil {
724- log.Error().Err(err).Str("domain", routeDomain).Msg("failed to remove route")
725782 switch {
726783 case errors.Is(err, domain.ErrRouteNotFound):
727- h.sendError(w, http.StatusNotFound, "route not found")
784+ log.Debug().Str("domain", routeDomain).Msg("route already absent, reconciling runtime state")
728785 case errors.Is(err, domain.ErrRouteDomainEmpty), errors.Is(err, domain.ErrRouteDomainInvalid):
786+ log.Error().Err(err).Str("domain", routeDomain).Msg("failed to remove route")
729787 h.sendError(w, http.StatusBadRequest, "invalid route domain")
788+ return
730789 default:
790+ log.Error().Err(err).Str("domain", routeDomain).Msg("failed to remove route")
731791 h.sendError(w, http.StatusInternalServerError, "failed to remove route")
792+ return
732793 }
733- return
794+ }
795+
796+ var cleanup *dto.CleanupReport
797+ if h.containerSvc != nil {
798+ report, err := h.containerSvc.ReconcileRemovedRoute(ctx, routeDomain)
799+ if err != nil {
800+ log.Error().Err(err).Str("domain", routeDomain).Msg("failed to cleanup removed route runtime state")
801+ h.sendError(w, http.StatusInternalServerError, "route removed but runtime cleanup failed")
802+ return
803+ }
804+ cleanup = dto.CleanupReportFromDomain(report)
734805 }
735806
736807 log.Info().Str("domain", routeDomain).Msg("route removed")
737- h.sendJSON(w, http.StatusOK, dto.RouteDeleteResponse{Status: "removed"})
808+ h.sendJSON(w, http.StatusOK, dto.RouteDeleteResponse{Status: "removed", Cleanup: cleanup})
738809 }
739810
740811 // handleRoutesByImage handles GET /admin/routes/by-image/{image} endpoint.
M internal/adapters/in/http/admin/handler_test.go +241 -1
@@ -720,7 +720,8 @@ func TestHandler_RoutesDelete_RequiresWriteScope(t *testing.T) {
720720 for _, tt := range tests {
721721 t.Run(tt.name, func(t *testing.T) {
722722 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).Once()
724725 }
725726
726727 req := httptest.NewRequest("DELETE", "/admin/routes/app.example.com", nil)
@@ -734,6 +735,115 @@ func TestHandler_RoutesDelete_RequiresWriteScope(t *testing.T) {
734735 }
735736 }
736737
738+func TestHandler_RoutesDelete_ReconcilesRuntimeWhenRouteAlreadyMissing(t *testing.T) {
739+ configSvc := inmocks.NewMockConfigService(t)
740+ authSvc := inmocks.NewMockAuthService(t)
741+ containerSvc := inmocks.NewMockContainerService(t)
742+ secretSvc := inmocks.NewMockSecretService(t)
743+ report := &domain.CleanupReport{Domain: "app.example.com"}
744+
745+ configSvc.EXPECT().RemoveRoute(mock.Anything, "app.example.com").Return(domain.ErrRouteNotFound).Once()
746+ containerSvc.EXPECT().ReconcileRemovedRoute(mock.Anything, "app.example.com").Return(report, nil).Once()
747+
748+ handler := newTestHandler(t, func(d *HandlerDeps) {
749+ d.ConfigSvc = configSvc
750+ d.AuthSvc = authSvc
751+ d.ContainerSvc = containerSvc
752+ d.SecretSvc = secretSvc
753+ })
754+ server := newScopedTestServer(t, handler, "admin:routes:write")
755+
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()
761+
762+ require.Equal(t, http.StatusOK, respHTTP.StatusCode)
763+ var resp dto.RouteDeleteResponse
764+ require.NoError(t, json.NewDecoder(respHTTP.Body).Decode(&resp))
765+ require.NotNil(t, resp.Cleanup)
766+ assert.Equal(t, "app.example.com", resp.Cleanup.Domain)
767+}
768+
769+func TestHandler_RoutesDelete_ReturnsServerErrorWhenRuntimeCleanupFails(t *testing.T) {
770+ configSvc := inmocks.NewMockConfigService(t)
771+ authSvc := inmocks.NewMockAuthService(t)
772+ containerSvc := inmocks.NewMockContainerService(t)
773+ secretSvc := inmocks.NewMockSecretService(t)
774+
775+ configSvc.EXPECT().RemoveRoute(mock.Anything, "app.example.com").Return(nil).Once()
776+ containerSvc.EXPECT().ReconcileRemovedRoute(mock.Anything, "app.example.com").Return(nil, errors.New("boom")).Once()
777+
778+ handler := newTestHandler(t, func(d *HandlerDeps) {
779+ d.ConfigSvc = configSvc
780+ d.AuthSvc = authSvc
781+ d.ContainerSvc = containerSvc
782+ d.SecretSvc = secretSvc
783+ })
784+ server := newScopedTestServer(t, handler, "admin:routes:write")
785+
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)
793+
794+ assert.Equal(t, http.StatusInternalServerError, resp.StatusCode)
795+ assert.Contains(t, string(body), "route removed but runtime cleanup failed")
796+}
797+
798+func TestHandler_RoutesDelete_ReconcilesRuntimeAndReturnsCleanupReport(t *testing.T) {
799+ configSvc := inmocks.NewMockConfigService(t)
800+ authSvc := inmocks.NewMockAuthService(t)
801+ containerSvc := inmocks.NewMockContainerService(t)
802+ secretSvc := inmocks.NewMockSecretService(t)
803+
804+ report := &domain.CleanupReport{
805+ Domain: "app.example.com",
806+ RemovedContainers: []domain.CleanupContainer{{
807+ ID: "container-1",
808+ Name: "gordon-app.example.com",
809+ }},
810+ PreservedAttachments: []domain.CleanupAttachment{{
811+ Name: "postgres",
812+ ContainerID: "attachment-1",
813+ Status: "running",
814+ }},
815+ }
816+
817+ removeCall := configSvc.EXPECT().RemoveRoute(mock.Anything, "app.example.com").Return(nil).Once()
818+ cleanupCall := containerSvc.EXPECT().ReconcileRemovedRoute(mock.Anything, "app.example.com").Return(report, nil).Once()
819+ mock.InOrder(removeCall, cleanupCall)
820+
821+ handler := newTestHandler(t, func(d *HandlerDeps) {
822+ d.ConfigSvc = configSvc
823+ d.AuthSvc = authSvc
824+ d.ContainerSvc = containerSvc
825+ d.SecretSvc = secretSvc
826+ })
827+ server := newScopedTestServer(t, handler, "admin:routes:write")
828+
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()
834+
835+ require.Equal(t, http.StatusOK, respHTTP.StatusCode)
836+ var resp dto.RouteDeleteResponse
837+ require.NoError(t, json.NewDecoder(respHTTP.Body).Decode(&resp))
838+ assert.Equal(t, "removed", resp.Status)
839+ require.NotNil(t, resp.Cleanup)
840+ assert.Equal(t, "app.example.com", resp.Cleanup.Domain)
841+ require.Len(t, resp.Cleanup.RemovedContainers, 1)
842+ assert.Equal(t, "container-1", resp.Cleanup.RemovedContainers[0].ID)
843+ require.Len(t, resp.Cleanup.PreservedAttachments, 1)
844+ assert.Equal(t, "attachment-1", resp.Cleanup.PreservedAttachments[0].ContainerID)
845+}
846+
737847 // Secrets endpoint tests
738848
739849 func TestHandler_SecretsGet_RequiresReadScope(t *testing.T) {
@@ -2712,3 +2822,133 @@ func TestHandler_TLSStatus_POSTReturns405(t *testing.T) {
27122822
27132823 assert.Equal(t, http.StatusMethodNotAllowed, resp.StatusCode)
27142824 }
2825+
2826+func TestHandler_AttachmentOrphans_ReturnsOrphanedAttachments(t *testing.T) {
2827+ configSvc := inmocks.NewMockConfigService(t)
2828+ authSvc := inmocks.NewMockAuthService(t)
2829+ containerSvc := inmocks.NewMockContainerService(t)
2830+ secretSvc := inmocks.NewMockSecretService(t)
2831+ attachments := []domain.CleanupAttachment{{ContainerID: "attachment-1", Name: "postgres", Owner: "app.example.com"}}
2832+ containerSvc.EXPECT().ListOrphanedAttachments(mock.Anything).Return(attachments, nil).Once()
2833+
2834+ handler := newTestHandler(t, func(d *HandlerDeps) {
2835+ d.ConfigSvc = configSvc
2836+ d.AuthSvc = authSvc
2837+ d.ContainerSvc = containerSvc
2838+ d.SecretSvc = secretSvc
2839+ })
2840+
2841+ req := httptest.NewRequest("GET", "/admin/attachments/orphans", nil)
2842+ req = req.WithContext(ctxWithScopes("admin:config:read"))
2843+ rec := httptest.NewRecorder()
2844+
2845+ handler.ServeHTTP(rec, req)
2846+
2847+ require.Equal(t, http.StatusOK, rec.Code)
2848+ var resp struct {
2849+ Attachments []domain.CleanupAttachment `json:"attachments"`
2850+ }
2851+ require.NoError(t, json.NewDecoder(rec.Body).Decode(&resp))
2852+ require.Len(t, resp.Attachments, 1)
2853+ assert.Equal(t, "app.example.com", resp.Attachments[0].Owner)
2854+}
2855+
2856+func TestHandler_AttachmentPrune_StopsOnlyWhenRequested(t *testing.T) {
2857+ configSvc := inmocks.NewMockConfigService(t)
2858+ authSvc := inmocks.NewMockAuthService(t)
2859+ containerSvc := inmocks.NewMockContainerService(t)
2860+ secretSvc := inmocks.NewMockSecretService(t)
2861+ report := &domain.CleanupReport{RemovedContainers: []domain.CleanupContainer{{ID: "attachment-1"}}}
2862+ containerSvc.EXPECT().CleanupOrphanedAttachments(mock.Anything, "", true).Return(report, nil).Once()
2863+
2864+ handler := newTestHandler(t, func(d *HandlerDeps) {
2865+ d.ConfigSvc = configSvc
2866+ d.AuthSvc = authSvc
2867+ d.ContainerSvc = containerSvc
2868+ d.SecretSvc = secretSvc
2869+ })
2870+
2871+ req := httptest.NewRequest("POST", "/admin/attachments/prune?stop=true", nil)
2872+ req = req.WithContext(ctxWithScopes("admin:config:write"))
2873+ rec := httptest.NewRecorder()
2874+
2875+ handler.ServeHTTP(rec, req)
2876+
2877+ require.Equal(t, http.StatusOK, rec.Code)
2878+ var got domain.CleanupReport
2879+ require.NoError(t, json.NewDecoder(rec.Body).Decode(&got))
2880+ require.Len(t, got.RemovedContainers, 1)
2881+ assert.Equal(t, "attachment-1", got.RemovedContainers[0].ID)
2882+}
2883+
2884+func TestHandler_AttachmentOrphans_ReturnsServerErrorOnListFailure(t *testing.T) {
2885+ configSvc := inmocks.NewMockConfigService(t)
2886+ authSvc := inmocks.NewMockAuthService(t)
2887+ containerSvc := inmocks.NewMockContainerService(t)
2888+ secretSvc := inmocks.NewMockSecretService(t)
2889+ containerSvc.EXPECT().ListOrphanedAttachments(mock.Anything).Return(nil, errors.New("boom")).Once()
2890+
2891+ handler := newTestHandler(t, func(d *HandlerDeps) {
2892+ d.ConfigSvc = configSvc
2893+ d.AuthSvc = authSvc
2894+ d.ContainerSvc = containerSvc
2895+ d.SecretSvc = secretSvc
2896+ })
2897+
2898+ req := httptest.NewRequest("GET", "/admin/attachments/orphans", nil)
2899+ req = req.WithContext(ctxWithScopes("admin:config:read"))
2900+ rec := httptest.NewRecorder()
2901+
2902+ handler.ServeHTTP(rec, req)
2903+
2904+ assert.Equal(t, http.StatusInternalServerError, rec.Code)
2905+}
2906+
2907+func TestHandler_AttachmentOrphans_PostReturns405(t *testing.T) {
2908+ handler := newTestHandler(t, func(d *HandlerDeps) {
2909+ d.ContainerSvc = inmocks.NewMockContainerService(t)
2910+ })
2911+ req := httptest.NewRequest("POST", "/admin/attachments/orphans", nil)
2912+ req = req.WithContext(ctxWithScopes("admin:config:read"))
2913+ rec := httptest.NewRecorder()
2914+
2915+ handler.ServeHTTP(rec, req)
2916+
2917+ assert.Equal(t, http.StatusMethodNotAllowed, rec.Code)
2918+}
2919+
2920+func TestHandler_AttachmentPrune_ReturnsServerErrorOnCleanupFailure(t *testing.T) {
2921+ configSvc := inmocks.NewMockConfigService(t)
2922+ authSvc := inmocks.NewMockAuthService(t)
2923+ containerSvc := inmocks.NewMockContainerService(t)
2924+ secretSvc := inmocks.NewMockSecretService(t)
2925+ containerSvc.EXPECT().CleanupOrphanedAttachments(mock.Anything, "", true).Return(nil, errors.New("boom")).Once()
2926+
2927+ handler := newTestHandler(t, func(d *HandlerDeps) {
2928+ d.ConfigSvc = configSvc
2929+ d.AuthSvc = authSvc
2930+ d.ContainerSvc = containerSvc
2931+ d.SecretSvc = secretSvc
2932+ })
2933+
2934+ req := httptest.NewRequest("POST", "/admin/attachments/prune?stop=true", nil)
2935+ req = req.WithContext(ctxWithScopes("admin:config:write"))
2936+ rec := httptest.NewRecorder()
2937+
2938+ handler.ServeHTTP(rec, req)
2939+
2940+ assert.Equal(t, http.StatusInternalServerError, rec.Code)
2941+}
2942+
2943+func TestHandler_AttachmentPrune_GetReturns405(t *testing.T) {
2944+ handler := newTestHandler(t, func(d *HandlerDeps) {
2945+ d.ContainerSvc = inmocks.NewMockContainerService(t)
2946+ })
2947+ req := httptest.NewRequest("GET", "/admin/attachments/prune", nil)
2948+ req = req.WithContext(ctxWithScopes("admin:config:write"))
2949+ rec := httptest.NewRecorder()
2950+
2951+ handler.ServeHTTP(rec, req)
2952+
2953+ assert.Equal(t, http.StatusMethodNotAllowed, rec.Code)
2954+}
M internal/boundaries/in/container.go +11 -0
@@ -20,6 +20,10 @@ type ContainerService interface {
2020 // Remove removes a container, optionally forcing removal.
2121 Remove(ctx context.Context, containerID string, force bool) error
2222
23+ // ReconcileRemovedRoute removes active route runtime containers after the
24+ // route has been removed from configuration, preserving stateful resources.
25+ ReconcileRemovedRoute(ctx context.Context, domain string) (*domain.CleanupReport, error)
26+
2327 // Get retrieves a container by domain name.
2428 Get(ctx context.Context, domain string) (*domain.Container, bool)
2529
@@ -36,6 +40,13 @@ type ContainerService interface {
3640 // ListAttachments returns attachments for a domain.
3741 ListAttachments(ctx context.Context, domain string) []domain.Attachment
3842
43+ // ListOrphanedAttachments returns running attachment containers no longer configured.
44+ ListOrphanedAttachments(ctx context.Context) ([]domain.CleanupAttachment, error)
45+
46+ // CleanupOrphanedAttachments optionally stops/removes orphaned attachment containers.
47+ // When owner is non-empty, cleanup is scoped to that attachment owner.
48+ CleanupOrphanedAttachments(ctx context.Context, owner string, stop bool) (*domain.CleanupReport, error)
49+
3950 // ListNetworks returns Gordon-managed networks.
4051 ListNetworks(ctx context.Context) ([]*domain.NetworkInfo, error)
4152
M internal/boundaries/in/mocks/mock_container_service.go +204 -0
@@ -95,6 +95,80 @@ func (_c *MockContainerService_AutoStart_Call) RunAndReturn(run func(ctx context
9595 return _c
9696 }
9797
98+// CleanupOrphanedAttachments provides a mock function for the type MockContainerService
99+func (_mock *MockContainerService) CleanupOrphanedAttachments(ctx context.Context, owner string, stop bool) (*domain.CleanupReport, error) {
100+ ret := _mock.Called(ctx, owner, stop)
101+
102+ if len(ret) == 0 {
103+ panic("no return value specified for CleanupOrphanedAttachments")
104+ }
105+
106+ var r0 *domain.CleanupReport
107+ var r1 error
108+ if returnFunc, ok := ret.Get(0).(func(context.Context, string, bool) (*domain.CleanupReport, error)); ok {
109+ return returnFunc(ctx, owner, stop)
110+ }
111+ if returnFunc, ok := ret.Get(0).(func(context.Context, string, bool) *domain.CleanupReport); ok {
112+ r0 = returnFunc(ctx, owner, stop)
113+ } else {
114+ if ret.Get(0) != nil {
115+ r0 = ret.Get(0).(*domain.CleanupReport)
116+ }
117+ }
118+ if returnFunc, ok := ret.Get(1).(func(context.Context, string, bool) error); ok {
119+ r1 = returnFunc(ctx, owner, stop)
120+ } else {
121+ r1 = ret.Error(1)
122+ }
123+ return r0, r1
124+}
125+
126+// MockContainerService_CleanupOrphanedAttachments_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CleanupOrphanedAttachments'
127+type MockContainerService_CleanupOrphanedAttachments_Call struct {
128+ *mock.Call
129+}
130+
131+// CleanupOrphanedAttachments is a helper method to define mock.On call
132+// - ctx context.Context
133+// - owner string
134+// - stop bool
135+func (_e *MockContainerService_Expecter) CleanupOrphanedAttachments(ctx interface{}, owner interface{}, stop interface{}) *MockContainerService_CleanupOrphanedAttachments_Call {
136+ return &MockContainerService_CleanupOrphanedAttachments_Call{Call: _e.mock.On("CleanupOrphanedAttachments", ctx, owner, stop)}
137+}
138+
139+func (_c *MockContainerService_CleanupOrphanedAttachments_Call) Run(run func(ctx context.Context, owner string, stop bool)) *MockContainerService_CleanupOrphanedAttachments_Call {
140+ _c.Call.Run(func(args mock.Arguments) {
141+ var arg0 context.Context
142+ if args[0] != nil {
143+ arg0 = args[0].(context.Context)
144+ }
145+ var arg1 string
146+ if args[1] != nil {
147+ arg1 = args[1].(string)
148+ }
149+ var arg2 bool
150+ if args[2] != nil {
151+ arg2 = args[2].(bool)
152+ }
153+ run(
154+ arg0,
155+ arg1,
156+ arg2,
157+ )
158+ })
159+ return _c
160+}
161+
162+func (_c *MockContainerService_CleanupOrphanedAttachments_Call) Return(cleanupReport *domain.CleanupReport, err error) *MockContainerService_CleanupOrphanedAttachments_Call {
163+ _c.Call.Return(cleanupReport, err)
164+ return _c
165+}
166+
167+func (_c *MockContainerService_CleanupOrphanedAttachments_Call) RunAndReturn(run func(ctx context.Context, owner string, stop bool) (*domain.CleanupReport, error)) *MockContainerService_CleanupOrphanedAttachments_Call {
168+ _c.Call.Return(run)
169+ return _c
170+}
171+
98172 // Deploy provides a mock function for the type MockContainerService
99173 func (_mock *MockContainerService) Deploy(ctx context.Context, route domain.Route) (*domain.Container, error) {
100174 ret := _mock.Called(ctx, route)
@@ -458,6 +532,68 @@ func (_c *MockContainerService_ListNetworks_Call) RunAndReturn(run func(ctx cont
458532 return _c
459533 }
460534
535+// ListOrphanedAttachments provides a mock function for the type MockContainerService
536+func (_mock *MockContainerService) ListOrphanedAttachments(ctx context.Context) ([]domain.CleanupAttachment, error) {
537+ ret := _mock.Called(ctx)
538+
539+ if len(ret) == 0 {
540+ panic("no return value specified for ListOrphanedAttachments")
541+ }
542+
543+ var r0 []domain.CleanupAttachment
544+ var r1 error
545+ if returnFunc, ok := ret.Get(0).(func(context.Context) ([]domain.CleanupAttachment, error)); ok {
546+ return returnFunc(ctx)
547+ }
548+ if returnFunc, ok := ret.Get(0).(func(context.Context) []domain.CleanupAttachment); ok {
549+ r0 = returnFunc(ctx)
550+ } else {
551+ if ret.Get(0) != nil {
552+ r0 = ret.Get(0).([]domain.CleanupAttachment)
553+ }
554+ }
555+ if returnFunc, ok := ret.Get(1).(func(context.Context) error); ok {
556+ r1 = returnFunc(ctx)
557+ } else {
558+ r1 = ret.Error(1)
559+ }
560+ return r0, r1
561+}
562+
563+// MockContainerService_ListOrphanedAttachments_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ListOrphanedAttachments'
564+type MockContainerService_ListOrphanedAttachments_Call struct {
565+ *mock.Call
566+}
567+
568+// ListOrphanedAttachments is a helper method to define mock.On call
569+// - ctx context.Context
570+func (_e *MockContainerService_Expecter) ListOrphanedAttachments(ctx interface{}) *MockContainerService_ListOrphanedAttachments_Call {
571+ return &MockContainerService_ListOrphanedAttachments_Call{Call: _e.mock.On("ListOrphanedAttachments", ctx)}
572+}
573+
574+func (_c *MockContainerService_ListOrphanedAttachments_Call) Run(run func(ctx context.Context)) *MockContainerService_ListOrphanedAttachments_Call {
575+ _c.Call.Run(func(args mock.Arguments) {
576+ var arg0 context.Context
577+ if args[0] != nil {
578+ arg0 = args[0].(context.Context)
579+ }
580+ run(
581+ arg0,
582+ )
583+ })
584+ return _c
585+}
586+
587+func (_c *MockContainerService_ListOrphanedAttachments_Call) Return(cleanupAttachments []domain.CleanupAttachment, err error) *MockContainerService_ListOrphanedAttachments_Call {
588+ _c.Call.Return(cleanupAttachments, err)
589+ return _c
590+}
591+
592+func (_c *MockContainerService_ListOrphanedAttachments_Call) RunAndReturn(run func(ctx context.Context) ([]domain.CleanupAttachment, error)) *MockContainerService_ListOrphanedAttachments_Call {
593+ _c.Call.Return(run)
594+ return _c
595+}
596+
461597 // ListRoutesWithDetails provides a mock function for the type MockContainerService
462598 func (_mock *MockContainerService) ListRoutesWithDetails(ctx context.Context) []domain.RouteInfo {
463599 ret := _mock.Called(ctx)
@@ -511,6 +647,74 @@ func (_c *MockContainerService_ListRoutesWithDetails_Call) RunAndReturn(run func
511647 return _c
512648 }
513649
650+// ReconcileRemovedRoute provides a mock function for the type MockContainerService
651+func (_mock *MockContainerService) ReconcileRemovedRoute(ctx context.Context, domain1 string) (*domain.CleanupReport, error) {
652+ ret := _mock.Called(ctx, domain1)
653+
654+ if len(ret) == 0 {
655+ panic("no return value specified for ReconcileRemovedRoute")
656+ }
657+
658+ var r0 *domain.CleanupReport
659+ var r1 error
660+ if returnFunc, ok := ret.Get(0).(func(context.Context, string) (*domain.CleanupReport, error)); ok {
661+ return returnFunc(ctx, domain1)
662+ }
663+ if returnFunc, ok := ret.Get(0).(func(context.Context, string) *domain.CleanupReport); ok {
664+ r0 = returnFunc(ctx, domain1)
665+ } else {
666+ if ret.Get(0) != nil {
667+ r0 = ret.Get(0).(*domain.CleanupReport)
668+ }
669+ }
670+ if returnFunc, ok := ret.Get(1).(func(context.Context, string) error); ok {
671+ r1 = returnFunc(ctx, domain1)
672+ } else {
673+ r1 = ret.Error(1)
674+ }
675+ return r0, r1
676+}
677+
678+// MockContainerService_ReconcileRemovedRoute_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ReconcileRemovedRoute'
679+type MockContainerService_ReconcileRemovedRoute_Call struct {
680+ *mock.Call
681+}
682+
683+// ReconcileRemovedRoute is a helper method to define mock.On call
684+// - ctx context.Context
685+// - domain1 string
686+func (_e *MockContainerService_Expecter) ReconcileRemovedRoute(ctx interface{}, domain1 interface{}) *MockContainerService_ReconcileRemovedRoute_Call {
687+ return &MockContainerService_ReconcileRemovedRoute_Call{Call: _e.mock.On("ReconcileRemovedRoute", ctx, domain1)}
688+}
689+
690+func (_c *MockContainerService_ReconcileRemovedRoute_Call) Run(run func(ctx context.Context, domain1 string)) *MockContainerService_ReconcileRemovedRoute_Call {
691+ _c.Call.Run(func(args mock.Arguments) {
692+ var arg0 context.Context
693+ if args[0] != nil {
694+ arg0 = args[0].(context.Context)
695+ }
696+ var arg1 string
697+ if args[1] != nil {
698+ arg1 = args[1].(string)
699+ }
700+ run(
701+ arg0,
702+ arg1,
703+ )
704+ })
705+ return _c
706+}
707+
708+func (_c *MockContainerService_ReconcileRemovedRoute_Call) Return(cleanupReport *domain.CleanupReport, err error) *MockContainerService_ReconcileRemovedRoute_Call {
709+ _c.Call.Return(cleanupReport, err)
710+ return _c
711+}
712+
713+func (_c *MockContainerService_ReconcileRemovedRoute_Call) RunAndReturn(run func(ctx context.Context, domain1 string) (*domain.CleanupReport, error)) *MockContainerService_ReconcileRemovedRoute_Call {
714+ _c.Call.Return(run)
715+ return _c
716+}
717+
514718 // Remove provides a mock function for the type MockContainerService
515719 func (_mock *MockContainerService) Remove(ctx context.Context, containerID string, force bool) error {
516720 ret := _mock.Called(ctx, containerID, force)
A internal/domain/cleanup.go +59 -0
@@ -0,0 +1,59 @@
1+package domain
2+
3+// CleanupReport describes the result of reconciling runtime state after a
4+// configuration entity was removed. Fields are additive-only for CLI/API JSON
5+// consumers; callers should tolerate unknown future fields.
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 []CleanupOrphanedEntity `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 stateful data intentionally preserved by cleanup.
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 intentionally preserved or kept for
33+// explicit follow-up cleanup.
34+type CleanupAttachment struct {
35+ Name string `json:"name"`
36+ Image string `json:"image"`
37+ ContainerID string `json:"container_id"`
38+ Status string `json:"status"`
39+ Owner string `json:"owner,omitempty"`
40+ Reason string `json:"reason,omitempty"`
41+}
42+
43+// CleanupOrphanedEntity identifies runtime state not currently configured.
44+type CleanupOrphanedEntity struct {
45+ Kind string `json:"kind"`
46+ ID string `json:"id,omitempty"`
47+ Name string `json:"name,omitempty"`
48+ Status string `json:"status,omitempty"`
49+ Reason string `json:"reason,omitempty"`
50+}
51+
52+// CleanupFailure records non-fatal cleanup failures that require follow-up.
53+type CleanupFailure struct {
54+ Action string `json:"action"`
55+ Kind string `json:"kind"`
56+ ID string `json:"id,omitempty"`
57+ Name string `json:"name,omitempty"`
58+ Error string `json:"error"`
59+}
M internal/usecase/container/events.go +20 -9
@@ -117,8 +117,8 @@ func (h *ConfigReloadHandler) Handle(ctx context.Context, event domain.Event) er
117117 currentContainers := h.containerSvc.List(ctx)
118118
119119 activeRoutes := make(map[string]*domain.Container)
120- for _, container := range currentContainers {
121- if route, exists := container.Labels[domain.LabelRoute]; exists {
120+ for domainName, container := range currentContainers {
121+ if route := routeDomainForContainer(domainName, container); route != "" {
122122 activeRoutes[route] = container
123123 }
124124 }
@@ -156,14 +156,10 @@ func (h *ConfigReloadHandler) Handle(ctx context.Context, event domain.Event) er
156156 log.Info().
157157 Str("domain", route).
158158 Str(zerowrap.FieldEntityID, container.ID).
159- Msg("route no longer configured, stopping container")
159+ Msg("route no longer configured, reconciling removed route runtime state")
160160
161- if err := h.containerSvc.Stop(ctx, container.ID); err != nil {
162- log.WrapErrWithFields(err, "failed to stop container", map[string]any{zerowrap.FieldEntityID: container.ID})
163- }
164-
165- if err := h.containerSvc.Remove(ctx, container.ID, true); err != nil {
166- log.WrapErrWithFields(err, "failed to remove container", map[string]any{zerowrap.FieldEntityID: container.ID})
161+ if _, err := h.containerSvc.ReconcileRemovedRoute(ctx, route); err != nil {
162+ log.WrapErrWithFields(err, "failed to reconcile removed route runtime state", map[string]any{"domain": route, zerowrap.FieldEntityID: container.ID})
167163 }
168164 }
169165
@@ -175,6 +171,21 @@ func (h *ConfigReloadHandler) CanHandle(eventType domain.EventType) bool {
175171 return eventType == domain.EventConfigReload
176172 }
177173
174+func routeDomainForContainer(trackedDomain string, container *domain.Container) string {
175+ if container == nil {
176+ return ""
177+ }
178+ if container.Labels != nil {
179+ if route := container.Labels[domain.LabelRoute]; route != "" {
180+ return route
181+ }
182+ if route := container.Labels[domain.LabelDomain]; route != "" {
183+ return route
184+ }
185+ }
186+ return trackedDomain
187+}
188+
178189 // ManualDeployHandler handles manual.deploy events for specific routes.
179190 type ManualDeployHandler struct {
180191 containerSvc in.ContainerService
M internal/usecase/container/events_test.go +29 -2
@@ -261,8 +261,7 @@ func TestConfigReloadHandler_Handle_StopsRemovedRoutes(t *testing.T) {
261261 // Empty routes - the route was removed
262262 configSvc.EXPECT().GetRoutes(mock.Anything).Return([]domain.Route{})
263263
264- containerSvc.EXPECT().Stop(mock.Anything, "container-old").Return(nil)
265- containerSvc.EXPECT().Remove(mock.Anything, "container-old", true).Return(nil)
264+ containerSvc.EXPECT().ReconcileRemovedRoute(mock.Anything, "removed.example.com").Return(&domain.CleanupReport{Domain: "removed.example.com"}, nil)
266265
267266 event := domain.Event{
268267 ID: "event-123",
@@ -274,6 +273,34 @@ func TestConfigReloadHandler_Handle_StopsRemovedRoutes(t *testing.T) {
274273 assert.NoError(t, err)
275274 }
276275
276+func TestConfigReloadHandler_Handle_ReconcilesRemovedRouteWithLegacyDomainLabel(t *testing.T) {
277+ containerSvc := inmocks.NewMockContainerService(t)
278+ configSvc := inmocks.NewMockConfigService(t)
279+
280+ handler := NewConfigReloadHandler(testCtx(), containerSvc, configSvc)
281+
282+ containerSvc.EXPECT().SyncContainers(mock.Anything).Return(nil)
283+ configSvc.EXPECT().GetAllAttachments(mock.Anything).Return(map[string][]string{})
284+ containerSvc.EXPECT().UpdateAttachments(map[string][]string{}).Return()
285+ containerSvc.EXPECT().List(mock.Anything).Return(map[string]*domain.Container{
286+ "legacy.example.com": {
287+ ID: "container-legacy",
288+ Labels: map[string]string{
289+ domain.LabelDomain: "legacy.example.com",
290+ domain.LabelImage: "legacy:latest",
291+ },
292+ },
293+ })
294+ configSvc.EXPECT().GetRoutes(mock.Anything).Return([]domain.Route{})
295+ containerSvc.EXPECT().ReconcileRemovedRoute(mock.Anything, "legacy.example.com").Return(&domain.CleanupReport{Domain: "legacy.example.com"}, nil)
296+
297+ event := domain.Event{ID: "event-legacy", Type: domain.EventConfigReload}
298+
299+ err := handler.Handle(context.Background(), event)
300+
301+ assert.NoError(t, err)
302+}
303+
277304 func TestConfigReloadHandler_Handle_RedeploysChangedImage(t *testing.T) {
278305 containerSvc := inmocks.NewMockContainerService(t)
279306 configSvc := inmocks.NewMockConfigService(t)
M internal/usecase/container/monitor.go +12 -4
@@ -134,6 +134,10 @@ func (m *Monitor) handleRunning(ctx context.Context, log zerowrap.Logger, domain
134134 }
135135
136136 if hasHealthcheck && healthStatus == "unhealthy" {
137+ if !m.isContainerStillTracked(domainName, containerID) {
138+ log.Debug().Str("domain", domainName).Msg("monitor: container no longer tracked, skipping unhealthy restart")
139+ return
140+ }
137141 log.Warn().Str("domain", domainName).Msg("monitor: container is unhealthy, restarting")
138142 if err := m.service.runtime.RestartContainer(ctx, containerID); err != nil {
139143 log.Warn().Err(err).Str("domain", domainName).Msg("monitor: failed to restart unhealthy container")
@@ -159,6 +163,13 @@ func (m *Monitor) handleRunning(ctx context.Context, log zerowrap.Logger, domain
159163 m.mu.Unlock()
160164 }
161165
166+func (m *Monitor) isContainerStillTracked(domainName, containerID string) bool {
167+ m.service.mu.RLock()
168+ current := m.service.containers[domainName]
169+ m.service.mu.RUnlock()
170+ return current != nil && current.ID == containerID
171+}
172+
162173 func (m *Monitor) handleCrash(ctx context.Context, log zerowrap.Logger, domainName, containerID string, exitCode int, now time.Time) {
163174 m.mu.Lock()
164175 rec := m.history[domainName]
@@ -212,10 +223,7 @@ func (m *Monitor) handleCrash(ctx context.Context, log zerowrap.Logger, domainNa
212223 m.mu.Unlock()
213224
214225 // Verify container ID still matches tracked state (deploy may have replaced it).
215- m.service.mu.RLock()
216- current := m.service.containers[domainName]
217- m.service.mu.RUnlock()
218- if current == nil || current.ID != containerID {
226+ if !m.isContainerStillTracked(domainName, containerID) {
219227 log.Debug().Str("domain", domainName).Msg("monitor: container replaced by deploy, skipping restart")
220228 return
221229 }
M internal/usecase/container/monitor_test.go +23 -0
@@ -127,6 +127,29 @@ func TestMonitor_RestartsUnhealthyContainer(t *testing.T) {
127127 runtime.AssertExpectations(t)
128128 }
129129
130+func TestMonitor_SkipsUnhealthyRestartWhenContainerWasUntracked(t *testing.T) {
131+ runtime := mocks.NewMockContainerRuntime(t)
132+ svc := newTestService(runtime)
133+
134+ tracked := &domain.Container{ID: "ctr-1", Image: "myapp:latest"}
135+ svc.containers["app.example.com"] = tracked
136+
137+ runtime.EXPECT().InspectContainer(mock.Anything, "ctr-1").Run(func(context.Context, string) {
138+ svc.mu.Lock()
139+ delete(svc.containers, "app.example.com")
140+ svc.mu.Unlock()
141+ }).Return(&domain.Container{
142+ ID: "ctr-1",
143+ Status: string(domain.ContainerStatusRunning),
144+ }, nil)
145+ runtime.EXPECT().GetContainerHealthStatus(mock.Anything, "ctr-1").Return("unhealthy", true, nil)
146+
147+ m := newMonitor(svc)
148+ m.check(monitorTestContext())
149+
150+ runtime.AssertNotCalled(t, "RestartContainer", mock.Anything, "ctr-1")
151+}
152+
130153 func TestMonitor_CrashLoopBackoff(t *testing.T) {
131154 runtime := mocks.NewMockContainerRuntime(t)
132155 svc := newTestService(runtime)
M internal/usecase/container/service.go +292 -0
@@ -1159,6 +1159,165 @@ func (s *Service) Stop(ctx context.Context, containerID string) error {
11591159 return nil
11601160 }
11611161
1162+// ReconcileRemovedRoute removes active runtime containers for a route that was
1163+// removed from configuration while preserving stateful resources for explicit
1164+// follow-up cleanup. It intentionally removes only main route containers, never
1165+// attachment containers or volumes.
1166+func (s *Service) ReconcileRemovedRoute(ctx context.Context, domainName string) (*domain.CleanupReport, error) {
1167+ canonicalDomain, ok := domain.CanonicalRouteDomain(domainName)
1168+ if !ok {
1169+ return &domain.CleanupReport{Domain: domainName}, domain.ErrRouteDomainInvalid
1170+ }
1171+ domainName = canonicalDomain
1172+
1173+ ctx = zerowrap.CtxWithFields(ctx, map[string]any{
1174+ zerowrap.FieldLayer: "usecase",
1175+ zerowrap.FieldUseCase: "ReconcileRemovedRoute",
1176+ "domain": domainName,
1177+ })
1178+ log := zerowrap.FromCtx(ctx)
1179+
1180+ report := &domain.CleanupReport{Domain: domainName}
1181+
1182+ unlock, err := s.acquireDomainDeployLock(ctx, domainName)
1183+ if err != nil {
1184+ return report, err
1185+ }
1186+ defer unlock()
1187+
1188+ allContainers, err := s.runtime.ListContainers(ctx, true)
1189+ if err != nil {
1190+ return report, log.WrapErr(err, "failed to list containers for removed route cleanup")
1191+ }
1192+
1193+ routeContainers := routeContainersForDomain(allContainers, domainName)
1194+
1195+ // Clear in-memory state before stopping containers so the monitor cannot
1196+ // interpret the deliberate removal as a crash that should be restarted.
1197+ s.clearRouteContainerTracking(ctx, domainName)
1198+ if inv := s.proxyCacheInvalidator(); inv != nil {
1199+ inv.InvalidateTarget(ctx, domainName)
1200+ }
1201+
1202+ report.PreservedAttachments = preservedAttachmentsForDomain(allContainers, domainName)
1203+ if len(report.PreservedAttachments) > 0 {
1204+ report.Hints = append(report.Hints, "attachments preserved; remove or purge attachments explicitly if they are no longer needed")
1205+ }
1206+
1207+ if len(routeContainers) == 0 {
1208+ report.Warnings = append(report.Warnings, "no active route container found for removed route")
1209+ return report, nil
1210+ }
1211+
1212+ s.removeRouteContainersForCleanup(ctx, log, routeContainers, report)
1213+
1214+ if len(report.PartialFailures) > 0 {
1215+ report.Warnings = append(report.Warnings, "route cleanup completed with partial failures; manual follow-up may be required")
1216+ }
1217+
1218+ return report, nil
1219+}
1220+
1221+func routeContainersForDomain(containers []*domain.Container, domainName string) []*domain.Container {
1222+ var routeContainers []*domain.Container
1223+ for _, c := range containers {
1224+ if isManagedRouteContainerForDomain(c, domainName) {
1225+ routeContainers = append(routeContainers, c)
1226+ }
1227+ }
1228+ return routeContainers
1229+}
1230+
1231+func preservedAttachmentsForDomain(containers []*domain.Container, domainName string) []domain.CleanupAttachment {
1232+ var attachments []domain.CleanupAttachment
1233+ for _, c := range containers {
1234+ if isPreservedAttachmentForDomain(c, domainName) {
1235+ attachments = append(attachments, domain.CleanupAttachment{
1236+ Name: c.Name,
1237+ Image: c.Image,
1238+ ContainerID: c.ID,
1239+ Status: c.Status,
1240+ Owner: c.Labels[domain.LabelAttachedTo],
1241+ Reason: "attachments are preserved by default; remove or purge them explicitly",
1242+ })
1243+ }
1244+ }
1245+ return attachments
1246+}
1247+
1248+func (s *Service) removeRouteContainersForCleanup(ctx context.Context, log zerowrap.Logger, containers []*domain.Container, report *domain.CleanupReport) {
1249+ for _, c := range containers {
1250+ s.stopRouteLogCollectionForCleanup(log, c, report)
1251+ s.stopRouteContainerForCleanup(ctx, log, c, report)
1252+ if err := s.runtime.RemoveContainer(ctx, c.ID, true); err != nil {
1253+ report.PartialFailures = append(report.PartialFailures, domain.CleanupFailure{Action: "remove", Kind: "container", ID: c.ID, Name: c.Name, Error: err.Error()})
1254+ log.Warn().Err(err).Str(zerowrap.FieldEntityID, c.ID).Msg("failed to remove removed route container")
1255+ continue
1256+ }
1257+ report.RemovedContainers = append(report.RemovedContainers, domain.CleanupContainer{ID: c.ID, Name: c.Name, Image: c.Image, Status: c.Status})
1258+ }
1259+}
1260+
1261+func (s *Service) stopRouteLogCollectionForCleanup(log zerowrap.Logger, c *domain.Container, report *domain.CleanupReport) {
1262+ if s.logWriter == nil {
1263+ return
1264+ }
1265+ if err := s.logWriter.StopLogging(c.ID); err != nil {
1266+ report.PartialFailures = append(report.PartialFailures, domain.CleanupFailure{Action: "stop_logging", Kind: "container", ID: c.ID, Name: c.Name, Error: err.Error()})
1267+ log.Warn().Err(err).Str(zerowrap.FieldEntityID, c.ID).Msg("failed to stop removed route log collection")
1268+ }
1269+}
1270+
1271+func (s *Service) stopRouteContainerForCleanup(ctx context.Context, log zerowrap.Logger, c *domain.Container, report *domain.CleanupReport) {
1272+ if c.Status != string(domain.ContainerStatusRunning) {
1273+ return
1274+ }
1275+ if err := s.runtime.StopContainer(ctx, c.ID); err != nil {
1276+ report.PartialFailures = append(report.PartialFailures, domain.CleanupFailure{Action: "stop", Kind: "container", ID: c.ID, Name: c.Name, Error: err.Error()})
1277+ log.Warn().Err(err).Str(zerowrap.FieldEntityID, c.ID).Msg("failed to stop removed route container")
1278+ }
1279+}
1280+
1281+func isManagedRouteContainerForDomain(c *domain.Container, domainName string) bool {
1282+ if c == nil || c.Labels == nil || c.Labels[domain.LabelManaged] != "true" {
1283+ return false
1284+ }
1285+ if c.Labels[domain.LabelAttachment] == "true" {
1286+ return false
1287+ }
1288+ return c.Labels[domain.LabelRoute] == domainName || c.Labels[domain.LabelDomain] == domainName || c.Name == fmt.Sprintf("gordon-%s", domainName)
1289+}
1290+
1291+func isPreservedAttachmentForDomain(c *domain.Container, domainName string) bool {
1292+ if c == nil || c.Labels == nil || c.Labels[domain.LabelManaged] != "true" {
1293+ return false
1294+ }
1295+ return c.Labels[domain.LabelAttachment] == "true" && c.Labels[domain.LabelAttachedTo] == domainName
1296+}
1297+
1298+func (s *Service) proxyCacheInvalidator() out.ProxyCacheInvalidator {
1299+ s.mu.RLock()
1300+ defer s.mu.RUnlock()
1301+ return s.cacheInvalidator
1302+}
1303+
1304+func (s *Service) clearRouteContainerTracking(ctx context.Context, domainName string) {
1305+ s.mu.Lock()
1306+ removed := false
1307+ if _, exists := s.containers[domainName]; exists {
1308+ delete(s.containers, domainName)
1309+ if s.managedCount > 0 {
1310+ s.managedCount--
1311+ }
1312+ removed = true
1313+ }
1314+ s.mu.Unlock()
1315+
1316+ if removed && s.metrics != nil {
1317+ s.metrics.ManagedContainers.Add(ctx, -1)
1318+ }
1319+}
1320+
11621321 // Remove removes a container.
11631322 func (s *Service) Remove(ctx context.Context, containerID string, force bool) error {
11641323 ctx = zerowrap.CtxWithFields(ctx, map[string]any{
@@ -1351,6 +1510,139 @@ func (s *Service) ListAttachments(ctx context.Context, domainName string) []doma
13511510 return s.getAttachmentsForDomain(ctx, domainName)
13521511 }
13531512
1513+// ListOrphanedAttachments returns running attachment containers no longer configured.
1514+func (s *Service) ListOrphanedAttachments(ctx context.Context) ([]domain.CleanupAttachment, error) {
1515+ containers, err := s.runtime.ListContainers(ctx, true)
1516+ if err != nil {
1517+ return nil, fmt.Errorf("failed to list containers with runtime for orphaned attachments: %w", err)
1518+ }
1519+ return s.orphanedAttachmentsFromContainers(containers), nil
1520+}
1521+
1522+// CleanupOrphanedAttachments optionally stops/removes orphaned attachment containers.
1523+// When owner is non-empty, cleanup is scoped to that attachment owner.
1524+func (s *Service) CleanupOrphanedAttachments(ctx context.Context, owner string, stop bool) (*domain.CleanupReport, error) {
1525+ ctx = zerowrap.CtxWithFields(ctx, map[string]any{
1526+ zerowrap.FieldLayer: "usecase",
1527+ zerowrap.FieldUseCase: "CleanupOrphanedAttachments",
1528+ "owner": owner,
1529+ })
1530+ log := zerowrap.FromCtx(ctx)
1531+ report := &domain.CleanupReport{Domain: owner}
1532+
1533+ containers, err := s.runtime.ListContainers(ctx, true)
1534+ if err != nil {
1535+ return report, fmt.Errorf("failed to list containers with runtime for orphaned attachment cleanup: %w", err)
1536+ }
1537+ orphans := filterAttachmentContainersByOwner(s.orphanedAttachmentContainers(containers), owner)
1538+ if !stop {
1539+ report.PreservedAttachments = cleanupAttachmentsFromContainers(orphans)
1540+ if len(report.PreservedAttachments) > 0 {
1541+ report.Hints = append(report.Hints, "orphaned attachments preserved; rerun with --stop to stop/remove containers while preserving volumes")
1542+ }
1543+ return report, nil
1544+ }
1545+
1546+ for _, c := range orphans {
1547+ s.stopRouteLogCollectionForCleanup(log, c, report)
1548+ s.stopRouteContainerForCleanup(ctx, log, c, report)
1549+ if err := s.runtime.RemoveContainer(ctx, c.ID, true); err != nil {
1550+ report.PartialFailures = append(report.PartialFailures, domain.CleanupFailure{Action: "remove", Kind: "attachment", ID: c.ID, Name: c.Name, Error: err.Error()})
1551+ log.Warn().Err(err).Str(zerowrap.FieldEntityID, c.ID).Msg("failed to remove orphaned attachment")
1552+ continue
1553+ }
1554+ report.RemovedContainers = append(report.RemovedContainers, domain.CleanupContainer{ID: c.ID, Name: c.Name, Image: attachmentImage(c), Status: c.Status})
1555+ s.untrackAttachmentContainer(c.Labels[domain.LabelAttachedTo], c.ID)
1556+ }
1557+ if len(report.RemovedContainers) > 0 {
1558+ report.Hints = append(report.Hints, "attachment volumes and data were preserved")
1559+ }
1560+ if len(report.PartialFailures) > 0 {
1561+ report.Warnings = append(report.Warnings, "attachment cleanup completed with partial failures; manual follow-up may be required")
1562+ }
1563+ return report, nil
1564+}
1565+
1566+func (s *Service) orphanedAttachmentsFromContainers(containers []*domain.Container) []domain.CleanupAttachment {
1567+ return cleanupAttachmentsFromContainers(s.orphanedAttachmentContainers(containers))
1568+}
1569+
1570+func (s *Service) orphanedAttachmentContainers(containers []*domain.Container) []*domain.Container {
1571+ var out []*domain.Container
1572+ for _, c := range containers {
1573+ if !isManagedAttachment(c) {
1574+ continue
1575+ }
1576+ owner := c.Labels[domain.LabelAttachedTo]
1577+ image := attachmentImage(c)
1578+ if owner == "" || image == "" || !s.isAttachmentConfiguredForOwner(owner, image) {
1579+ out = append(out, c)
1580+ }
1581+ }
1582+ return out
1583+}
1584+
1585+func filterAttachmentContainersByOwner(containers []*domain.Container, owner string) []*domain.Container {
1586+ if owner == "" {
1587+ return containers
1588+ }
1589+ var out []*domain.Container
1590+ for _, c := range containers {
1591+ if c != nil && c.Labels != nil && strings.EqualFold(c.Labels[domain.LabelAttachedTo], owner) {
1592+ out = append(out, c)
1593+ }
1594+ }
1595+ return out
1596+}
1597+
1598+func cleanupAttachmentsFromContainers(containers []*domain.Container) []domain.CleanupAttachment {
1599+ attachments := make([]domain.CleanupAttachment, 0, len(containers))
1600+ for _, c := range containers {
1601+ attachments = append(attachments, domain.CleanupAttachment{
1602+ Name: c.Name,
1603+ Image: attachmentImage(c),
1604+ ContainerID: c.ID,
1605+ Status: c.Status,
1606+ Owner: c.Labels[domain.LabelAttachedTo],
1607+ Reason: "attachment container is no longer configured",
1608+ })
1609+ }
1610+ return attachments
1611+}
1612+
1613+func isManagedAttachment(c *domain.Container) bool {
1614+ return c != nil && c.Status == string(domain.ContainerStatusRunning) && c.Labels != nil && c.Labels[domain.LabelManaged] == "true" && c.Labels[domain.LabelAttachment] == "true"
1615+}
1616+
1617+func attachmentImage(c *domain.Container) string {
1618+ if c == nil {
1619+ return ""
1620+ }
1621+ if c.Labels != nil && c.Labels[domain.LabelImage] != "" {
1622+ return c.Labels[domain.LabelImage]
1623+ }
1624+ return c.Image
1625+}
1626+
1627+func (s *Service) isAttachmentConfiguredForOwner(owner, image string) bool {
1628+ return slices.Contains(s.resolveAttachmentsForDomain(owner), image)
1629+}
1630+
1631+func (s *Service) untrackAttachmentContainer(owner, containerID string) {
1632+ s.mu.Lock()
1633+ defer s.mu.Unlock()
1634+ ids := s.attachments[owner]
1635+ for i, id := range ids {
1636+ if id == containerID {
1637+ s.attachments[owner] = slices.Delete(ids, i, i+1)
1638+ break
1639+ }
1640+ }
1641+ if len(s.attachments[owner]) == 0 {
1642+ delete(s.attachments, owner)
1643+ }
1644+}
1645+
13541646 // ListNetworks returns Gordon-managed networks.
13551647 func (s *Service) ListNetworks(ctx context.Context) ([]*domain.NetworkInfo, error) {
13561648 s.mu.RLock()
M internal/usecase/container/service_test.go +342 -0
@@ -4418,3 +4418,345 @@ func TestService_BuildContainerConfig_SetsRestartPolicyAlways(t *testing.T) {
44184418
44194419 assert.Equal(t, domain.RestartPolicyAlways, cfg.RestartPolicy)
44204420 }
4421+
4422+func TestService_ReconcileRemovedRoute_RemovesOnlyMainRouteContainers(t *testing.T) {
4423+ ctx := testContext()
4424+ runtime := mocks.NewMockContainerRuntime(t)
4425+ svc := NewService(runtime, nil, mocks.NewMockEventPublisher(t), nil, Config{}, nil)
4426+
4427+ mainContainer := &domain.Container{
4428+ ID: "route-container",
4429+ Name: "gordon-app.example.com",
4430+ Status: string(domain.ContainerStatusRunning),
4431+ Labels: map[string]string{
4432+ domain.LabelManaged: "true",
4433+ domain.LabelRoute: "app.example.com",
4434+ domain.LabelDomain: "app.example.com",
4435+ },
4436+ }
4437+ attachment := &domain.Container{
4438+ ID: "attachment-container",
4439+ Name: "gordon-app-example-com-postgres",
4440+ Status: string(domain.ContainerStatusRunning),
4441+ Labels: map[string]string{
4442+ domain.LabelManaged: "true",
4443+ domain.LabelAttachment: "true",
4444+ domain.LabelAttachedTo: "app.example.com",
4445+ },
4446+ }
4447+
4448+ svc.mu.Lock()
4449+ svc.containers["app.example.com"] = mainContainer
4450+ svc.attachments["app.example.com"] = []string{attachment.ID}
4451+ svc.managedCount = 1
4452+ svc.mu.Unlock()
4453+
4454+ runtime.EXPECT().ListContainers(mock.Anything, true).Return([]*domain.Container{mainContainer, attachment}, nil).Once()
4455+ runtime.EXPECT().StopContainer(mock.Anything, mainContainer.ID).Return(nil).Once()
4456+ runtime.EXPECT().RemoveContainer(mock.Anything, mainContainer.ID, true).Return(nil).Once()
4457+
4458+ report, err := svc.ReconcileRemovedRoute(ctx, "app.example.com")
4459+ require.NoError(t, err)
4460+ require.NotNil(t, report)
4461+
4462+ assert.Equal(t, "app.example.com", report.Domain)
4463+ require.Len(t, report.RemovedContainers, 1)
4464+ assert.Equal(t, mainContainer.ID, report.RemovedContainers[0].ID)
4465+ require.Len(t, report.PreservedAttachments, 1)
4466+ assert.Equal(t, attachment.ID, report.PreservedAttachments[0].ContainerID)
4467+ assert.Contains(t, report.Hints, "attachments preserved; remove or purge attachments explicitly if they are no longer needed")
4468+
4469+ _, exists := svc.Get(ctx, "app.example.com")
4470+ assert.False(t, exists, "removed route must be cleared from in-memory tracking so monitor cannot restart it")
4471+
4472+ svc.mu.RLock()
4473+ _, attachmentsTracked := svc.attachments["app.example.com"]
4474+ svc.mu.RUnlock()
4475+ assert.True(t, attachmentsTracked, "route cleanup preserves attachment tracking for follow-up cleanup")
4476+}
4477+
4478+func TestService_ReconcileRemovedRoute_IsIdempotentWhenNoContainerExists(t *testing.T) {
4479+ ctx := testContext()
4480+ runtime := mocks.NewMockContainerRuntime(t)
4481+ svc := NewService(runtime, nil, mocks.NewMockEventPublisher(t), nil, Config{}, nil)
4482+
4483+ runtime.EXPECT().ListContainers(mock.Anything, true).Return([]*domain.Container{}, nil).Once()
4484+
4485+ report, err := svc.ReconcileRemovedRoute(ctx, "missing.example.com")
4486+ require.NoError(t, err)
4487+ require.NotNil(t, report)
4488+
4489+ assert.Equal(t, "missing.example.com", report.Domain)
4490+ assert.Empty(t, report.RemovedContainers)
4491+ assert.Contains(t, report.Warnings, "no active route container found for removed route")
4492+}
4493+
4494+func TestService_ReconcileRemovedRoute_CanonicalizesDomainBeforeCleanup(t *testing.T) {
4495+ ctx := testContext()
4496+ runtime := mocks.NewMockContainerRuntime(t)
4497+ svc := NewService(runtime, nil, mocks.NewMockEventPublisher(t), nil, Config{}, nil)
4498+
4499+ mainContainer := &domain.Container{
4500+ ID: "route-container",
4501+ Name: "gordon-app.example.com",
4502+ Status: string(domain.ContainerStatusRunning),
4503+ Labels: map[string]string{
4504+ domain.LabelManaged: "true",
4505+ domain.LabelRoute: "app.example.com",
4506+ domain.LabelDomain: "app.example.com",
4507+ },
4508+ }
4509+ svc.mu.Lock()
4510+ svc.containers["app.example.com"] = mainContainer
4511+ svc.managedCount = 1
4512+ svc.mu.Unlock()
4513+
4514+ runtime.EXPECT().ListContainers(mock.Anything, true).Return([]*domain.Container{mainContainer}, nil).Once()
4515+ runtime.EXPECT().StopContainer(mock.Anything, mainContainer.ID).Return(nil).Once()
4516+ runtime.EXPECT().RemoveContainer(mock.Anything, mainContainer.ID, true).Return(nil).Once()
4517+
4518+ report, err := svc.ReconcileRemovedRoute(ctx, "App.EXAMPLE.com")
4519+ require.NoError(t, err)
4520+ require.NotNil(t, report)
4521+ assert.Equal(t, "app.example.com", report.Domain)
4522+ require.Len(t, report.RemovedContainers, 1)
4523+
4524+ _, exists := svc.Get(ctx, "app.example.com")
4525+ assert.False(t, exists)
4526+}
4527+
4528+func TestService_ReconcileRemovedRoute_StopsLogCollectionForRemovedContainer(t *testing.T) {
4529+ ctx := testContext()
4530+ runtime := mocks.NewMockContainerRuntime(t)
4531+ logWriter := mocks.NewMockContainerLogWriter(t)
4532+ svc := NewService(runtime, nil, mocks.NewMockEventPublisher(t), logWriter, Config{}, nil)
4533+
4534+ mainContainer := &domain.Container{
4535+ ID: "route-container",
4536+ Name: "gordon-app.example.com",
4537+ Status: string(domain.ContainerStatusRunning),
4538+ Labels: map[string]string{
4539+ domain.LabelManaged: "true",
4540+ domain.LabelRoute: "app.example.com",
4541+ },
4542+ }
4543+
4544+ runtime.EXPECT().ListContainers(mock.Anything, true).Return([]*domain.Container{mainContainer}, nil).Once()
4545+ logWriter.EXPECT().StopLogging(mainContainer.ID).Return(nil).Once()
4546+ runtime.EXPECT().StopContainer(mock.Anything, mainContainer.ID).Return(nil).Once()
4547+ runtime.EXPECT().RemoveContainer(mock.Anything, mainContainer.ID, true).Return(nil).Once()
4548+
4549+ _, err := svc.ReconcileRemovedRoute(ctx, "app.example.com")
4550+ require.NoError(t, err)
4551+}
4552+
4553+func TestService_ReconcileRemovedRoute_InvalidatesProxyCacheAndMetric(t *testing.T) {
4554+ ctx := testContext()
4555+ runtime := mocks.NewMockContainerRuntime(t)
4556+ eventBus := mocks.NewMockEventPublisher(t)
4557+ cacheInvalidator := mocks.NewMockProxyCacheInvalidator(t)
4558+ svc := NewService(runtime, nil, eventBus, nil, Config{}, nil)
4559+
4560+ metrics, reader := setupMetricsTest(t)
4561+ svc.SetMetrics(metrics)
4562+
4563+ mainContainer := &domain.Container{
4564+ ID: "route-container",
4565+ Name: "gordon-app.example.com",
4566+ Status: string(domain.ContainerStatusRunning),
4567+ Labels: map[string]string{
4568+ domain.LabelManaged: "true",
4569+ domain.LabelRoute: "app.example.com",
4570+ },
4571+ }
4572+ eventBus.EXPECT().Publish(domain.EventContainerDeployed, mock.AnythingOfType("*domain.ContainerEventPayload")).Return(nil).Once()
4573+ svc.activateDeployedContainer(ctx, "app.example.com", mainContainer)
4574+ svc.SetProxyCacheInvalidator(cacheInvalidator)
4575+
4576+ runtime.EXPECT().ListContainers(mock.Anything, true).Return([]*domain.Container{mainContainer}, nil).Once()
4577+ cacheInvalidator.EXPECT().InvalidateTarget(mock.Anything, "app.example.com").Return().Once()
4578+ runtime.EXPECT().StopContainer(mock.Anything, mainContainer.ID).Return(nil).Once()
4579+ runtime.EXPECT().RemoveContainer(mock.Anything, mainContainer.ID, true).Return(nil).Once()
4580+
4581+ _, err := svc.ReconcileRemovedRoute(ctx, "app.example.com")
4582+ require.NoError(t, err)
4583+
4584+ value, _, _ := managedMetricState(t, reader)
4585+ assert.Equal(t, int64(0), value)
4586+}
4587+
4588+func TestService_ListOrphanedAttachments_DetectsRunningAttachmentNoLongerConfigured(t *testing.T) {
4589+ ctx := testContext()
4590+ runtime := mocks.NewMockContainerRuntime(t)
4591+ svc := NewService(runtime, nil, mocks.NewMockEventPublisher(t), nil, Config{Attachments: map[string][]string{
4592+ "app.example.com": {"redis:7"},
4593+ }}, nil)
4594+
4595+ orphan := &domain.Container{
4596+ ID: "postgres-1",
4597+ Name: "gordon-app-example-com-postgres",
4598+ Image: "postgres:16",
4599+ Status: string(domain.ContainerStatusRunning),
4600+ Labels: map[string]string{
4601+ domain.LabelManaged: "true",
4602+ domain.LabelAttachment: "true",
4603+ domain.LabelAttachedTo: "app.example.com",
4604+ domain.LabelImage: "postgres:16",
4605+ },
4606+ }
4607+ configured := &domain.Container{
4608+ ID: "redis-1",
4609+ Name: "gordon-app-example-com-redis",
4610+ Image: "redis:7",
4611+ Status: string(domain.ContainerStatusRunning),
4612+ Labels: map[string]string{
4613+ domain.LabelManaged: "true",
4614+ domain.LabelAttachment: "true",
4615+ domain.LabelAttachedTo: "app.example.com",
4616+ domain.LabelImage: "redis:7",
4617+ },
4618+ }
4619+
4620+ runtime.EXPECT().ListContainers(mock.Anything, true).Return([]*domain.Container{orphan, configured}, nil).Once()
4621+
4622+ orphans, err := svc.ListOrphanedAttachments(ctx)
4623+ require.NoError(t, err)
4624+ require.Len(t, orphans, 1)
4625+ assert.Equal(t, orphan.ID, orphans[0].ContainerID)
4626+ assert.Equal(t, "postgres:16", orphans[0].Image)
4627+ assert.Contains(t, orphans[0].Reason, "no longer configured")
4628+}
4629+
4630+func TestService_CleanupOrphanedAttachments_DryRunPreservesRunningContainer(t *testing.T) {
4631+ ctx := testContext()
4632+ runtime := mocks.NewMockContainerRuntime(t)
4633+ svc := NewService(runtime, nil, mocks.NewMockEventPublisher(t), nil, Config{}, nil)
4634+ orphan := &domain.Container{
4635+ ID: "postgres-1",
4636+ Name: "gordon-app-example-com-postgres",
4637+ Image: "postgres:16",
4638+ Status: string(domain.ContainerStatusRunning),
4639+ Labels: map[string]string{
4640+ domain.LabelManaged: "true",
4641+ domain.LabelAttachment: "true",
4642+ domain.LabelAttachedTo: "app.example.com",
4643+ domain.LabelImage: "postgres:16",
4644+ },
4645+ }
4646+ runtime.EXPECT().ListContainers(mock.Anything, true).Return([]*domain.Container{orphan}, nil).Once()
4647+
4648+ report, err := svc.CleanupOrphanedAttachments(ctx, "", false)
4649+ require.NoError(t, err)
4650+ require.NotNil(t, report)
4651+ require.Len(t, report.PreservedAttachments, 1)
4652+ assert.Equal(t, orphan.ID, report.PreservedAttachments[0].ContainerID)
4653+ runtime.AssertNotCalled(t, "StopContainer", mock.Anything, orphan.ID)
4654+ runtime.AssertNotCalled(t, "RemoveContainer", mock.Anything, orphan.ID, mock.Anything)
4655+}
4656+
4657+func TestService_CleanupOrphanedAttachments_StopRemovesContainerButPreservesData(t *testing.T) {
4658+ ctx := testContext()
4659+ runtime := mocks.NewMockContainerRuntime(t)
4660+ logWriter := mocks.NewMockContainerLogWriter(t)
4661+ svc := NewService(runtime, nil, mocks.NewMockEventPublisher(t), logWriter, Config{}, nil)
4662+ orphan := &domain.Container{
4663+ ID: "postgres-1",
4664+ Name: "gordon-app-example-com-postgres",
4665+ Image: "postgres:16",
4666+ Status: string(domain.ContainerStatusRunning),
4667+ Labels: map[string]string{
4668+ domain.LabelManaged: "true",
4669+ domain.LabelAttachment: "true",
4670+ domain.LabelAttachedTo: "app.example.com",
4671+ domain.LabelImage: "postgres:16",
4672+ },
4673+ }
4674+ svc.mu.Lock()
4675+ svc.attachments["app.example.com"] = []string{orphan.ID}
4676+ svc.mu.Unlock()
4677+
4678+ runtime.EXPECT().ListContainers(mock.Anything, true).Return([]*domain.Container{orphan}, nil).Once()
4679+ logWriter.EXPECT().StopLogging(orphan.ID).Return(nil).Once()
4680+ runtime.EXPECT().StopContainer(mock.Anything, orphan.ID).Return(nil).Once()
4681+ runtime.EXPECT().RemoveContainer(mock.Anything, orphan.ID, true).Return(nil).Once()
4682+
4683+ report, err := svc.CleanupOrphanedAttachments(ctx, "", true)
4684+ require.NoError(t, err)
4685+ require.NotNil(t, report)
4686+ require.Len(t, report.RemovedContainers, 1)
4687+ assert.Equal(t, orphan.ID, report.RemovedContainers[0].ID)
4688+ assert.Contains(t, report.Hints, "attachment volumes and data were preserved")
4689+
4690+ svc.mu.RLock()
4691+ ids := svc.attachments["app.example.com"]
4692+ svc.mu.RUnlock()
4693+ assert.Empty(t, ids)
4694+}
4695+
4696+func TestService_ListOrphanedAttachments_IgnoresStoppedAttachments(t *testing.T) {
4697+ ctx := testContext()
4698+ runtime := mocks.NewMockContainerRuntime(t)
4699+ svc := NewService(runtime, nil, mocks.NewMockEventPublisher(t), nil, Config{}, nil)
4700+ stopped := &domain.Container{
4701+ ID: "postgres-1",
4702+ Name: "gordon-app-example-com-postgres",
4703+ Image: "postgres:16",
4704+ Status: string(domain.ContainerStatusExited),
4705+ Labels: map[string]string{
4706+ domain.LabelManaged: "true",
4707+ domain.LabelAttachment: "true",
4708+ domain.LabelAttachedTo: "app.example.com",
4709+ domain.LabelImage: "postgres:16",
4710+ },
4711+ }
4712+ runtime.EXPECT().ListContainers(mock.Anything, true).Return([]*domain.Container{stopped}, nil).Once()
4713+
4714+ orphans, err := svc.ListOrphanedAttachments(ctx)
4715+ require.NoError(t, err)
4716+ assert.Empty(t, orphans)
4717+}
4718+
4719+func TestService_ListOrphanedAttachments_IncludesOwnerAndHonorsGroupConfig(t *testing.T) {
4720+ ctx := testContext()
4721+ runtime := mocks.NewMockContainerRuntime(t)
4722+ svc := NewService(runtime, nil, mocks.NewMockEventPublisher(t), nil, Config{
4723+ NetworkIsolation: true,
4724+ NetworkGroups: map[string][]string{
4725+ "backend": {"app.example.com"},
4726+ },
4727+ Attachments: map[string][]string{
4728+ "backend": {"postgres:16"},
4729+ },
4730+ }, nil)
4731+ configuredByGroup := &domain.Container{
4732+ ID: "postgres-1",
4733+ Name: "gordon-app-example-com-postgres",
4734+ Image: "postgres:16",
4735+ Status: string(domain.ContainerStatusRunning),
4736+ Labels: map[string]string{
4737+ domain.LabelManaged: "true",
4738+ domain.LabelAttachment: "true",
4739+ domain.LabelAttachedTo: "app.example.com",
4740+ domain.LabelImage: "postgres:16",
4741+ },
4742+ }
4743+ orphan := &domain.Container{
4744+ ID: "redis-1",
4745+ Name: "gordon-app-example-com-redis",
4746+ Image: "redis:7",
4747+ Status: string(domain.ContainerStatusRunning),
4748+ Labels: map[string]string{
4749+ domain.LabelManaged: "true",
4750+ domain.LabelAttachment: "true",
4751+ domain.LabelAttachedTo: "app.example.com",
4752+ domain.LabelImage: "redis:7",
4753+ },
4754+ }
4755+ runtime.EXPECT().ListContainers(mock.Anything, true).Return([]*domain.Container{configuredByGroup, orphan}, nil).Once()
4756+
4757+ orphans, err := svc.ListOrphanedAttachments(ctx)
4758+ require.NoError(t, err)
4759+ require.Len(t, orphans, 1)
4760+ assert.Equal(t, "redis-1", orphans[0].ContainerID)
4761+ assert.Equal(t, "app.example.com", orphans[0].Owner)
4762+}

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