From 5da761f9687e8be3ad3a122cd4349c867f753b0c Mon Sep 17 00:00:00 2001 From: "pulse-triage[bot]" <249995291+pulse-triage[bot]@users.noreply.github.com> Date: Sun, 20 Sep 2026 01:47:42 +0100 Subject: [PATCH] fix(vmware): send VI JSON _typeName on data-object arguments The vSphere VI JSON API documents _typeName on request data objects. Calls whose only argument is a bare ManagedObjectReference (QueryPerfProviderSummary, QueryAvailablePerfMetric) succeed against vCenter 8.0.3, but every call that passes a composite data object (PerfQuerySpec/PerfMetricId for QueryPerf, EventFilterSpec/EventFilterSpecByEntity for QueryEvents) is rejected with HTTP 500. Issue #2070 shows exactly that split, so add the discriminators the documented schema carries to those request bodies. Morefs now carry _typeName too, matching the same schema. Focused tests pin the serialized request bodies. Change-source: pulse-maintainer --- internal/vmware/client_metrics.go | 30 ++++-- internal/vmware/client_signals.go | 15 ++- .../vmware/client_vi_json_request_test.go | 98 +++++++++++++++++++ 3 files changed, 130 insertions(+), 13 deletions(-) create mode 100644 internal/vmware/client_vi_json_request_test.go diff --git a/internal/vmware/client_metrics.go b/internal/vmware/client_metrics.go index 9ccb4f8230..7670eaf31c 100644 --- a/internal/vmware/client_metrics.go +++ b/internal/vmware/client_metrics.go @@ -38,6 +38,7 @@ type viJSONPerfProviderSummary struct { } type viJSONPerfMetricID struct { + TypeName string `json:"_typeName,omitempty"` CounterID int `json:"counterId"` Instance string `json:"instance"` } @@ -232,9 +233,10 @@ func (c *Client) queryPerfProviderSummary( var summary viJSONPerfProviderSummary path := fmt.Sprintf("/sdk/vim25/%s/PerformanceManager/%s/QueryPerfProviderSummary", release, perfManagerMoID) body := map[string]any{ - "entity": map[string]string{ - "type": strings.TrimSpace(entityType), - "value": strings.TrimSpace(entityMoID), + "entity": viJSONReference{ + TypeName: "ManagedObjectReference", + Type: strings.TrimSpace(entityType), + Value: strings.TrimSpace(entityMoID), }, } if err := c.postVIJSONJSON(ctx, sessionID, path, "vmware performance provider summary", body, &summary); err != nil { @@ -254,9 +256,10 @@ func (c *Client) queryAvailablePerfMetrics( ) ([]viJSONPerfMetricID, error) { path := fmt.Sprintf("/sdk/vim25/%s/PerformanceManager/%s/QueryAvailablePerfMetric", release, perfManagerMoID) body := map[string]any{ - "entity": map[string]string{ - "type": strings.TrimSpace(entityType), - "value": strings.TrimSpace(entityMoID), + "entity": viJSONReference{ + TypeName: "ManagedObjectReference", + Type: strings.TrimSpace(entityType), + Value: strings.TrimSpace(entityMoID), }, "intervalId": intervalID, } @@ -278,15 +281,22 @@ func (c *Client) queryPerfMetrics( metricIDs []viJSONPerfMetricID, ) ([]viJSONPerfEntityMetric, error) { path := fmt.Sprintf("/sdk/vim25/%s/PerformanceManager/%s/QueryPerf", release, perfManagerMoID) + metricRequests := make([]viJSONPerfMetricID, len(metricIDs)) + for i, metricID := range metricIDs { + metricID.TypeName = "PerfMetricId" + metricRequests[i] = metricID + } body := map[string]any{ "querySpec": []map[string]any{{ - "entity": map[string]string{ - "type": strings.TrimSpace(entityType), - "value": strings.TrimSpace(entityMoID), + "_typeName": "PerfQuerySpec", + "entity": viJSONReference{ + TypeName: "ManagedObjectReference", + Type: strings.TrimSpace(entityType), + Value: strings.TrimSpace(entityMoID), }, "intervalId": intervalID, "maxSample": 1, - "metricId": metricIDs, + "metricId": metricRequests, }}, } var result []viJSONPerfEntityMetric diff --git a/internal/vmware/client_signals.go b/internal/vmware/client_signals.go index f444642f5f..ed194380bc 100644 --- a/internal/vmware/client_signals.go +++ b/internal/vmware/client_signals.go @@ -11,9 +11,14 @@ import ( const vmwareSignalEnrichmentConcurrency = 8 +// viJSONReference is a ManagedObjectReference. TypeName carries the VI JSON +// API _typeName discriminator; the documented request schema includes it, and +// the API rejects a composite data object that omits its discriminator while +// accepting a bare moref without one. type viJSONReference struct { - Type string `json:"type"` - Value string `json:"value"` + TypeName string `json:"_typeName,omitempty"` + Type string `json:"type"` + Value string `json:"value"` } type viJSONLocalizedMessage struct { @@ -39,11 +44,13 @@ type viJSONTaskInfo struct { } type viJSONEventFilterSpec struct { + TypeName string `json:"_typeName,omitempty"` Entity *viJSONEventFilterEntity `json:"entity,omitempty"` MaxCount int `json:"maxCount,omitempty"` } type viJSONEventFilterEntity struct { + TypeName string `json:"_typeName,omitempty"` Entity viJSONReference `json:"entity"` Recursion string `json:"recursion,omitempty"` } @@ -530,8 +537,10 @@ func (c *Client) collectRecentEvents(ctx context.Context, release, sessionID, ev path := fmt.Sprintf("/sdk/vim25/%s/EventManager/%s/QueryEvents", release, eventManagerMoID) body := map[string]any{ "filter": viJSONEventFilterSpec{ + TypeName: "EventFilterSpec", Entity: &viJSONEventFilterEntity{ - Entity: viJSONReference{Type: strings.TrimSpace(managedType), Value: strings.TrimSpace(managedObjectID)}, + TypeName: "EventFilterSpecByEntity", + Entity: viJSONReference{TypeName: "ManagedObjectReference", Type: strings.TrimSpace(managedType), Value: strings.TrimSpace(managedObjectID)}, Recursion: "self", }, MaxCount: 3, diff --git a/internal/vmware/client_vi_json_request_test.go b/internal/vmware/client_vi_json_request_test.go new file mode 100644 index 0000000000..df70ea0ed5 --- /dev/null +++ b/internal/vmware/client_vi_json_request_test.go @@ -0,0 +1,98 @@ +package vmware + +import ( + "context" + "io" + "net/http" + "net/http/httptest" + "strings" + "testing" + "time" +) + +// The vSphere VI JSON API documents a `_typeName` discriminator on request data +// objects. A bare ManagedObjectReference is accepted without one, but the +// composite argument objects (PerfQuerySpec, PerfMetricId, EventFilterSpec and +// its nested EventFilterSpecByEntity) are rejected with HTTP 500 when the +// discriminator is absent. These tests pin the exact request bodies that +// vCenter 8.0.3 rejected for issue #2070. + +func newVIJSONRequestClient(t *testing.T, handler http.Handler) (*Client, func()) { + t.Helper() + server := httptest.NewTLSServer(handler) + client, err := NewClient(ClientConfig{ + Host: server.URL, + Username: "admin", + Password: "secret", + InsecureSkipVerify: true, + Timeout: 5 * time.Second, + }) + if err != nil { + server.Close() + t.Fatalf("NewClient: %v", err) + } + return client, server.Close +} + +func TestQueryPerfRequestCarriesTypeNameDiscriminators(t *testing.T) { + var raw []byte + mux := http.NewServeMux() + mux.HandleFunc("/sdk/vim25/9.0.0.0/PerformanceManager/PerformanceManager/QueryPerf", func(w http.ResponseWriter, r *http.Request) { + raw, _ = io.ReadAll(r.Body) + writeJSON(w, []map[string]any{}) + }) + client, closeServer := newVIJSONRequestClient(t, mux) + defer closeServer() + + _, err := client.queryPerfMetrics( + context.Background(), + "9.0.0.0", + "vi-session", + "PerformanceManager", + "HostSystem", + "host-101", + 20, + []viJSONPerfMetricID{{CounterID: 1, Instance: ""}}, + ) + if err != nil { + t.Fatalf("queryPerfMetrics: %v", err) + } + + body := string(raw) + for _, want := range []string{ + `"_typeName":"PerfQuerySpec"`, + `"_typeName":"ManagedObjectReference"`, + `"_typeName":"PerfMetricId"`, + } { + if !strings.Contains(body, want) { + t.Fatalf("QueryPerf request %s missing %s", body, want) + } + } +} + +func TestQueryEventsRequestCarriesTypeNameDiscriminators(t *testing.T) { + var raw []byte + mux := http.NewServeMux() + mux.HandleFunc("/sdk/vim25/9.0.0.0/EventManager/EventManager/QueryEvents", func(w http.ResponseWriter, r *http.Request) { + raw, _ = io.ReadAll(r.Body) + writeJSON(w, []map[string]any{}) + }) + client, closeServer := newVIJSONRequestClient(t, mux) + defer closeServer() + + _, err := client.collectRecentEvents(context.Background(), "9.0.0.0", "vi-session", "EventManager", "Network", "network-14") + if err != nil { + t.Fatalf("collectRecentEvents: %v", err) + } + + body := string(raw) + for _, want := range []string{ + `"_typeName":"EventFilterSpec"`, + `"_typeName":"EventFilterSpecByEntity"`, + `"_typeName":"ManagedObjectReference"`, + } { + if !strings.Contains(body, want) { + t.Fatalf("QueryEvents request %s missing %s", body, want) + } + } +}