fix(cli): tighten route diagnose resource scoping
- commit
- e8ee147b04062e8bab4b15fbe0f8d815be0f76f5
- author
- brice · · 2026-05-22 07:00 UTC
- parent
- 7b9923f
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
2 files · +167 -6
M internal/adapters/in/cli/routes.go +86 -6
| @@ -1125,8 +1125,8 @@ func buildRouteDiagnosis(ctx context.Context, cp ControlPlane, domainName string | |||
| 1125 | 1125 | loadRouteDiagnosisConfig(ctx, cp, domainName, diag) | |
| 1126 | 1126 | loadRouteDiagnosisRuntime(ctx, cp, domainName, diag) | |
| 1127 | 1127 | loadRouteDiagnosisHealth(ctx, cp, domainName, diag) | |
| 1128 | - | loadRouteDiagnosisVolumes(ctx, cp, domainName, diag) | |
| 1129 | 1128 | loadRouteDiagnosisOrphanedAttachments(ctx, cp, domainName, diag) | |
| 1129 | + | loadRouteDiagnosisVolumes(ctx, cp, domainName, diag) | |
| 1130 | 1130 | finalizeRouteDiagnosis(diag) | |
| 1131 | 1131 | return diag, nil | |
| 1132 | 1132 | } | |
| @@ -1166,7 +1166,8 @@ func loadRouteDiagnosisHealth(ctx context.Context, cp ControlPlane, domainName s | |||
| 1166 | 1166 | ||
| 1167 | 1167 | func loadRouteDiagnosisVolumes(ctx context.Context, cp ControlPlane, domainName string, diag *routeDiagnosis) { | |
| 1168 | 1168 | if volumes, err := cp.ListVolumes(ctx); err == nil { | |
| 1169 | - | diag.Volumes = volumesForRouteDiagnosis(volumes, domainName) | |
| 1169 | + | scope := newRouteResourceScope(domainName, diag.Runtime, diag.OrphanedAttachments) | |
| 1170 | + | diag.Volumes = volumesForRouteDiagnosis(volumes, scope) | |
| 1170 | 1171 | } | |
| 1171 | 1172 | } | |
| 1172 | 1173 | ||
| @@ -1194,17 +1195,75 @@ func finalizeRouteDiagnosis(diag *routeDiagnosis) { | |||
| 1194 | 1195 | } | |
| 1195 | 1196 | } | |
| 1196 | 1197 | ||
| 1197 | - | func volumesForRouteDiagnosis(volumes []dto.Volume, domainName string) []dto.Volume { | |
| 1198 | - | needle := strings.ReplaceAll(strings.ToLower(domainName), ".", "-") | |
| 1198 | + | type routeResourceScope struct { | |
| 1199 | + | containerNames map[string]struct{} | |
| 1200 | + | volumePrefixes []string | |
| 1201 | + | } | |
| 1202 | + | ||
| 1203 | + | func newRouteResourceScope(domainName string, runtime *remote.RouteInfo, orphaned []domain.CleanupAttachment) routeResourceScope { | |
| 1204 | + | scope := routeResourceScope{ | |
| 1205 | + | containerNames: make(map[string]struct{}), | |
| 1206 | + | volumePrefixes: []string{"gordon-" + strings.ReplaceAll(domainName, ".", "-") + "-"}, | |
| 1207 | + | } | |
| 1208 | + | for _, name := range []string{ | |
| 1209 | + | fmt.Sprintf("gordon-%s", domainName), | |
| 1210 | + | fmt.Sprintf("gordon-%s-new", domainName), | |
| 1211 | + | fmt.Sprintf("gordon-%s-next", domainName), | |
| 1212 | + | } { | |
| 1213 | + | scope.containerNames[name] = struct{}{} | |
| 1214 | + | } | |
| 1215 | + | if runtime != nil { | |
| 1216 | + | for _, attachment := range runtime.Attachments { | |
| 1217 | + | for _, name := range routeAttachmentContainerNameCandidates(domainName, attachment.Name) { | |
| 1218 | + | scope.containerNames[name] = struct{}{} | |
| 1219 | + | scope.volumePrefixes = append(scope.volumePrefixes, "gordon-"+name+"-") | |
| 1220 | + | } | |
| 1221 | + | } | |
| 1222 | + | } | |
| 1223 | + | for _, attachment := range orphaned { | |
| 1224 | + | if attachment.Name == "" { | |
| 1225 | + | continue | |
| 1226 | + | } | |
| 1227 | + | scope.containerNames[attachment.Name] = struct{}{} | |
| 1228 | + | scope.volumePrefixes = append(scope.volumePrefixes, "gordon-"+attachment.Name+"-") | |
| 1229 | + | } | |
| 1230 | + | return scope | |
| 1231 | + | } | |
| 1232 | + | ||
| 1233 | + | func routeAttachmentContainerNameCandidates(domainName, serviceName string) []string { | |
| 1234 | + | if serviceName == "" { | |
| 1235 | + | return nil | |
| 1236 | + | } | |
| 1237 | + | return []string{ | |
| 1238 | + | fmt.Sprintf("gordon-%s-%s", domain.SanitizeDomainForContainer(domainName), serviceName), | |
| 1239 | + | fmt.Sprintf("gordon-%s-%s", domain.SanitizeDomainForContainerLegacy(domainName), serviceName), | |
| 1240 | + | } | |
| 1241 | + | } | |
| 1242 | + | ||
| 1243 | + | func volumesForRouteDiagnosis(volumes []dto.Volume, scope routeResourceScope) []dto.Volume { | |
| 1199 | 1244 | var out []dto.Volume | |
| 1200 | 1245 | for _, volume := range volumes { | |
| 1201 | - | if strings.Contains(strings.ToLower(volume.Name), needle) { | |
| 1246 | + | if scope.matchesVolume(volume) { | |
| 1202 | 1247 | out = append(out, volume) | |
| 1203 | 1248 | } | |
| 1204 | 1249 | } | |
| 1205 | 1250 | return out | |
| 1206 | 1251 | } | |
| 1207 | 1252 | ||
| 1253 | + | func (s routeResourceScope) matchesVolume(volume dto.Volume) bool { | |
| 1254 | + | for _, containerName := range volume.Containers { | |
| 1255 | + | if _, ok := s.containerNames[containerName]; ok { | |
| 1256 | + | return true | |
| 1257 | + | } | |
| 1258 | + | } | |
| 1259 | + | for _, prefix := range s.volumePrefixes { | |
| 1260 | + | if strings.HasPrefix(volume.Name, prefix) { | |
| 1261 | + | return true | |
| 1262 | + | } | |
| 1263 | + | } | |
| 1264 | + | return false | |
| 1265 | + | } | |
| 1266 | + | ||
| 1208 | 1267 | func attachmentsForRouteDiagnosis(attachments []domain.CleanupAttachment, domainName string) []domain.CleanupAttachment { | |
| 1209 | 1268 | var out []domain.CleanupAttachment | |
| 1210 | 1269 | for _, attachment := range attachments { | |
| @@ -1239,7 +1298,28 @@ func writeRouteDiagnosisSummary(out io.Writer, diag *routeDiagnosis) error { | |||
| 1239 | 1298 | if diag.Runtime == nil { | |
| 1240 | 1299 | return nil | |
| 1241 | 1300 | } | |
| 1242 | - | return cliWriteLine(out, cliRenderMeta("Container:", diag.Runtime.ContainerStatus+" "+shortContainerID(diag.Runtime.ContainerID))) | |
| 1301 | + | if err := cliWriteLine(out, cliRenderMeta("Container:", diag.Runtime.ContainerStatus+" "+shortContainerID(diag.Runtime.ContainerID))); err != nil { | |
| 1302 | + | return err | |
| 1303 | + | } | |
| 1304 | + | if len(diag.Runtime.Attachments) == 0 { | |
| 1305 | + | return nil | |
| 1306 | + | } | |
| 1307 | + | if err := cliWriteLine(out, cliRenderMeta("Active attachments:", "")); err != nil { | |
| 1308 | + | return err | |
| 1309 | + | } | |
| 1310 | + | for _, attachment := range diag.Runtime.Attachments { | |
| 1311 | + | label := attachment.Name | |
| 1312 | + | if label == "" { | |
| 1313 | + | label = attachment.ContainerID | |
| 1314 | + | } | |
| 1315 | + | if attachment.Status != "" { | |
| 1316 | + | label = fmt.Sprintf("%s (%s)", label, attachment.Status) | |
| 1317 | + | } | |
| 1318 | + | if err := cliWriteLine(out, " "+label); err != nil { | |
| 1319 | + | return err | |
| 1320 | + | } | |
| 1321 | + | } | |
| 1322 | + | return nil | |
| 1243 | 1323 | } | |
| 1244 | 1324 | ||
| 1245 | 1325 | func writeRouteDiagnosisLeftovers(out io.Writer, diag *routeDiagnosis) error { | |
| @@ -1125,8 +1125,8 @@ func buildRouteDiagnosis(ctx context.Context, cp ControlPlane, domainName string | |||||
| 1125 | loadRouteDiagnosisConfig(ctx, cp, domainName, diag) | 1125 | loadRouteDiagnosisConfig(ctx, cp, domainName, diag) | ||
| 1126 | loadRouteDiagnosisRuntime(ctx, cp, domainName, diag) | 1126 | loadRouteDiagnosisRuntime(ctx, cp, domainName, diag) | ||
| 1127 | loadRouteDiagnosisHealth(ctx, cp, domainName, diag) | 1127 | loadRouteDiagnosisHealth(ctx, cp, domainName, diag) | ||
| 1128 | - | loadRouteDiagnosisVolumes(ctx, cp, domainName, diag) | |||
| 1129 | loadRouteDiagnosisOrphanedAttachments(ctx, cp, domainName, diag) | 1128 | loadRouteDiagnosisOrphanedAttachments(ctx, cp, domainName, diag) | ||
| 1129 | + | loadRouteDiagnosisVolumes(ctx, cp, domainName, diag) | |||
| 1130 | finalizeRouteDiagnosis(diag) | 1130 | finalizeRouteDiagnosis(diag) | ||
| 1131 | return diag, nil | 1131 | return diag, nil | ||
| 1132 | } | 1132 | } | ||
| @@ -1166,7 +1166,8 @@ func loadRouteDiagnosisHealth(ctx context.Context, cp ControlPlane, domainName s | |||||
| 1166 | 1166 | ||||
| 1167 | func loadRouteDiagnosisVolumes(ctx context.Context, cp ControlPlane, domainName string, diag *routeDiagnosis) { | 1167 | func loadRouteDiagnosisVolumes(ctx context.Context, cp ControlPlane, domainName string, diag *routeDiagnosis) { | ||
| 1168 | if volumes, err := cp.ListVolumes(ctx); err == nil { | 1168 | if volumes, err := cp.ListVolumes(ctx); err == nil { | ||
| 1169 | - | diag.Volumes = volumesForRouteDiagnosis(volumes, domainName) | 1169 | + | scope := newRouteResourceScope(domainName, diag.Runtime, diag.OrphanedAttachments) |
| 1170 | + | diag.Volumes = volumesForRouteDiagnosis(volumes, scope) | |||
| 1170 | } | 1171 | } | ||
| 1171 | } | 1172 | } | ||
| 1172 | 1173 | ||||
| @@ -1194,17 +1195,75 @@ func finalizeRouteDiagnosis(diag *routeDiagnosis) { | |||||
| 1194 | } | 1195 | } | ||
| 1195 | } | 1196 | } | ||
| 1196 | 1197 | ||||
| 1197 | - | func volumesForRouteDiagnosis(volumes []dto.Volume, domainName string) []dto.Volume { | 1198 | + | type routeResourceScope struct { |
| 1198 | - | needle := strings.ReplaceAll(strings.ToLower(domainName), ".", "-") | 1199 | + | containerNames map[string]struct{} |
| 1200 | + | volumePrefixes []string | |||
| 1201 | + | } | |||
| 1202 | + | ||||
| 1203 | + | func newRouteResourceScope(domainName string, runtime *remote.RouteInfo, orphaned []domain.CleanupAttachment) routeResourceScope { | |||
| 1204 | + | scope := routeResourceScope{ | |||
| 1205 | + | containerNames: make(map[string]struct{}), | |||
| 1206 | + | volumePrefixes: []string{"gordon-" + strings.ReplaceAll(domainName, ".", "-") + "-"}, | |||
| 1207 | + | } | |||
| 1208 | + | for _, name := range []string{ | |||
| 1209 | + | fmt.Sprintf("gordon-%s", domainName), | |||
| 1210 | + | fmt.Sprintf("gordon-%s-new", domainName), | |||
| 1211 | + | fmt.Sprintf("gordon-%s-next", domainName), | |||
| 1212 | + | } { | |||
| 1213 | + | scope.containerNames[name] = struct{}{} | |||
| 1214 | + | } | |||
| 1215 | + | if runtime != nil { | |||
| 1216 | + | for _, attachment := range runtime.Attachments { | |||
| 1217 | + | for _, name := range routeAttachmentContainerNameCandidates(domainName, attachment.Name) { | |||
| 1218 | + | scope.containerNames[name] = struct{}{} | |||
| 1219 | + | scope.volumePrefixes = append(scope.volumePrefixes, "gordon-"+name+"-") | |||
| 1220 | + | } | |||
| 1221 | + | } | |||
| 1222 | + | } | |||
| 1223 | + | for _, attachment := range orphaned { | |||
| 1224 | + | if attachment.Name == "" { | |||
| 1225 | + | continue | |||
| 1226 | + | } | |||
| 1227 | + | scope.containerNames[attachment.Name] = struct{}{} | |||
| 1228 | + | scope.volumePrefixes = append(scope.volumePrefixes, "gordon-"+attachment.Name+"-") | |||
| 1229 | + | } | |||
| 1230 | + | return scope | |||
| 1231 | + | } | |||
| 1232 | + | ||||
| 1233 | + | func routeAttachmentContainerNameCandidates(domainName, serviceName string) []string { | |||
| 1234 | + | if serviceName == "" { | |||
| 1235 | + | return nil | |||
| 1236 | + | } | |||
| 1237 | + | return []string{ | |||
| 1238 | + | fmt.Sprintf("gordon-%s-%s", domain.SanitizeDomainForContainer(domainName), serviceName), | |||
| 1239 | + | fmt.Sprintf("gordon-%s-%s", domain.SanitizeDomainForContainerLegacy(domainName), serviceName), | |||
| 1240 | + | } | |||
| 1241 | + | } | |||
| 1242 | + | ||||
| 1243 | + | func volumesForRouteDiagnosis(volumes []dto.Volume, scope routeResourceScope) []dto.Volume { | |||
| 1199 | var out []dto.Volume | 1244 | var out []dto.Volume | ||
| 1200 | for _, volume := range volumes { | 1245 | for _, volume := range volumes { | ||
| 1201 | - | if strings.Contains(strings.ToLower(volume.Name), needle) { | 1246 | + | if scope.matchesVolume(volume) { |
| 1202 | out = append(out, volume) | 1247 | out = append(out, volume) | ||
| 1203 | } | 1248 | } | ||
| 1204 | } | 1249 | } | ||
| 1205 | return out | 1250 | return out | ||
| 1206 | } | 1251 | } | ||
| 1207 | 1252 | ||||
| 1253 | + | func (s routeResourceScope) matchesVolume(volume dto.Volume) bool { | |||
| 1254 | + | for _, containerName := range volume.Containers { | |||
| 1255 | + | if _, ok := s.containerNames[containerName]; ok { | |||
| 1256 | + | return true | |||
| 1257 | + | } | |||
| 1258 | + | } | |||
| 1259 | + | for _, prefix := range s.volumePrefixes { | |||
| 1260 | + | if strings.HasPrefix(volume.Name, prefix) { | |||
| 1261 | + | return true | |||
| 1262 | + | } | |||
| 1263 | + | } | |||
| 1264 | + | return false | |||
| 1265 | + | } | |||
| 1266 | + | ||||
| 1208 | func attachmentsForRouteDiagnosis(attachments []domain.CleanupAttachment, domainName string) []domain.CleanupAttachment { | 1267 | func attachmentsForRouteDiagnosis(attachments []domain.CleanupAttachment, domainName string) []domain.CleanupAttachment { | ||
| 1209 | var out []domain.CleanupAttachment | 1268 | var out []domain.CleanupAttachment | ||
| 1210 | for _, attachment := range attachments { | 1269 | for _, attachment := range attachments { | ||
| @@ -1239,7 +1298,28 @@ func writeRouteDiagnosisSummary(out io.Writer, diag *routeDiagnosis) error { | |||||
| 1239 | if diag.Runtime == nil { | 1298 | if diag.Runtime == nil { | ||
| 1240 | return nil | 1299 | return nil | ||
| 1241 | } | 1300 | } | ||
| 1242 | - | return cliWriteLine(out, cliRenderMeta("Container:", diag.Runtime.ContainerStatus+" "+shortContainerID(diag.Runtime.ContainerID))) | 1301 | + | if err := cliWriteLine(out, cliRenderMeta("Container:", diag.Runtime.ContainerStatus+" "+shortContainerID(diag.Runtime.ContainerID))); err != nil { |
| 1302 | + | return err | |||
| 1303 | + | } | |||
| 1304 | + | if len(diag.Runtime.Attachments) == 0 { | |||
| 1305 | + | return nil | |||
| 1306 | + | } | |||
| 1307 | + | if err := cliWriteLine(out, cliRenderMeta("Active attachments:", "")); err != nil { | |||
| 1308 | + | return err | |||
| 1309 | + | } | |||
| 1310 | + | for _, attachment := range diag.Runtime.Attachments { | |||
| 1311 | + | label := attachment.Name | |||
| 1312 | + | if label == "" { | |||
| 1313 | + | label = attachment.ContainerID | |||
| 1314 | + | } | |||
| 1315 | + | if attachment.Status != "" { | |||
| 1316 | + | label = fmt.Sprintf("%s (%s)", label, attachment.Status) | |||
| 1317 | + | } | |||
| 1318 | + | if err := cliWriteLine(out, " "+label); err != nil { | |||
| 1319 | + | return err | |||
| 1320 | + | } | |||
| 1321 | + | } | |||
| 1322 | + | return nil | |||
| 1243 | } | 1323 | } | ||
| 1244 | 1324 | ||||
| 1245 | func writeRouteDiagnosisLeftovers(out io.Writer, diag *routeDiagnosis) error { | 1325 | func writeRouteDiagnosisLeftovers(out io.Writer, diag *routeDiagnosis) error { | ||
M internal/adapters/in/cli/routes_test.go +81 -0
| @@ -899,6 +899,87 @@ func TestAttachmentsForRouteDiagnosisUsesOwner(t *testing.T) { | |||
| 899 | 899 | assert.Equal(t, "match", matched[0].ContainerID) | |
| 900 | 900 | } | |
| 901 | 901 | ||
| 902 | + | func TestBuildRouteDiagnosis_VolumesAreScopedToExactRouteContainers(t *testing.T) { | |
| 903 | + | cpMock := climocks.NewMockControlPlane(t) | |
| 904 | + | cpMock.EXPECT().GetRoute(context.Background(), "example.test").Return(&domain.Route{Domain: "example.test", Image: "site:latest"}, nil).Once() | |
| 905 | + | cpMock.EXPECT().ListRoutesWithDetails(context.Background()).Return([]remote.RouteInfo{{ | |
| 906 | + | Domain: "example.test", | |
| 907 | + | ContainerID: "route-1", | |
| 908 | + | ContainerStatus: "running", | |
| 909 | + | }}, nil).Once() | |
| 910 | + | cpMock.EXPECT().GetHealth(context.Background()).Return(nil, nil).Once() | |
| 911 | + | cpMock.EXPECT().ListVolumes(context.Background()).Return([]dto.Volume{ | |
| 912 | + | {Name: "gordon-example-test-data-docs", Containers: []string{"gordon-example.test"}}, | |
| 913 | + | {Name: "gordon-api-example-test-cache", Containers: []string{"gordon-api.example.test"}}, | |
| 914 | + | }, nil).Once() | |
| 915 | + | cpMock.EXPECT().ListOrphanedAttachments(context.Background()).Return(nil, nil).Once() | |
| 916 | + | ||
| 917 | + | diag, err := buildRouteDiagnosis(context.Background(), cpMock, "example.test") | |
| 918 | + | ||
| 919 | + | require.NoError(t, err) | |
| 920 | + | require.Len(t, diag.Volumes, 1) | |
| 921 | + | assert.Equal(t, "gordon-example-test-data-docs", diag.Volumes[0].Name) | |
| 922 | + | } | |
| 923 | + | ||
| 924 | + | func TestBuildRouteDiagnosis_IncludesAttachmentVolumesWithDoubleUnderscoreOwnerEncoding(t *testing.T) { | |
| 925 | + | cpMock := climocks.NewMockControlPlane(t) | |
| 926 | + | cpMock.EXPECT().GetRoute(context.Background(), "app.example.test").Return(&domain.Route{Domain: "app.example.test", Image: "app:latest"}, nil).Once() | |
| 927 | + | cpMock.EXPECT().ListRoutesWithDetails(context.Background()).Return([]remote.RouteInfo{{ | |
| 928 | + | Domain: "app.example.test", | |
| 929 | + | ContainerID: "route-1", | |
| 930 | + | ContainerStatus: "running", | |
| 931 | + | Attachments: []dto.Attachment{{ | |
| 932 | + | Name: "postgres", | |
| 933 | + | Image: "postgres:16", | |
| 934 | + | ContainerID: "attachment-1", | |
| 935 | + | Status: "running", | |
| 936 | + | }}, | |
| 937 | + | }}, nil).Once() | |
| 938 | + | cpMock.EXPECT().GetHealth(context.Background()).Return(nil, nil).Once() | |
| 939 | + | cpMock.EXPECT().ListVolumes(context.Background()).Return([]dto.Volume{ | |
| 940 | + | {Name: "gordon-gordon-app__example__test-postgres-var-lib-postgresql", Containers: []string{"gordon-app__example__test-postgres"}}, | |
| 941 | + | }, nil).Once() | |
| 942 | + | cpMock.EXPECT().ListOrphanedAttachments(context.Background()).Return(nil, nil).Once() | |
| 943 | + | ||
| 944 | + | diag, err := buildRouteDiagnosis(context.Background(), cpMock, "app.example.test") | |
| 945 | + | ||
| 946 | + | require.NoError(t, err) | |
| 947 | + | require.Len(t, diag.Volumes, 1) | |
| 948 | + | assert.Equal(t, "gordon-gordon-app__example__test-postgres-var-lib-postgresql", diag.Volumes[0].Name) | |
| 949 | + | } | |
| 950 | + | ||
| 951 | + | func TestWriteRouteDiagnosisText_ShowsActiveAttachmentsSeparatelyFromOrphans(t *testing.T) { | |
| 952 | + | var out bytes.Buffer | |
| 953 | + | diag := &routeDiagnosis{ | |
| 954 | + | Domain: "app.example.test", | |
| 955 | + | Configured: true, | |
| 956 | + | Route: &domain.Route{Domain: "app.example.test", Image: "app:latest"}, | |
| 957 | + | Runtime: &remote.RouteInfo{ | |
| 958 | + | Domain: "app.example.test", | |
| 959 | + | ContainerID: "route-1", | |
| 960 | + | ContainerStatus: "running", | |
| 961 | + | Attachments: []dto.Attachment{{ | |
| 962 | + | Name: "postgres", | |
| 963 | + | Image: "postgres:16", | |
| 964 | + | ContainerID: "attachment-1", | |
| 965 | + | Status: "running", | |
| 966 | + | }}, | |
| 967 | + | }, | |
| 968 | + | OrphanedAttachments: []domain.CleanupAttachment{{ | |
| 969 | + | Name: "old-postgres", | |
| 970 | + | ContainerID: "attachment-2", | |
| 971 | + | Owner: "app.example.test", | |
| 972 | + | }}, | |
| 973 | + | } | |
| 974 | + | ||
| 975 | + | err := writeRouteDiagnosisText(&out, diag) | |
| 976 | + | require.NoError(t, err) | |
| 977 | + | rendered := stripANSI(out.String()) | |
| 978 | + | assert.Contains(t, rendered, "Active attachments:") | |
| 979 | + | assert.Contains(t, rendered, "postgres (running)") | |
| 980 | + | assert.Contains(t, rendered, "Orphaned attachment: old-postgres") | |
| 981 | + | } | |
| 982 | + | ||
| 902 | 983 | func TestRunRoutePurge_AttachmentsForceIsScopedToRoute(t *testing.T) { | |
| 903 | 984 | cpMock := climocks.NewMockControlPlane(t) | |
| 904 | 985 | orphanedAttachments := []domain.CleanupAttachment{ | |
| @@ -899,6 +899,87 @@ func TestAttachmentsForRouteDiagnosisUsesOwner(t *testing.T) { | |||||
| 899 | assert.Equal(t, "match", matched[0].ContainerID) | 899 | assert.Equal(t, "match", matched[0].ContainerID) | ||
| 900 | } | 900 | } | ||
| 901 | 901 | ||||
| 902 | + | func TestBuildRouteDiagnosis_VolumesAreScopedToExactRouteContainers(t *testing.T) { | |||
| 903 | + | cpMock := climocks.NewMockControlPlane(t) | |||
| 904 | + | cpMock.EXPECT().GetRoute(context.Background(), "example.test").Return(&domain.Route{Domain: "example.test", Image: "site:latest"}, nil).Once() | |||
| 905 | + | cpMock.EXPECT().ListRoutesWithDetails(context.Background()).Return([]remote.RouteInfo{{ | |||
| 906 | + | Domain: "example.test", | |||
| 907 | + | ContainerID: "route-1", | |||
| 908 | + | ContainerStatus: "running", | |||
| 909 | + | }}, nil).Once() | |||
| 910 | + | cpMock.EXPECT().GetHealth(context.Background()).Return(nil, nil).Once() | |||
| 911 | + | cpMock.EXPECT().ListVolumes(context.Background()).Return([]dto.Volume{ | |||
| 912 | + | {Name: "gordon-example-test-data-docs", Containers: []string{"gordon-example.test"}}, | |||
| 913 | + | {Name: "gordon-api-example-test-cache", Containers: []string{"gordon-api.example.test"}}, | |||
| 914 | + | }, nil).Once() | |||
| 915 | + | cpMock.EXPECT().ListOrphanedAttachments(context.Background()).Return(nil, nil).Once() | |||
| 916 | + | ||||
| 917 | + | diag, err := buildRouteDiagnosis(context.Background(), cpMock, "example.test") | |||
| 918 | + | ||||
| 919 | + | require.NoError(t, err) | |||
| 920 | + | require.Len(t, diag.Volumes, 1) | |||
| 921 | + | assert.Equal(t, "gordon-example-test-data-docs", diag.Volumes[0].Name) | |||
| 922 | + | } | |||
| 923 | + | ||||
| 924 | + | func TestBuildRouteDiagnosis_IncludesAttachmentVolumesWithDoubleUnderscoreOwnerEncoding(t *testing.T) { | |||
| 925 | + | cpMock := climocks.NewMockControlPlane(t) | |||
| 926 | + | cpMock.EXPECT().GetRoute(context.Background(), "app.example.test").Return(&domain.Route{Domain: "app.example.test", Image: "app:latest"}, nil).Once() | |||
| 927 | + | cpMock.EXPECT().ListRoutesWithDetails(context.Background()).Return([]remote.RouteInfo{{ | |||
| 928 | + | Domain: "app.example.test", | |||
| 929 | + | ContainerID: "route-1", | |||
| 930 | + | ContainerStatus: "running", | |||
| 931 | + | Attachments: []dto.Attachment{{ | |||
| 932 | + | Name: "postgres", | |||
| 933 | + | Image: "postgres:16", | |||
| 934 | + | ContainerID: "attachment-1", | |||
| 935 | + | Status: "running", | |||
| 936 | + | }}, | |||
| 937 | + | }}, nil).Once() | |||
| 938 | + | cpMock.EXPECT().GetHealth(context.Background()).Return(nil, nil).Once() | |||
| 939 | + | cpMock.EXPECT().ListVolumes(context.Background()).Return([]dto.Volume{ | |||
| 940 | + | {Name: "gordon-gordon-app__example__test-postgres-var-lib-postgresql", Containers: []string{"gordon-app__example__test-postgres"}}, | |||
| 941 | + | }, nil).Once() | |||
| 942 | + | cpMock.EXPECT().ListOrphanedAttachments(context.Background()).Return(nil, nil).Once() | |||
| 943 | + | ||||
| 944 | + | diag, err := buildRouteDiagnosis(context.Background(), cpMock, "app.example.test") | |||
| 945 | + | ||||
| 946 | + | require.NoError(t, err) | |||
| 947 | + | require.Len(t, diag.Volumes, 1) | |||
| 948 | + | assert.Equal(t, "gordon-gordon-app__example__test-postgres-var-lib-postgresql", diag.Volumes[0].Name) | |||
| 949 | + | } | |||
| 950 | + | ||||
| 951 | + | func TestWriteRouteDiagnosisText_ShowsActiveAttachmentsSeparatelyFromOrphans(t *testing.T) { | |||
| 952 | + | var out bytes.Buffer | |||
| 953 | + | diag := &routeDiagnosis{ | |||
| 954 | + | Domain: "app.example.test", | |||
| 955 | + | Configured: true, | |||
| 956 | + | Route: &domain.Route{Domain: "app.example.test", Image: "app:latest"}, | |||
| 957 | + | Runtime: &remote.RouteInfo{ | |||
| 958 | + | Domain: "app.example.test", | |||
| 959 | + | ContainerID: "route-1", | |||
| 960 | + | ContainerStatus: "running", | |||
| 961 | + | Attachments: []dto.Attachment{{ | |||
| 962 | + | Name: "postgres", | |||
| 963 | + | Image: "postgres:16", | |||
| 964 | + | ContainerID: "attachment-1", | |||
| 965 | + | Status: "running", | |||
| 966 | + | }}, | |||
| 967 | + | }, | |||
| 968 | + | OrphanedAttachments: []domain.CleanupAttachment{{ | |||
| 969 | + | Name: "old-postgres", | |||
| 970 | + | ContainerID: "attachment-2", | |||
| 971 | + | Owner: "app.example.test", | |||
| 972 | + | }}, | |||
| 973 | + | } | |||
| 974 | + | ||||
| 975 | + | err := writeRouteDiagnosisText(&out, diag) | |||
| 976 | + | require.NoError(t, err) | |||
| 977 | + | rendered := stripANSI(out.String()) | |||
| 978 | + | assert.Contains(t, rendered, "Active attachments:") | |||
| 979 | + | assert.Contains(t, rendered, "postgres (running)") | |||
| 980 | + | assert.Contains(t, rendered, "Orphaned attachment: old-postgres") | |||
| 981 | + | } | |||
| 982 | + | ||||
| 902 | func TestRunRoutePurge_AttachmentsForceIsScopedToRoute(t *testing.T) { | 983 | func TestRunRoutePurge_AttachmentsForceIsScopedToRoute(t *testing.T) { | ||
| 903 | cpMock := climocks.NewMockControlPlane(t) | 984 | cpMock := climocks.NewMockControlPlane(t) | ||
| 904 | orphanedAttachments := []domain.CleanupAttachment{ | 985 | orphanedAttachments := []domain.CleanupAttachment{ | ||