From 80d09fd0df96e6b5eee8e15ff2e338d75f3fb2f6 Mon Sep 17 00:00:00 2001 From: rcourtman Date: Mon, 16 Feb 2026 16:27:44 +0000 Subject: [PATCH] feat(api): add deprecated /api/v2/resources alias for unified resources --- docs/releases/RELEASE_NOTES_v6.md | 6 ++-- internal/api/route_inventory_test.go | 3 ++ internal/api/router_routes_monitoring.go | 18 ++++++++++ internal/api/router_routes_monitoring_test.go | 33 +++++++++++++++++++ 4 files changed, 57 insertions(+), 3 deletions(-) create mode 100644 internal/api/router_routes_monitoring_test.go diff --git a/docs/releases/RELEASE_NOTES_v6.md b/docs/releases/RELEASE_NOTES_v6.md index d578cc0bc..6f806e75f 100644 --- a/docs/releases/RELEASE_NOTES_v6.md +++ b/docs/releases/RELEASE_NOTES_v6.md @@ -84,8 +84,8 @@ This release is the Unified Resources + unified navigation foundation, plus opt- ## Breaking Changes - Unified Resources is now the canonical resource model. - - `/api/v2/resources` has been renamed to `/api/resources`. - - Legacy resource plumbing and compatibility shims were removed as unified-only consumers landed (AI runtime, alerts, frontend selectors). + - Canonical API is now `/api/resources`. + - Deprecated alias: `/api/v2/resources` (temporary compatibility shim for older clients; will be removed after the migration window). - Frontend routing is now canonicalized around unified navigation. - Legacy routes (for example `/services` and `/kubernetes`) are transitional aliases and will be removed after the migration window. - Storage/Backups naming and routing were de-V2'ed. @@ -101,7 +101,7 @@ This release is the Unified Resources + unified navigation foundation, plus opt- ### API Clients and Integrations -- Replace calls to `/api/v2/resources` with `/api/resources`. +- Replace calls to `/api/v2/resources` with `/api/resources` (recommended). - If an integration depended on legacy resource arrays or legacy resource endpoints, migrate to unified resource types and unified resource IDs. ### Multi-Tenant Organizations (Enterprise, Opt-In) diff --git a/internal/api/route_inventory_test.go b/internal/api/route_inventory_test.go index 2ea79357b..16a246d99 100644 --- a/internal/api/route_inventory_test.go +++ b/internal/api/route_inventory_test.go @@ -354,6 +354,9 @@ var allRouteAllowlist = []string{ "/api/resources", "/api/resources/stats", "/api/resources/", + "/api/v2/resources", + "/api/v2/resources/stats", + "/api/v2/resources/", "/api/guests/metadata", "/api/guests/metadata/", "/api/docker/metadata", diff --git a/internal/api/router_routes_monitoring.go b/internal/api/router_routes_monitoring.go index 753189449..0236e1e9d 100644 --- a/internal/api/router_routes_monitoring.go +++ b/internal/api/router_routes_monitoring.go @@ -7,6 +7,20 @@ import ( "github.com/rcourtman/pulse-go-rewrite/internal/config" ) +func deprecatedV2ResourceHandler(next http.HandlerFunc) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + // Back-compat shim for older clients. Canonical unified resources API is /api/resources. + w.Header().Set("Deprecation", "true") + + // Rewrite /api/v2/resources/... -> /api/resources/... + if strings.HasPrefix(r.URL.Path, "/api/v2/") { + r.URL.Path = "/api/" + strings.TrimPrefix(r.URL.Path, "/api/v2/") + } + + next(w, r) + } +} + func (r *Router) registerMonitoringResourceRoutes( guestMetadataHandler *GuestMetadataHandler, dockerMetadataHandler *DockerMetadataHandler, @@ -34,6 +48,10 @@ func (r *Router) registerMonitoringResourceRoutes( r.mux.HandleFunc("/api/resources", RequireAuth(r.config, RequireScope(config.ScopeMonitoringRead, r.resourceHandlers.HandleListResources))) r.mux.HandleFunc("/api/resources/stats", RequireAuth(r.config, RequireScope(config.ScopeMonitoringRead, r.resourceHandlers.HandleStats))) r.mux.HandleFunc("/api/resources/", RequireAuth(r.config, RequireScope(config.ScopeMonitoringRead, r.resourceHandlers.HandleResourceRoutes))) + // Deprecated v2 alias for unified resources (temporary compatibility shim). + r.mux.HandleFunc("/api/v2/resources", RequireAuth(r.config, RequireScope(config.ScopeMonitoringRead, deprecatedV2ResourceHandler(r.resourceHandlers.HandleListResources)))) + r.mux.HandleFunc("/api/v2/resources/stats", RequireAuth(r.config, RequireScope(config.ScopeMonitoringRead, deprecatedV2ResourceHandler(r.resourceHandlers.HandleStats)))) + r.mux.HandleFunc("/api/v2/resources/", RequireAuth(r.config, RequireScope(config.ScopeMonitoringRead, deprecatedV2ResourceHandler(r.resourceHandlers.HandleResourceRoutes)))) // Guest metadata routes r.mux.HandleFunc("/api/guests/metadata", RequireAuth(r.config, RequireScope(config.ScopeMonitoringRead, guestMetadataHandler.HandleGetMetadata))) r.mux.HandleFunc("/api/guests/metadata/", RequireAuth(r.config, func(w http.ResponseWriter, req *http.Request) { diff --git a/internal/api/router_routes_monitoring_test.go b/internal/api/router_routes_monitoring_test.go new file mode 100644 index 000000000..bb6d0dd67 --- /dev/null +++ b/internal/api/router_routes_monitoring_test.go @@ -0,0 +1,33 @@ +package api + +import ( + "net/http" + "net/http/httptest" + "testing" +) + +func TestDeprecatedV2ResourceHandler_RewritesPathAndSetsHeader(t *testing.T) { + rec := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodGet, "/api/v2/resources/host-1/children", nil) + + called := false + next := func(w http.ResponseWriter, r *http.Request) { + called = true + if r.URL.Path != "/api/resources/host-1/children" { + t.Fatalf("rewritten path = %q, want %q", r.URL.Path, "/api/resources/host-1/children") + } + w.WriteHeader(http.StatusOK) + } + + deprecatedV2ResourceHandler(next)(rec, req) + + if !called { + t.Fatalf("expected next handler to be called") + } + if got := rec.Header().Get("Deprecation"); got != "true" { + t.Fatalf("Deprecation header = %q, want %q", got, "true") + } + if rec.Code != http.StatusOK { + t.Fatalf("status = %d, want %d", rec.Code, http.StatusOK) + } +}