fix(cli): honor configured volume prefix in diagnose
- commit
- 64bf6f171c900a5e20d558b1f96bf3a58ea7b193
- author
- brice · · 2026-05-22 16:48 UTC
- parent
- ea2fb16
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
6 files · +105 -5
M internal/adapters/dto/admin_config.go +8 -0
| @@ -5,6 +5,7 @@ type ConfigResponse struct { | |||
| 5 | 5 | Server ServerConfig `json:"server"` | |
| 6 | 6 | AutoRoute AutoRouteConfig `json:"auto_route"` | |
| 7 | 7 | NetworkIsolation NetworkIsolationConfig `json:"network_isolation"` | |
| 8 | + | Volumes VolumesConfig `json:"volumes"` | |
| 8 | 9 | Routes []Route `json:"routes"` | |
| 9 | 10 | ExternalRoutes []ExternalRoute `json:"external_routes"` | |
| 10 | 11 | } | |
| @@ -28,6 +29,13 @@ type NetworkIsolationConfig struct { | |||
| 28 | 29 | Prefix string `json:"prefix"` | |
| 29 | 30 | } | |
| 30 | 31 | ||
| 32 | + | // VolumesConfig represents volume settings relevant to runtime-managed volumes. | |
| 33 | + | type VolumesConfig struct { | |
| 34 | + | AutoCreate bool `json:"auto_create"` | |
| 35 | + | Prefix string `json:"prefix"` | |
| 36 | + | Preserve bool `json:"preserve"` | |
| 37 | + | } | |
| 38 | + | ||
| 31 | 39 | // ExternalRoute represents an external route config entry. | |
| 32 | 40 | type ExternalRoute struct { | |
| 33 | 41 | Domain string `json:"domain"` | |
| @@ -5,6 +5,7 @@ type ConfigResponse struct { | |||||
| 5 | Server ServerConfig `json:"server"` | 5 | Server ServerConfig `json:"server"` | ||
| 6 | AutoRoute AutoRouteConfig `json:"auto_route"` | 6 | AutoRoute AutoRouteConfig `json:"auto_route"` | ||
| 7 | NetworkIsolation NetworkIsolationConfig `json:"network_isolation"` | 7 | NetworkIsolation NetworkIsolationConfig `json:"network_isolation"` | ||
| 8 | + | Volumes VolumesConfig `json:"volumes"` | |||
| 8 | Routes []Route `json:"routes"` | 9 | Routes []Route `json:"routes"` | ||
| 9 | ExternalRoutes []ExternalRoute `json:"external_routes"` | 10 | ExternalRoutes []ExternalRoute `json:"external_routes"` | ||
| 10 | } | 11 | } | ||
| @@ -28,6 +29,13 @@ type NetworkIsolationConfig struct { | |||||
| 28 | Prefix string `json:"prefix"` | 29 | Prefix string `json:"prefix"` | ||
| 29 | } | 30 | } | ||
| 30 | 31 | ||||
| 32 | + | // VolumesConfig represents volume settings relevant to runtime-managed volumes. | |||
| 33 | + | type VolumesConfig struct { | |||
| 34 | + | AutoCreate bool `json:"auto_create"` | |||
| 35 | + | Prefix string `json:"prefix"` | |||
| 36 | + | Preserve bool `json:"preserve"` | |||
| 37 | + | } | |||
| 38 | + | ||||
| 31 | // ExternalRoute represents an external route config entry. | 39 | // ExternalRoute represents an external route config entry. | ||
| 32 | type ExternalRoute struct { | 40 | type ExternalRoute struct { | ||
| 33 | Domain string `json:"domain"` | 41 | Domain string `json:"domain"` | ||
M internal/adapters/in/cli/controlplane_local.go +3 -0
| @@ -452,6 +452,9 @@ func (l *localControlPlane) GetConfig(ctx context.Context) (*remote.Config, erro | |||
| 452 | 452 | cfg.AutoRoute.Enabled = l.configSvc.IsAutoRouteEnabled() | |
| 453 | 453 | cfg.NetworkIsolation.Enabled = l.configSvc.IsNetworkIsolationEnabled() | |
| 454 | 454 | cfg.NetworkIsolation.Prefix = l.configSvc.GetNetworkPrefix() | |
| 455 | + | if volumeCfg, ok := any(l.configSvc).(interface{ GetVolumeConfig() (bool, string, bool) }); ok { | |
| 456 | + | cfg.Volumes.AutoCreate, cfg.Volumes.Prefix, cfg.Volumes.Preserve = volumeCfg.GetVolumeConfig() | |
| 457 | + | } | |
| 455 | 458 | return cfg, nil | |
| 456 | 459 | } | |
| 457 | 460 | ||
| @@ -452,6 +452,9 @@ func (l *localControlPlane) GetConfig(ctx context.Context) (*remote.Config, erro | |||||
| 452 | cfg.AutoRoute.Enabled = l.configSvc.IsAutoRouteEnabled() | 452 | cfg.AutoRoute.Enabled = l.configSvc.IsAutoRouteEnabled() | ||
| 453 | cfg.NetworkIsolation.Enabled = l.configSvc.IsNetworkIsolationEnabled() | 453 | cfg.NetworkIsolation.Enabled = l.configSvc.IsNetworkIsolationEnabled() | ||
| 454 | cfg.NetworkIsolation.Prefix = l.configSvc.GetNetworkPrefix() | 454 | cfg.NetworkIsolation.Prefix = l.configSvc.GetNetworkPrefix() | ||
| 455 | + | if volumeCfg, ok := any(l.configSvc).(interface{ GetVolumeConfig() (bool, string, bool) }); ok { | |||
| 456 | + | cfg.Volumes.AutoCreate, cfg.Volumes.Prefix, cfg.Volumes.Preserve = volumeCfg.GetVolumeConfig() | |||
| 457 | + | } | |||
| 455 | return cfg, nil | 458 | return cfg, nil | ||
| 456 | } | 459 | } | ||
| 457 | 460 | ||||
M internal/adapters/in/cli/remote/client.go +5 -0
| @@ -864,6 +864,11 @@ type Config struct { | |||
| 864 | 864 | Enabled bool `json:"enabled"` | |
| 865 | 865 | Prefix string `json:"prefix"` | |
| 866 | 866 | } `json:"network_isolation"` | |
| 867 | + | Volumes struct { | |
| 868 | + | AutoCreate bool `json:"auto_create"` | |
| 869 | + | Prefix string `json:"prefix"` | |
| 870 | + | Preserve bool `json:"preserve"` | |
| 871 | + | } `json:"volumes"` | |
| 867 | 872 | Routes []domain.Route `json:"routes"` | |
| 868 | 873 | ExternalRoutes []ExternalRoute `json:"external_routes"` | |
| 869 | 874 | } | |
| @@ -864,6 +864,11 @@ type Config struct { | |||||
| 864 | Enabled bool `json:"enabled"` | 864 | Enabled bool `json:"enabled"` | ||
| 865 | Prefix string `json:"prefix"` | 865 | Prefix string `json:"prefix"` | ||
| 866 | } `json:"network_isolation"` | 866 | } `json:"network_isolation"` | ||
| 867 | + | Volumes struct { | |||
| 868 | + | AutoCreate bool `json:"auto_create"` | |||
| 869 | + | Prefix string `json:"prefix"` | |||
| 870 | + | Preserve bool `json:"preserve"` | |||
| 871 | + | } `json:"volumes"` | |||
| 867 | Routes []domain.Route `json:"routes"` | 872 | Routes []domain.Route `json:"routes"` | ||
| 868 | ExternalRoutes []ExternalRoute `json:"external_routes"` | 873 | ExternalRoutes []ExternalRoute `json:"external_routes"` | ||
| 869 | } | 874 | } | ||
M internal/adapters/in/cli/routes.go +20 -5
| @@ -1166,11 +1166,23 @@ 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 | - | scope := newRouteResourceScope(domainName, diag.Runtime, diag.OrphanedAttachments) | |
| 1169 | + | if len(volumes) == 0 { | |
| 1170 | + | return | |
| 1171 | + | } | |
| 1172 | + | scope := newRouteResourceScope(domainName, routeDiagnosisVolumePrefix(ctx, cp), diag.Runtime, diag.OrphanedAttachments) | |
| 1170 | 1173 | diag.Volumes = volumesForRouteDiagnosis(volumes, scope) | |
| 1171 | 1174 | } | |
| 1172 | 1175 | } | |
| 1173 | 1176 | ||
| 1177 | + | func routeDiagnosisVolumePrefix(ctx context.Context, cp ControlPlane) string { | |
| 1178 | + | const defaultPrefix = "gordon" | |
| 1179 | + | cfg, err := cp.GetConfig(ctx) | |
| 1180 | + | if err != nil || cfg == nil || cfg.Volumes.Prefix == "" { | |
| 1181 | + | return defaultPrefix | |
| 1182 | + | } | |
| 1183 | + | return cfg.Volumes.Prefix | |
| 1184 | + | } | |
| 1185 | + | ||
| 1174 | 1186 | func loadRouteDiagnosisOrphanedAttachments(ctx context.Context, cp ControlPlane, domainName string, diag *routeDiagnosis) { | |
| 1175 | 1187 | lister, ok := cp.(interface { | |
| 1176 | 1188 | ListOrphanedAttachments(context.Context) ([]domain.CleanupAttachment, error) | |
| @@ -1200,10 +1212,13 @@ type routeResourceScope struct { | |||
| 1200 | 1212 | volumePrefixes []string | |
| 1201 | 1213 | } | |
| 1202 | 1214 | ||
| 1203 | - | func newRouteResourceScope(domainName string, runtime *remote.RouteInfo, orphaned []domain.CleanupAttachment) routeResourceScope { | |
| 1215 | + | func newRouteResourceScope(domainName, volumePrefix string, runtime *remote.RouteInfo, orphaned []domain.CleanupAttachment) routeResourceScope { | |
| 1216 | + | if volumePrefix == "" { | |
| 1217 | + | volumePrefix = "gordon" | |
| 1218 | + | } | |
| 1204 | 1219 | scope := routeResourceScope{ | |
| 1205 | 1220 | containerNames: make(map[string]struct{}), | |
| 1206 | - | volumePrefixes: []string{"gordon-" + strings.ReplaceAll(domainName, ".", "-") + "-"}, | |
| 1221 | + | volumePrefixes: []string{volumePrefix + "-" + strings.ReplaceAll(domainName, ".", "-") + "-"}, | |
| 1207 | 1222 | } | |
| 1208 | 1223 | for _, name := range []string{ | |
| 1209 | 1224 | fmt.Sprintf("gordon-%s", domainName), | |
| @@ -1216,7 +1231,7 @@ func newRouteResourceScope(domainName string, runtime *remote.RouteInfo, orphane | |||
| 1216 | 1231 | for _, attachment := range runtime.Attachments { | |
| 1217 | 1232 | for _, name := range routeAttachmentContainerNameCandidates(domainName, attachment.Name) { | |
| 1218 | 1233 | scope.containerNames[name] = struct{}{} | |
| 1219 | - | scope.volumePrefixes = append(scope.volumePrefixes, "gordon-"+name+"-") | |
| 1234 | + | scope.volumePrefixes = append(scope.volumePrefixes, volumePrefix+"-"+name+"-") | |
| 1220 | 1235 | } | |
| 1221 | 1236 | } | |
| 1222 | 1237 | } | |
| @@ -1225,7 +1240,7 @@ func newRouteResourceScope(domainName string, runtime *remote.RouteInfo, orphane | |||
| 1225 | 1240 | continue | |
| 1226 | 1241 | } | |
| 1227 | 1242 | scope.containerNames[attachment.Name] = struct{}{} | |
| 1228 | - | scope.volumePrefixes = append(scope.volumePrefixes, "gordon-"+attachment.Name+"-") | |
| 1243 | + | scope.volumePrefixes = append(scope.volumePrefixes, volumePrefix+"-"+attachment.Name+"-") | |
| 1229 | 1244 | } | |
| 1230 | 1245 | return scope | |
| 1231 | 1246 | } | |
| @@ -1166,11 +1166,23 @@ 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 | - | scope := newRouteResourceScope(domainName, diag.Runtime, diag.OrphanedAttachments) | 1169 | + | if len(volumes) == 0 { |
| 1170 | + | return | |||
| 1171 | + | } | |||
| 1172 | + | scope := newRouteResourceScope(domainName, routeDiagnosisVolumePrefix(ctx, cp), diag.Runtime, diag.OrphanedAttachments) | |||
| 1170 | diag.Volumes = volumesForRouteDiagnosis(volumes, scope) | 1173 | diag.Volumes = volumesForRouteDiagnosis(volumes, scope) | ||
| 1171 | } | 1174 | } | ||
| 1172 | } | 1175 | } | ||
| 1173 | 1176 | ||||
| 1177 | + | func routeDiagnosisVolumePrefix(ctx context.Context, cp ControlPlane) string { | |||
| 1178 | + | const defaultPrefix = "gordon" | |||
| 1179 | + | cfg, err := cp.GetConfig(ctx) | |||
| 1180 | + | if err != nil || cfg == nil || cfg.Volumes.Prefix == "" { | |||
| 1181 | + | return defaultPrefix | |||
| 1182 | + | } | |||
| 1183 | + | return cfg.Volumes.Prefix | |||
| 1184 | + | } | |||
| 1185 | + | ||||
| 1174 | func loadRouteDiagnosisOrphanedAttachments(ctx context.Context, cp ControlPlane, domainName string, diag *routeDiagnosis) { | 1186 | func loadRouteDiagnosisOrphanedAttachments(ctx context.Context, cp ControlPlane, domainName string, diag *routeDiagnosis) { | ||
| 1175 | lister, ok := cp.(interface { | 1187 | lister, ok := cp.(interface { | ||
| 1176 | ListOrphanedAttachments(context.Context) ([]domain.CleanupAttachment, error) | 1188 | ListOrphanedAttachments(context.Context) ([]domain.CleanupAttachment, error) | ||
| @@ -1200,10 +1212,13 @@ type routeResourceScope struct { | |||||
| 1200 | volumePrefixes []string | 1212 | volumePrefixes []string | ||
| 1201 | } | 1213 | } | ||
| 1202 | 1214 | ||||
| 1203 | - | func newRouteResourceScope(domainName string, runtime *remote.RouteInfo, orphaned []domain.CleanupAttachment) routeResourceScope { | 1215 | + | func newRouteResourceScope(domainName, volumePrefix string, runtime *remote.RouteInfo, orphaned []domain.CleanupAttachment) routeResourceScope { |
| 1216 | + | if volumePrefix == "" { | |||
| 1217 | + | volumePrefix = "gordon" | |||
| 1218 | + | } | |||
| 1204 | scope := routeResourceScope{ | 1219 | scope := routeResourceScope{ | ||
| 1205 | containerNames: make(map[string]struct{}), | 1220 | containerNames: make(map[string]struct{}), | ||
| 1206 | - | volumePrefixes: []string{"gordon-" + strings.ReplaceAll(domainName, ".", "-") + "-"}, | 1221 | + | volumePrefixes: []string{volumePrefix + "-" + strings.ReplaceAll(domainName, ".", "-") + "-"}, |
| 1207 | } | 1222 | } | ||
| 1208 | for _, name := range []string{ | 1223 | for _, name := range []string{ | ||
| 1209 | fmt.Sprintf("gordon-%s", domainName), | 1224 | fmt.Sprintf("gordon-%s", domainName), | ||
| @@ -1216,7 +1231,7 @@ func newRouteResourceScope(domainName string, runtime *remote.RouteInfo, orphane | |||||
| 1216 | for _, attachment := range runtime.Attachments { | 1231 | for _, attachment := range runtime.Attachments { | ||
| 1217 | for _, name := range routeAttachmentContainerNameCandidates(domainName, attachment.Name) { | 1232 | for _, name := range routeAttachmentContainerNameCandidates(domainName, attachment.Name) { | ||
| 1218 | scope.containerNames[name] = struct{}{} | 1233 | scope.containerNames[name] = struct{}{} | ||
| 1219 | - | scope.volumePrefixes = append(scope.volumePrefixes, "gordon-"+name+"-") | 1234 | + | scope.volumePrefixes = append(scope.volumePrefixes, volumePrefix+"-"+name+"-") |
| 1220 | } | 1235 | } | ||
| 1221 | } | 1236 | } | ||
| 1222 | } | 1237 | } | ||
| @@ -1225,7 +1240,7 @@ func newRouteResourceScope(domainName string, runtime *remote.RouteInfo, orphane | |||||
| 1225 | continue | 1240 | continue | ||
| 1226 | } | 1241 | } | ||
| 1227 | scope.containerNames[attachment.Name] = struct{}{} | 1242 | scope.containerNames[attachment.Name] = struct{}{} | ||
| 1228 | - | scope.volumePrefixes = append(scope.volumePrefixes, "gordon-"+attachment.Name+"-") | 1243 | + | scope.volumePrefixes = append(scope.volumePrefixes, volumePrefix+"-"+attachment.Name+"-") |
| 1229 | } | 1244 | } | ||
| 1230 | return scope | 1245 | return scope | ||
| 1231 | } | 1246 | } | ||
M internal/adapters/in/cli/routes_test.go +66 -0
| @@ -914,6 +914,7 @@ func TestBuildRouteDiagnosis_VolumesAreScopedToExactRouteContainers(t *testing.T | |||
| 914 | 914 | {Name: "gordon-example-test-data-docs", Containers: []string{"gordon-example.test"}}, | |
| 915 | 915 | {Name: "gordon-api-example-test-cache", Containers: []string{"gordon-api.example.test"}}, | |
| 916 | 916 | }, nil).Once() | |
| 917 | + | cpMock.EXPECT().GetConfig(context.Background()).Return(&remote.Config{}, nil).Once() | |
| 917 | 918 | cpMock.EXPECT().ListOrphanedAttachments(context.Background()).Return(nil, nil).Once() | |
| 918 | 919 | ||
| 919 | 920 | diag, err := buildRouteDiagnosis(context.Background(), cpMock, "example.test") | |
| @@ -923,6 +924,38 @@ func TestBuildRouteDiagnosis_VolumesAreScopedToExactRouteContainers(t *testing.T | |||
| 923 | 924 | assert.Equal(t, "gordon-example-test-data-docs", diag.Volumes[0].Name) | |
| 924 | 925 | } | |
| 925 | 926 | ||
| 927 | + | func TestBuildRouteDiagnosis_UsesConfiguredVolumePrefixForPreservedVolumes(t *testing.T) { | |
| 928 | + | srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | |
| 929 | + | w.Header().Set("Content-Type", "application/json") | |
| 930 | + | switch r.URL.Path { | |
| 931 | + | case "/admin/routes/example.test": | |
| 932 | + | w.WriteHeader(http.StatusNotFound) | |
| 933 | + | _, _ = w.Write([]byte(`{"error":"route not found"}`)) | |
| 934 | + | case "/admin/routes": | |
| 935 | + | require.Equal(t, "detailed=true", r.URL.RawQuery) | |
| 936 | + | _, _ = w.Write([]byte(`{"routes":[]}`)) | |
| 937 | + | case "/admin/health": | |
| 938 | + | _, _ = w.Write([]byte(`{"health":{}}`)) | |
| 939 | + | case "/admin/volumes": | |
| 940 | + | _, _ = w.Write([]byte(`[{"name":"custom-example-test-data-docs","containers":[]}]`)) | |
| 941 | + | case "/admin/attachments/orphans": | |
| 942 | + | _, _ = w.Write([]byte(`{"attachments":[]}`)) | |
| 943 | + | case "/admin/config": | |
| 944 | + | _, _ = w.Write([]byte(`{"server":{"port":80,"registry_port":5000,"registry_domain":"registry.example.test"},"auto_route":{"enabled":false},"network_isolation":{"enabled":true,"prefix":"custom-network"},"volumes":{"auto_create":true,"prefix":"custom","preserve":true},"routes":[],"external_routes":[]}`)) | |
| 945 | + | default: | |
| 946 | + | t.Fatalf("unexpected path: %s", r.URL.Path) | |
| 947 | + | } | |
| 948 | + | })) | |
| 949 | + | defer srv.Close() | |
| 950 | + | ||
| 951 | + | cp := NewRemoteControlPlane(remote.NewClient(srv.URL)) | |
| 952 | + | diag, err := buildRouteDiagnosis(context.Background(), cp, "example.test") | |
| 953 | + | ||
| 954 | + | require.NoError(t, err) | |
| 955 | + | require.Len(t, diag.Volumes, 1) | |
| 956 | + | assert.Equal(t, "custom-example-test-data-docs", diag.Volumes[0].Name) | |
| 957 | + | } | |
| 958 | + | ||
| 926 | 959 | func TestBuildRouteDiagnosis_IncludesAttachmentVolumesWithDoubleUnderscoreOwnerEncoding(t *testing.T) { | |
| 927 | 960 | cpMock := climocks.NewMockControlPlane(t) | |
| 928 | 961 | cpMock.EXPECT().GetRoute(context.Background(), "app.example.test").Return(&domain.Route{Domain: "app.example.test", Image: "app:latest"}, nil).Once() | |
| @@ -941,6 +974,7 @@ func TestBuildRouteDiagnosis_IncludesAttachmentVolumesWithDoubleUnderscoreOwnerE | |||
| 941 | 974 | cpMock.EXPECT().ListVolumes(context.Background()).Return([]dto.Volume{ | |
| 942 | 975 | {Name: "gordon-gordon-app__example__test-postgres-var-lib-postgresql", Containers: []string{"gordon-app__example__test-postgres"}}, | |
| 943 | 976 | }, nil).Once() | |
| 977 | + | cpMock.EXPECT().GetConfig(context.Background()).Return(&remote.Config{}, nil).Once() | |
| 944 | 978 | cpMock.EXPECT().ListOrphanedAttachments(context.Background()).Return(nil, nil).Once() | |
| 945 | 979 | ||
| 946 | 980 | diag, err := buildRouteDiagnosis(context.Background(), cpMock, "app.example.test") | |
| @@ -980,6 +1014,38 @@ func TestBuildRouteDiagnosis_RemoteMissingRouteDoesNotWarnOnExpectedNotFound(t * | |||
| 980 | 1014 | assert.Empty(t, diag.Warnings) | |
| 981 | 1015 | } | |
| 982 | 1016 | ||
| 1017 | + | func TestRunRoutePurge_VolumesUsesConfiguredVolumePrefix(t *testing.T) { | |
| 1018 | + | srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | |
| 1019 | + | w.Header().Set("Content-Type", "application/json") | |
| 1020 | + | switch r.URL.Path { | |
| 1021 | + | case "/admin/routes/example.test": | |
| 1022 | + | w.WriteHeader(http.StatusNotFound) | |
| 1023 | + | _, _ = w.Write([]byte(`{"error":"route not found"}`)) | |
| 1024 | + | case "/admin/routes": | |
| 1025 | + | require.Equal(t, "detailed=true", r.URL.RawQuery) | |
| 1026 | + | _, _ = w.Write([]byte(`{"routes":[]}`)) | |
| 1027 | + | case "/admin/health": | |
| 1028 | + | _, _ = w.Write([]byte(`{"health":{}}`)) | |
| 1029 | + | case "/admin/volumes": | |
| 1030 | + | _, _ = w.Write([]byte(`[{"name":"custom-example-test-data-docs","containers":[]}]`)) | |
| 1031 | + | case "/admin/attachments/orphans": | |
| 1032 | + | _, _ = w.Write([]byte(`{"attachments":[]}`)) | |
| 1033 | + | case "/admin/config": | |
| 1034 | + | _, _ = w.Write([]byte(`{"server":{"port":80,"registry_port":5000,"registry_domain":"registry.example.test"},"auto_route":{"enabled":false},"network_isolation":{"enabled":true,"prefix":"custom-network"},"volumes":{"auto_create":true,"prefix":"custom","preserve":true},"routes":[],"external_routes":[]}`)) | |
| 1035 | + | default: | |
| 1036 | + | t.Fatalf("unexpected path: %s", r.URL.Path) | |
| 1037 | + | } | |
| 1038 | + | })) | |
| 1039 | + | defer srv.Close() | |
| 1040 | + | ||
| 1041 | + | cp := NewRemoteControlPlane(remote.NewClient(srv.URL)) | |
| 1042 | + | report, err := runRoutePurge(context.Background(), cp, "example.test", routePurgeOptions{Volumes: true}) | |
| 1043 | + | ||
| 1044 | + | require.NoError(t, err) | |
| 1045 | + | require.Len(t, report.PreservedVolumes, 1) | |
| 1046 | + | assert.Equal(t, "custom-example-test-data-docs", report.PreservedVolumes[0].Name) | |
| 1047 | + | } | |
| 1048 | + | ||
| 983 | 1049 | func TestBuildRouteDiagnosis_UsesRouteScopedReviewHintForOrphanedAttachments(t *testing.T) { | |
| 984 | 1050 | cpMock := climocks.NewMockControlPlane(t) | |
| 985 | 1051 | cpMock.EXPECT().GetRoute(context.Background(), "app.example.test").Return(&domain.Route{Domain: "app.example.test", Image: "app:latest"}, nil).Once() | |
| @@ -914,6 +914,7 @@ func TestBuildRouteDiagnosis_VolumesAreScopedToExactRouteContainers(t *testing.T | |||||
| 914 | {Name: "gordon-example-test-data-docs", Containers: []string{"gordon-example.test"}}, | 914 | {Name: "gordon-example-test-data-docs", Containers: []string{"gordon-example.test"}}, | ||
| 915 | {Name: "gordon-api-example-test-cache", Containers: []string{"gordon-api.example.test"}}, | 915 | {Name: "gordon-api-example-test-cache", Containers: []string{"gordon-api.example.test"}}, | ||
| 916 | }, nil).Once() | 916 | }, nil).Once() | ||
| 917 | + | cpMock.EXPECT().GetConfig(context.Background()).Return(&remote.Config{}, nil).Once() | |||
| 917 | cpMock.EXPECT().ListOrphanedAttachments(context.Background()).Return(nil, nil).Once() | 918 | cpMock.EXPECT().ListOrphanedAttachments(context.Background()).Return(nil, nil).Once() | ||
| 918 | 919 | ||||
| 919 | diag, err := buildRouteDiagnosis(context.Background(), cpMock, "example.test") | 920 | diag, err := buildRouteDiagnosis(context.Background(), cpMock, "example.test") | ||
| @@ -923,6 +924,38 @@ func TestBuildRouteDiagnosis_VolumesAreScopedToExactRouteContainers(t *testing.T | |||||
| 923 | assert.Equal(t, "gordon-example-test-data-docs", diag.Volumes[0].Name) | 924 | assert.Equal(t, "gordon-example-test-data-docs", diag.Volumes[0].Name) | ||
| 924 | } | 925 | } | ||
| 925 | 926 | ||||
| 927 | + | func TestBuildRouteDiagnosis_UsesConfiguredVolumePrefixForPreservedVolumes(t *testing.T) { | |||
| 928 | + | srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | |||
| 929 | + | w.Header().Set("Content-Type", "application/json") | |||
| 930 | + | switch r.URL.Path { | |||
| 931 | + | case "/admin/routes/example.test": | |||
| 932 | + | w.WriteHeader(http.StatusNotFound) | |||
| 933 | + | _, _ = w.Write([]byte(`{"error":"route not found"}`)) | |||
| 934 | + | case "/admin/routes": | |||
| 935 | + | require.Equal(t, "detailed=true", r.URL.RawQuery) | |||
| 936 | + | _, _ = w.Write([]byte(`{"routes":[]}`)) | |||
| 937 | + | case "/admin/health": | |||
| 938 | + | _, _ = w.Write([]byte(`{"health":{}}`)) | |||
| 939 | + | case "/admin/volumes": | |||
| 940 | + | _, _ = w.Write([]byte(`[{"name":"custom-example-test-data-docs","containers":[]}]`)) | |||
| 941 | + | case "/admin/attachments/orphans": | |||
| 942 | + | _, _ = w.Write([]byte(`{"attachments":[]}`)) | |||
| 943 | + | case "/admin/config": | |||
| 944 | + | _, _ = w.Write([]byte(`{"server":{"port":80,"registry_port":5000,"registry_domain":"registry.example.test"},"auto_route":{"enabled":false},"network_isolation":{"enabled":true,"prefix":"custom-network"},"volumes":{"auto_create":true,"prefix":"custom","preserve":true},"routes":[],"external_routes":[]}`)) | |||
| 945 | + | default: | |||
| 946 | + | t.Fatalf("unexpected path: %s", r.URL.Path) | |||
| 947 | + | } | |||
| 948 | + | })) | |||
| 949 | + | defer srv.Close() | |||
| 950 | + | ||||
| 951 | + | cp := NewRemoteControlPlane(remote.NewClient(srv.URL)) | |||
| 952 | + | diag, err := buildRouteDiagnosis(context.Background(), cp, "example.test") | |||
| 953 | + | ||||
| 954 | + | require.NoError(t, err) | |||
| 955 | + | require.Len(t, diag.Volumes, 1) | |||
| 956 | + | assert.Equal(t, "custom-example-test-data-docs", diag.Volumes[0].Name) | |||
| 957 | + | } | |||
| 958 | + | ||||
| 926 | func TestBuildRouteDiagnosis_IncludesAttachmentVolumesWithDoubleUnderscoreOwnerEncoding(t *testing.T) { | 959 | func TestBuildRouteDiagnosis_IncludesAttachmentVolumesWithDoubleUnderscoreOwnerEncoding(t *testing.T) { | ||
| 927 | cpMock := climocks.NewMockControlPlane(t) | 960 | cpMock := climocks.NewMockControlPlane(t) | ||
| 928 | cpMock.EXPECT().GetRoute(context.Background(), "app.example.test").Return(&domain.Route{Domain: "app.example.test", Image: "app:latest"}, nil).Once() | 961 | cpMock.EXPECT().GetRoute(context.Background(), "app.example.test").Return(&domain.Route{Domain: "app.example.test", Image: "app:latest"}, nil).Once() | ||
| @@ -941,6 +974,7 @@ func TestBuildRouteDiagnosis_IncludesAttachmentVolumesWithDoubleUnderscoreOwnerE | |||||
| 941 | cpMock.EXPECT().ListVolumes(context.Background()).Return([]dto.Volume{ | 974 | cpMock.EXPECT().ListVolumes(context.Background()).Return([]dto.Volume{ | ||
| 942 | {Name: "gordon-gordon-app__example__test-postgres-var-lib-postgresql", Containers: []string{"gordon-app__example__test-postgres"}}, | 975 | {Name: "gordon-gordon-app__example__test-postgres-var-lib-postgresql", Containers: []string{"gordon-app__example__test-postgres"}}, | ||
| 943 | }, nil).Once() | 976 | }, nil).Once() | ||
| 977 | + | cpMock.EXPECT().GetConfig(context.Background()).Return(&remote.Config{}, nil).Once() | |||
| 944 | cpMock.EXPECT().ListOrphanedAttachments(context.Background()).Return(nil, nil).Once() | 978 | cpMock.EXPECT().ListOrphanedAttachments(context.Background()).Return(nil, nil).Once() | ||
| 945 | 979 | ||||
| 946 | diag, err := buildRouteDiagnosis(context.Background(), cpMock, "app.example.test") | 980 | diag, err := buildRouteDiagnosis(context.Background(), cpMock, "app.example.test") | ||
| @@ -980,6 +1014,38 @@ func TestBuildRouteDiagnosis_RemoteMissingRouteDoesNotWarnOnExpectedNotFound(t * | |||||
| 980 | assert.Empty(t, diag.Warnings) | 1014 | assert.Empty(t, diag.Warnings) | ||
| 981 | } | 1015 | } | ||
| 982 | 1016 | ||||
| 1017 | + | func TestRunRoutePurge_VolumesUsesConfiguredVolumePrefix(t *testing.T) { | |||
| 1018 | + | srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | |||
| 1019 | + | w.Header().Set("Content-Type", "application/json") | |||
| 1020 | + | switch r.URL.Path { | |||
| 1021 | + | case "/admin/routes/example.test": | |||
| 1022 | + | w.WriteHeader(http.StatusNotFound) | |||
| 1023 | + | _, _ = w.Write([]byte(`{"error":"route not found"}`)) | |||
| 1024 | + | case "/admin/routes": | |||
| 1025 | + | require.Equal(t, "detailed=true", r.URL.RawQuery) | |||
| 1026 | + | _, _ = w.Write([]byte(`{"routes":[]}`)) | |||
| 1027 | + | case "/admin/health": | |||
| 1028 | + | _, _ = w.Write([]byte(`{"health":{}}`)) | |||
| 1029 | + | case "/admin/volumes": | |||
| 1030 | + | _, _ = w.Write([]byte(`[{"name":"custom-example-test-data-docs","containers":[]}]`)) | |||
| 1031 | + | case "/admin/attachments/orphans": | |||
| 1032 | + | _, _ = w.Write([]byte(`{"attachments":[]}`)) | |||
| 1033 | + | case "/admin/config": | |||
| 1034 | + | _, _ = w.Write([]byte(`{"server":{"port":80,"registry_port":5000,"registry_domain":"registry.example.test"},"auto_route":{"enabled":false},"network_isolation":{"enabled":true,"prefix":"custom-network"},"volumes":{"auto_create":true,"prefix":"custom","preserve":true},"routes":[],"external_routes":[]}`)) | |||
| 1035 | + | default: | |||
| 1036 | + | t.Fatalf("unexpected path: %s", r.URL.Path) | |||
| 1037 | + | } | |||
| 1038 | + | })) | |||
| 1039 | + | defer srv.Close() | |||
| 1040 | + | ||||
| 1041 | + | cp := NewRemoteControlPlane(remote.NewClient(srv.URL)) | |||
| 1042 | + | report, err := runRoutePurge(context.Background(), cp, "example.test", routePurgeOptions{Volumes: true}) | |||
| 1043 | + | ||||
| 1044 | + | require.NoError(t, err) | |||
| 1045 | + | require.Len(t, report.PreservedVolumes, 1) | |||
| 1046 | + | assert.Equal(t, "custom-example-test-data-docs", report.PreservedVolumes[0].Name) | |||
| 1047 | + | } | |||
| 1048 | + | ||||
| 983 | func TestBuildRouteDiagnosis_UsesRouteScopedReviewHintForOrphanedAttachments(t *testing.T) { | 1049 | func TestBuildRouteDiagnosis_UsesRouteScopedReviewHintForOrphanedAttachments(t *testing.T) { | ||
| 984 | cpMock := climocks.NewMockControlPlane(t) | 1050 | cpMock := climocks.NewMockControlPlane(t) | ||
| 985 | cpMock.EXPECT().GetRoute(context.Background(), "app.example.test").Return(&domain.Route{Domain: "app.example.test", Image: "app:latest"}, nil).Once() | 1051 | cpMock.EXPECT().GetRoute(context.Background(), "app.example.test").Return(&domain.Route{Domain: "app.example.test", Image: "app:latest"}, nil).Once() | ||
M internal/adapters/in/http/admin/handler.go +3 -0
| @@ -1377,6 +1377,9 @@ func (h *Handler) handleConfig(w http.ResponseWriter, r *http.Request) { | |||
| 1377 | 1377 | Routes: routeResponses, | |
| 1378 | 1378 | ExternalRoutes: externalResponses, | |
| 1379 | 1379 | } | |
| 1380 | + | if volumeCfg, ok := any(h.configSvc).(interface{ GetVolumeConfig() (bool, string, bool) }); ok { | |
| 1381 | + | config.Volumes.AutoCreate, config.Volumes.Prefix, config.Volumes.Preserve = volumeCfg.GetVolumeConfig() | |
| 1382 | + | } | |
| 1380 | 1383 | ||
| 1381 | 1384 | h.sendJSON(w, http.StatusOK, config) | |
| 1382 | 1385 | } | |
| @@ -1377,6 +1377,9 @@ func (h *Handler) handleConfig(w http.ResponseWriter, r *http.Request) { | |||||
| 1377 | Routes: routeResponses, | 1377 | Routes: routeResponses, | ||
| 1378 | ExternalRoutes: externalResponses, | 1378 | ExternalRoutes: externalResponses, | ||
| 1379 | } | 1379 | } | ||
| 1380 | + | if volumeCfg, ok := any(h.configSvc).(interface{ GetVolumeConfig() (bool, string, bool) }); ok { | |||
| 1381 | + | config.Volumes.AutoCreate, config.Volumes.Prefix, config.Volumes.Preserve = volumeCfg.GetVolumeConfig() | |||
| 1382 | + | } | |||
| 1380 | 1383 | ||||
| 1381 | h.sendJSON(w, http.StatusOK, config) | 1384 | h.sendJSON(w, http.StatusOK, config) | ||
| 1382 | } | 1385 | } | ||