mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-11 14:00:29 +00:00
Remove dead capability and relationship endpoints
This commit is contained in:
@@ -96,13 +96,13 @@ Those unified audit list endpoints also clamp oversized `limit` requests to
|
||||
the governed maximum, so audit history stays bounded even when callers ask
|
||||
for arbitrarily large pages.
|
||||
The same shared API runtime now also exposes dedicated unified-resource
|
||||
capability, relationship, and timeline reads through
|
||||
`internal/api/resources.go`, but those query surfaces remain owned by the API
|
||||
and unified-resource contracts rather than by lifecycle continuity.
|
||||
Those same dedicated reads also accept governed timeline filters for change
|
||||
kind, source type, and source adapter, and the underlying store owns the
|
||||
filtered counts so agent lifecycle routing still stays on canonical
|
||||
fleet-continuity ownership instead of re-deriving resource history locally.
|
||||
timeline reads through `internal/api/resources.go` plus the bundled facet
|
||||
history read used by the drawer, but those query surfaces remain owned by the
|
||||
API and unified-resource contracts rather than by lifecycle continuity.
|
||||
Those timeline reads also accept governed filters for change kind, source
|
||||
type, and source adapter, and the underlying store owns the filtered counts so
|
||||
agent lifecycle routing still stays on canonical fleet-continuity ownership
|
||||
instead of re-deriving resource history locally.
|
||||
The same API serializer now also refreshes canonical identity and policy
|
||||
metadata through the shared unified-resource helper before it returns
|
||||
resource payloads, so lifecycle-adjacent links keep the same canonical
|
||||
@@ -1064,7 +1064,7 @@ policy-aware resource metadata. Agent lifecycle and fleet-control surfaces may
|
||||
consume canonical `policy` and `aiSafeSummary` fields from unified resource
|
||||
payloads when they need resource context, but they must not fork their own
|
||||
sensitivity-classification or local-vs-cloud routing heuristics on the same
|
||||
runtime boundary. The same shared resource boundary now also owns the canonical
|
||||
facet-bundle read path for capabilities, relationships, and timeline history,
|
||||
so fleet lifecycle surfaces that open resource drawers must continue to consume
|
||||
the backend bundle instead of reassembling a local multi-call summary.
|
||||
runtime boundary. The same shared resource boundary now also owns the bundled
|
||||
facet history read path for timeline data, so fleet lifecycle surfaces that
|
||||
open resource drawers must continue to consume the backend bundle instead of
|
||||
reassembling a local multi-call summary.
|
||||
|
||||
@@ -134,10 +134,10 @@ through the owned backend response: resource objects can expose canonical
|
||||
in addition to policy and identity metadata, so the backend payload contract
|
||||
stays aligned with the
|
||||
timeline and control-plane model instead of flattening those fields away.
|
||||
The same resource contract now also exposes dedicated facet endpoints for
|
||||
`/api/resources/{id}/capabilities`, `/api/resources/{id}/relationships`, and
|
||||
`/api/resources/{id}/timeline`, so operators can read the graph and change
|
||||
history without depending on a monolithic resource payload.
|
||||
The same resource contract now also exposes a dedicated
|
||||
`/api/resources/{id}/timeline` history endpoint and bundled facet reads under
|
||||
`/api/resources/{id}/facets`, so operators can inspect change history without
|
||||
depending on a monolithic resource payload.
|
||||
The `/api/resources` serializer now also refreshes canonical identity and
|
||||
policy metadata through the shared unified-resource helper before it writes
|
||||
the payload, so backend and frontend contract tests stay aligned on one
|
||||
|
||||
@@ -150,8 +150,8 @@ Those unified audit list endpoints also clamp oversized `limit` requests to
|
||||
the governed maximum, so adjacent recovery and storage workflows do not turn
|
||||
bounded history reads into unbounded collection scans.
|
||||
The same shared API runtime now also exposes dedicated
|
||||
`/api/resources/{id}/capabilities`, `/api/resources/{id}/relationships`, and
|
||||
`/api/resources/{id}/timeline` reads, but storage and recovery must continue
|
||||
`/api/resources/{id}/timeline` reads plus the bundled
|
||||
`/api/resources/{id}/facets` surface, but storage and recovery must continue
|
||||
to treat those as adjacent governed API ownership rather than storage/recovery
|
||||
timeline ownership.
|
||||
Those resource timeline reads now also accept governed kind and source-type
|
||||
|
||||
@@ -200,10 +200,9 @@ the pre-v6 `timestamp` column by backfilling canonical `observed_at` values,
|
||||
adding the newer `occurred_at` field, and preserving the legacy timestamp on
|
||||
write when the target database still requires it.
|
||||
`internal/api/resources.go` now exposes that same history through dedicated
|
||||
`/api/resources/{id}/timeline` reads, while `/api/resources/{id}/capabilities`
|
||||
and `/api/resources/{id}/relationships` expose the current graph facets as
|
||||
separate queryable surfaces instead of forcing consumers to parse the full
|
||||
resource payload.
|
||||
`/api/resources/{id}/timeline` reads, while the bundled `/api/resources/{id}/facets`
|
||||
surface keeps the facet summary and recent-change history available without
|
||||
forcing consumers to parse the full resource payload.
|
||||
Those filtered timeline reads are backed by dedicated `resource_changes`
|
||||
indexes on `canonical_id`, `kind`, `source_type`, and `observed_at`, so the
|
||||
canonical history path stays fast as the filtered timeline grows instead of
|
||||
|
||||
@@ -11,30 +11,9 @@ describe('ResourceAPI', () => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('fetches capabilities with the canonical facet endpoint', async () => {
|
||||
it('fetches the resource history bundle from the facet endpoint', async () => {
|
||||
vi.mocked(apiFetchJSON).mockResolvedValueOnce({
|
||||
resourceId: 'vm:42',
|
||||
capabilities: [],
|
||||
count: 0,
|
||||
} as any);
|
||||
|
||||
const result = await ResourceAPI.getCapabilities(' vm:42 ');
|
||||
|
||||
expect(apiFetchJSON).toHaveBeenCalledWith('/api/resources/vm%3A42/capabilities', {
|
||||
cache: 'no-store',
|
||||
});
|
||||
expect(result).toEqual({
|
||||
resourceId: 'vm:42',
|
||||
capabilities: [],
|
||||
count: 0,
|
||||
});
|
||||
});
|
||||
|
||||
it('fetches the resource history bundle from the dedicated facet endpoints', async () => {
|
||||
vi.mocked(apiFetchJSON).mockResolvedValueOnce({
|
||||
resourceId: 'vm:42',
|
||||
capabilities: [{ name: 'restart' }],
|
||||
relationships: [{ sourceId: 'node:1', targetId: 'vm:42' }],
|
||||
recentChanges: [{ id: 'change-1' }],
|
||||
counts: {
|
||||
capabilities: 1,
|
||||
@@ -71,8 +50,6 @@ describe('ResourceAPI', () => {
|
||||
);
|
||||
expect(result).toEqual({
|
||||
resourceId: 'vm:42',
|
||||
capabilities: [{ name: 'restart' }],
|
||||
relationships: [{ sourceId: 'node:1', targetId: 'vm:42' }],
|
||||
recentChanges: [{ id: 'change-1' }],
|
||||
counts: {
|
||||
capabilities: 1,
|
||||
|
||||
@@ -1,26 +1,12 @@
|
||||
import { apiFetchJSON } from '@/utils/apiClient';
|
||||
import type {
|
||||
ResourceCapability,
|
||||
ResourceChange,
|
||||
ResourceChangeKind,
|
||||
ResourceChangeSourceAdapter,
|
||||
ResourceChangeSourceType,
|
||||
ResourceFacetCounts,
|
||||
ResourceRelationship,
|
||||
} from '@/types/resource';
|
||||
|
||||
export interface ResourceCapabilitiesResponse {
|
||||
resourceId: string;
|
||||
capabilities: ResourceCapability[];
|
||||
count: number;
|
||||
}
|
||||
|
||||
export interface ResourceRelationshipsResponse {
|
||||
resourceId: string;
|
||||
relationships: ResourceRelationship[];
|
||||
count: number;
|
||||
}
|
||||
|
||||
export interface ResourceTimelineQueryOptions {
|
||||
since?: string | number | Date;
|
||||
limit?: number;
|
||||
@@ -36,8 +22,6 @@ export interface ResourceTimelineResponse {
|
||||
}
|
||||
|
||||
export interface ResourceFacetBundle {
|
||||
capabilities: ResourceCapability[];
|
||||
relationships: ResourceRelationship[];
|
||||
recentChanges: ResourceChange[];
|
||||
counts: ResourceFacetCounts;
|
||||
}
|
||||
@@ -77,14 +61,6 @@ const fetchFacet = async <T>(url: string): Promise<T> =>
|
||||
});
|
||||
|
||||
export class ResourceAPI {
|
||||
static async getCapabilities(resourceId: string): Promise<ResourceCapabilitiesResponse> {
|
||||
return fetchFacet<ResourceCapabilitiesResponse>(buildFacetPath(resourceId, 'capabilities'));
|
||||
}
|
||||
|
||||
static async getRelationships(resourceId: string): Promise<ResourceRelationshipsResponse> {
|
||||
return fetchFacet<ResourceRelationshipsResponse>(buildFacetPath(resourceId, 'relationships'));
|
||||
}
|
||||
|
||||
static async getTimeline(
|
||||
resourceId: string,
|
||||
options?: ResourceTimelineQueryOptions,
|
||||
|
||||
@@ -291,14 +291,6 @@ func (h *ResourceHandlers) HandleResourceRoutes(w http.ResponseWriter, r *http.R
|
||||
h.HandleGetResourceFacets(w, r)
|
||||
return
|
||||
}
|
||||
if strings.HasSuffix(r.URL.Path, "/capabilities") {
|
||||
h.HandleGetResourceCapabilities(w, r)
|
||||
return
|
||||
}
|
||||
if strings.HasSuffix(r.URL.Path, "/relationships") {
|
||||
h.HandleGetResourceRelationships(w, r)
|
||||
return
|
||||
}
|
||||
if strings.HasSuffix(r.URL.Path, "/timeline") {
|
||||
h.HandleGetResourceTimeline(w, r)
|
||||
return
|
||||
@@ -502,90 +494,6 @@ func (h *ResourceHandlers) HandleGetMetrics(w http.ResponseWriter, r *http.Reque
|
||||
json.NewEncoder(w).Encode(resource.Metrics)
|
||||
}
|
||||
|
||||
// HandleGetResourceCapabilities handles GET /api/resources/{id}/capabilities.
|
||||
func (h *ResourceHandlers) HandleGetResourceCapabilities(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
orgID := GetOrgID(r.Context())
|
||||
registry, err := h.buildRegistry(orgID)
|
||||
if err != nil {
|
||||
http.Error(w, sanitizeErrorForClient(err, "Internal server error"), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
resourceID := strings.TrimPrefix(r.URL.Path, "/api/resources/")
|
||||
resourceID = strings.TrimSuffix(resourceID, "/capabilities")
|
||||
resourceID = strings.TrimSuffix(resourceID, "/")
|
||||
resourceID = unified.CanonicalResourceID(resourceID)
|
||||
if resourceID == "" {
|
||||
http.Error(w, "Resource ID required", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
resource, ok := registry.Get(resourceID)
|
||||
if !ok {
|
||||
http.Error(w, "Resource not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
capabilities := resource.Capabilities
|
||||
if capabilities == nil {
|
||||
capabilities = []unified.ResourceCapability{}
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(map[string]any{
|
||||
"resourceId": resourceID,
|
||||
"capabilities": capabilities,
|
||||
"count": resource.FacetCounts.Capabilities,
|
||||
})
|
||||
}
|
||||
|
||||
// HandleGetResourceRelationships handles GET /api/resources/{id}/relationships.
|
||||
func (h *ResourceHandlers) HandleGetResourceRelationships(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
orgID := GetOrgID(r.Context())
|
||||
registry, err := h.buildRegistry(orgID)
|
||||
if err != nil {
|
||||
http.Error(w, sanitizeErrorForClient(err, "Internal server error"), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
resourceID := strings.TrimPrefix(r.URL.Path, "/api/resources/")
|
||||
resourceID = strings.TrimSuffix(resourceID, "/relationships")
|
||||
resourceID = strings.TrimSuffix(resourceID, "/")
|
||||
resourceID = unified.CanonicalResourceID(resourceID)
|
||||
if resourceID == "" {
|
||||
http.Error(w, "Resource ID required", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
resource, ok := registry.Get(resourceID)
|
||||
if !ok {
|
||||
http.Error(w, "Resource not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
relationships := resource.Relationships
|
||||
if relationships == nil {
|
||||
relationships = []unified.ResourceRelationship{}
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(map[string]any{
|
||||
"resourceId": resourceID,
|
||||
"relationships": relationships,
|
||||
"count": resource.FacetCounts.Relationships,
|
||||
})
|
||||
}
|
||||
|
||||
// HandleGetResourceTimeline handles GET /api/resources/{id}/timeline.
|
||||
func (h *ResourceHandlers) HandleGetResourceTimeline(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
|
||||
@@ -1020,49 +1020,6 @@ func TestResourceGetFacetsAndTimeline(t *testing.T) {
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("capabilities", func(t *testing.T) {
|
||||
rec := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/resources/vm:42/capabilities", nil)
|
||||
h.HandleResourceRoutes(rec, req)
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("status = %d, body=%s", rec.Code, rec.Body.String())
|
||||
}
|
||||
var payload struct {
|
||||
ResourceID string `json:"resourceId"`
|
||||
Capabilities []unified.ResourceCapability `json:"capabilities"`
|
||||
Count int `json:"count"`
|
||||
}
|
||||
if err := json.NewDecoder(rec.Body).Decode(&payload); err != nil {
|
||||
t.Fatalf("decode capabilities: %v", err)
|
||||
}
|
||||
if payload.ResourceID != "vm:42" || payload.Count != 1 || len(payload.Capabilities) != 1 {
|
||||
t.Fatalf("unexpected capabilities payload: %#v", payload)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("relationships", func(t *testing.T) {
|
||||
rec := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/resources/vm:42/relationships", nil)
|
||||
h.HandleResourceRoutes(rec, req)
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("status = %d, body=%s", rec.Code, rec.Body.String())
|
||||
}
|
||||
var payload struct {
|
||||
ResourceID string `json:"resourceId"`
|
||||
Relationships []unified.ResourceRelationship `json:"relationships"`
|
||||
Count int `json:"count"`
|
||||
}
|
||||
if err := json.NewDecoder(rec.Body).Decode(&payload); err != nil {
|
||||
t.Fatalf("decode relationships: %v", err)
|
||||
}
|
||||
if payload.ResourceID != "vm:42" || payload.Count != 1 || len(payload.Relationships) != 1 {
|
||||
t.Fatalf("unexpected relationships payload: %#v", payload)
|
||||
}
|
||||
if got := payload.Relationships[0].Metadata["cluster"]; got != "pve-prod" {
|
||||
t.Fatalf("unexpected relationship metadata: %#v", payload.Relationships[0].Metadata)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("timeline", func(t *testing.T) {
|
||||
rec := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/resources/vm:42/timeline?limit=10", nil)
|
||||
|
||||
@@ -354,8 +354,6 @@ var allRouteAllowlist = []string{
|
||||
"/api/resources/stats",
|
||||
"/api/resources/",
|
||||
"/api/resources/{id}/facets",
|
||||
"/api/resources/{id}/capabilities",
|
||||
"/api/resources/{id}/relationships",
|
||||
"/api/resources/{id}/timeline",
|
||||
"/api/guests/metadata",
|
||||
"/api/guests/metadata/",
|
||||
|
||||
@@ -34,8 +34,6 @@ func (r *Router) registerMonitoringResourceRoutes(
|
||||
r.mux.HandleFunc("/api/resources/stats", RequireAuth(r.config, RequireScope(config.ScopeMonitoringRead, r.resourceHandlers.HandleStats)))
|
||||
r.mux.HandleFunc("/api/resources/k8s/namespaces", RequireAuth(r.config, RequireScope(config.ScopeMonitoringRead, r.resourceHandlers.HandleK8sNamespaces)))
|
||||
r.mux.HandleFunc("/api/resources/{id}/facets", RequireAuth(r.config, RequireScope(config.ScopeMonitoringRead, r.resourceHandlers.HandleGetResourceFacets)))
|
||||
r.mux.HandleFunc("/api/resources/{id}/capabilities", RequireAuth(r.config, RequireScope(config.ScopeMonitoringRead, r.resourceHandlers.HandleGetResourceCapabilities)))
|
||||
r.mux.HandleFunc("/api/resources/{id}/relationships", RequireAuth(r.config, RequireScope(config.ScopeMonitoringRead, r.resourceHandlers.HandleGetResourceRelationships)))
|
||||
r.mux.HandleFunc("/api/resources/{id}/timeline", RequireAuth(r.config, RequireScope(config.ScopeMonitoringRead, r.resourceHandlers.HandleGetResourceTimeline)))
|
||||
r.mux.HandleFunc("/api/resources/", RequireAuth(r.config, RequireScope(config.ScopeMonitoringRead, r.resourceHandlers.HandleResourceRoutes)))
|
||||
// Guest metadata routes
|
||||
|
||||
@@ -242,8 +242,6 @@ func TestResourceAPIExposesDedicatedFacetReads(t *testing.T) {
|
||||
|
||||
requiredSnippets := []string{
|
||||
"HandleGetResourceFacets",
|
||||
"HandleGetResourceCapabilities",
|
||||
"HandleGetResourceRelationships",
|
||||
"HandleGetResourceTimeline",
|
||||
"unified.ParseResourceChangeFilters(r.URL.Query()[\"kind\"], r.URL.Query()[\"sourceType\"], r.URL.Query()[\"sourceAdapter\"])",
|
||||
"GetRecentChangesFiltered(resourceID, since, limit, filters)",
|
||||
@@ -252,8 +250,6 @@ func TestResourceAPIExposesDedicatedFacetReads(t *testing.T) {
|
||||
"CountRecentChangesBySourceTypeFiltered(resourceID, since, filters)",
|
||||
"sourceAdapter",
|
||||
"strings.HasSuffix(r.URL.Path, \"/facets\")",
|
||||
"strings.HasSuffix(r.URL.Path, \"/capabilities\")",
|
||||
"strings.HasSuffix(r.URL.Path, \"/relationships\")",
|
||||
"strings.HasSuffix(r.URL.Path, \"/timeline\")",
|
||||
}
|
||||
for _, snippet := range requiredSnippets {
|
||||
|
||||
Reference in New Issue
Block a user