This is an automated email from the ASF dual-hosted git repository.
abh1sar pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/cloudstack-go.git
The following commit(s) were added to refs/heads/main by this push:
new 340f0b1 Fix list-response JSON tags and guard getRawValue against
empty arrays (#154)
340f0b1 is described below
commit 340f0b17b9e212ca40a0f11ac2e17a1d7b948597
Author: James Peru Mmbono <[email protected]>
AuthorDate: Wed Aug 5 19:08:00 2026 +0300
Fix list-response JSON tags and guard getRawValue against empty arrays
(#154)
Several List<X>Response structs use a json tag that does not match the key
CloudStack actually returns (the generator derives it from parseSingular()),
so Count parses but the slice stays nil — silent data loss. Verified against
the CloudStack source object names (setObjectName):
- listHypervisorCapabilities: hypervisorcapability -> hypervisorCapabilities
- listGuestNetworkIpv6Prefixes: guestnetworkipv6prefixe ->
guestnetworkipv6prefix
- listLBHealthCheckPolicies: lbhealthcheckpolicy -> healthcheckpolicies
- listLBStickinessPolicies: lbstickinesspolicy -> stickinesspolicies
Fixed via explicit cases in the generator's override switch (alongside the
existing metrics cases) and the regenerated tags.
Also guards getRawValue() against a count-wrapped response carrying an empty
data array, which previously panicked on resp[0] (index out of range); it
now
returns a descriptive error.
Adds regression tests for both.
Signed-off-by: James Peru <[email protected]>
Co-authored-by: James Peru <[email protected]>
---
cloudstack/GetRawValueGuard_test.go | 36 ++++++++++++++
cloudstack/HypervisorService.go | 2 +-
cloudstack/LoadBalancerService.go | 2 +-
cloudstack/NetworkService.go | 2 +-
cloudstack/cloudstack.go | 3 ++
generate/generate.go | 9 ++++
test/ListResponseJSONTagsRegression_test.go | 73 +++++++++++++++++++++++++++++
7 files changed, 124 insertions(+), 3 deletions(-)
diff --git a/cloudstack/GetRawValueGuard_test.go
b/cloudstack/GetRawValueGuard_test.go
new file mode 100644
index 0000000..8fa4bae
--- /dev/null
+++ b/cloudstack/GetRawValueGuard_test.go
@@ -0,0 +1,36 @@
+//
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+//
+
+package cloudstack
+
+import "testing"
+
+// getRawValue must not panic when a count-wrapped response carries an empty
+// data array ({"count":0,"<entity>":[]}); it should return a descriptive
error.
+func TestGetRawValueEmptyArrayReturnsErrorNotPanic(t *testing.T) {
+ defer func() {
+ if r := recover(); r != nil {
+ t.Fatalf("getRawValue panicked on a count-wrapped empty
array: %v", r)
+ }
+ }()
+ _, err := getRawValue([]byte(`{"count":0,"entity":[]}`))
+ if err == nil {
+ t.Fatal("expected an error for a count-wrapped empty array, got
nil")
+ }
+}
diff --git a/cloudstack/HypervisorService.go b/cloudstack/HypervisorService.go
index 605070e..688b203 100644
--- a/cloudstack/HypervisorService.go
+++ b/cloudstack/HypervisorService.go
@@ -229,7 +229,7 @@ func (s *HypervisorService) ListHypervisorCapabilities(p
*ListHypervisorCapabili
type ListHypervisorCapabilitiesResponse struct {
Count int `json:"count"`
- HypervisorCapabilities []*HypervisorCapability
`json:"hypervisorcapability"`
+ HypervisorCapabilities []*HypervisorCapability
`json:"hypervisorCapabilities"`
}
type HypervisorCapability struct {
diff --git a/cloudstack/LoadBalancerService.go
b/cloudstack/LoadBalancerService.go
index a6afb81..18997dc 100644
--- a/cloudstack/LoadBalancerService.go
+++ b/cloudstack/LoadBalancerService.go
@@ -3539,7 +3539,7 @@ func (s *LoadBalancerService) ListLBHealthCheckPolicies(p
*ListLBHealthCheckPoli
type ListLBHealthCheckPoliciesResponse struct {
Count int `json:"count"`
- LBHealthCheckPolicies []*LBHealthCheckPolicy
`json:"lbhealthcheckpolicy"`
+ LBHealthCheckPolicies []*LBHealthCheckPolicy
`json:"healthcheckpolicies"`
}
type LBHealthCheckPolicy struct {
diff --git a/cloudstack/NetworkService.go b/cloudstack/NetworkService.go
index 96f1a2e..cccb28f 100644
--- a/cloudstack/NetworkService.go
+++ b/cloudstack/NetworkService.go
@@ -8475,7 +8475,7 @@ func (s *NetworkService) ListGuestNetworkIpv6Prefixes(p
*ListGuestNetworkIpv6Pre
type ListGuestNetworkIpv6PrefixesResponse struct {
Count int `json:"count"`
- GuestNetworkIpv6Prefixes []*GuestNetworkIpv6Prefixe
`json:"guestnetworkipv6prefixe"`
+ GuestNetworkIpv6Prefixes []*GuestNetworkIpv6Prefixe
`json:"guestnetworkipv6prefix"`
}
type GuestNetworkIpv6Prefixe struct {
diff --git a/cloudstack/cloudstack.go b/cloudstack/cloudstack.go
index e16a934..240174e 100644
--- a/cloudstack/cloudstack.go
+++ b/cloudstack/cloudstack.go
@@ -653,6 +653,9 @@ func getRawValue(b json.RawMessage) (json.RawMessage,
error) {
if err := json.Unmarshal(v, &resp); err != nil {
return nil, err
}
+ if len(resp) == 0 {
+ return nil, fmt.Errorf("Unable to
extract raw value: empty array for key %q in:\n\n%s\n\n", k, string(b))
+ }
return resp[0], nil
}
}
diff --git a/generate/generate.go b/generate/generate.go
index 52577e0..a11213d 100644
--- a/generate/generate.go
+++ b/generate/generate.go
@@ -2111,6 +2111,15 @@ func (s *service) generateResponseType(a *API) {
case "quotaSummary":
pn(" Count int `json:\"count\"`")
pn(" %s []*%s `json:\"%s\"`", ln, parseSingular(ln),
"summary")
+ case "listHypervisorCapabilities":
+ pn(" Count int `json:\"count\"`")
+ pn(" %s []*%s `json:\"%s\"`", ln, parseSingular(ln),
"hypervisorCapabilities")
+ case "listGuestNetworkIpv6Prefixes":
+ pn(" Count int `json:\"count\"`")
+ pn(" %s []*%s `json:\"%s\"`", ln, parseSingular(ln),
"guestnetworkipv6prefix")
+ case "listLBHealthCheckPolicies":
+ pn(" Count int `json:\"count\"`")
+ pn(" %s []*%s `json:\"%s\"`", ln, parseSingular(ln),
"healthcheckpolicies")
case "listLBStickinessPolicies":
pn(" Count int `json:\"count\"`")
pn(" %s []*%s `json:\"%s\"`", ln, parseSingular(ln),
"stickinesspolicies")
diff --git a/test/ListResponseJSONTagsRegression_test.go
b/test/ListResponseJSONTagsRegression_test.go
new file mode 100644
index 0000000..1552328
--- /dev/null
+++ b/test/ListResponseJSONTagsRegression_test.go
@@ -0,0 +1,73 @@
+//
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+//
+
+package test
+
+import (
+ "encoding/json"
+ "testing"
+
+ "github.com/apache/cloudstack-go/v2/cloudstack"
+)
+
+// Regression test for list-response JSON-tag mismatches: the response struct
+// tag must match the object key CloudStack actually returns, otherwise Count
+// parses but the slice stays nil (silent data loss). Keys below are the
+// authoritative server object names (setObjectName in the CloudStack source).
+func TestListResponseJSONTagsPopulateSlices(t *testing.T) {
+ t.Run("HypervisorCapabilities", func(t *testing.T) {
+ var r cloudstack.ListHypervisorCapabilitiesResponse
+ if err :=
json.Unmarshal([]byte(`{"count":1,"hypervisorCapabilities":[{"id":"h1"}]}`),
&r); err != nil {
+ t.Fatal(err)
+ }
+ if len(r.HypervisorCapabilities) != 1 {
+ t.Fatalf("expected 1 item under key
'hypervisorCapabilities', got %d (nil slice = wrong json tag)",
len(r.HypervisorCapabilities))
+ }
+ })
+
+ t.Run("GuestNetworkIpv6Prefixes", func(t *testing.T) {
+ var r cloudstack.ListGuestNetworkIpv6PrefixesResponse
+ if err :=
json.Unmarshal([]byte(`{"count":1,"guestnetworkipv6prefix":[{"id":"p1"}]}`),
&r); err != nil {
+ t.Fatal(err)
+ }
+ if len(r.GuestNetworkIpv6Prefixes) != 1 {
+ t.Fatalf("expected 1 item under key
'guestnetworkipv6prefix', got %d", len(r.GuestNetworkIpv6Prefixes))
+ }
+ })
+
+ t.Run("LBHealthCheckPolicies", func(t *testing.T) {
+ var r cloudstack.ListLBHealthCheckPoliciesResponse
+ if err :=
json.Unmarshal([]byte(`{"count":1,"healthcheckpolicies":[{"lbruleid":"r1"}]}`),
&r); err != nil {
+ t.Fatal(err)
+ }
+ if len(r.LBHealthCheckPolicies) != 1 {
+ t.Fatalf("expected 1 item under key
'healthcheckpolicies', got %d", len(r.LBHealthCheckPolicies))
+ }
+ })
+
+ t.Run("LBStickinessPolicies", func(t *testing.T) {
+ var r cloudstack.ListLBStickinessPoliciesResponse
+ if err :=
json.Unmarshal([]byte(`{"count":1,"stickinesspolicies":[{"lbruleid":"r1"}]}`),
&r); err != nil {
+ t.Fatal(err)
+ }
+ if len(r.LBStickinessPolicies) != 1 {
+ t.Fatalf("expected 1 item under key
'stickinesspolicies', got %d", len(r.LBStickinessPolicies))
+ }
+ })
+}