Skip to content

Commit 21c5ded

Browse files
committed
fix: system prune JSON unmarshalling error in remote client
Add custom JSON methods to PruneReport to handle error field marshalling. Fixes: #27267 Signed-off-by: Jan Rodák <[email protected]>
1 parent 3e85b2e commit 21c5ded

File tree

3 files changed

+211
-0
lines changed

3 files changed

+211
-0
lines changed

pkg/bindings/errors_test.go

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
package bindings
2+
3+
import (
4+
"bytes"
5+
"io"
6+
"net/http"
7+
"testing"
8+
9+
"github.com/containers/podman/v5/pkg/domain/entities/types"
10+
. "github.com/onsi/ginkgo/v2"
11+
. "github.com/onsi/gomega"
12+
)
13+
14+
func TestBindings(t *testing.T) {
15+
RegisterFailHandler(Fail)
16+
RunSpecs(t, "Bindings Suite")
17+
}
18+
19+
var _ = Describe("APIResponse Process method", func() {
20+
21+
createMockResponse := func(jsonResponse string, statusCode int) *APIResponse {
22+
response := &http.Response{
23+
StatusCode: statusCode,
24+
Body: io.NopCloser(bytes.NewBufferString(jsonResponse)),
25+
Header: make(http.Header),
26+
}
27+
response.Header.Set("Content-Type", "application/json")
28+
return &APIResponse{Response: response}
29+
}
30+
31+
Describe("when processing SystemPruneReport", func() {
32+
Context("with the exact JSON that caused the original marshalling error", func() {
33+
It("should successfully unmarshal the response", func() {
34+
// This is the exact JSON that was causing the unmarshalling error
35+
jsonResponse := `{
36+
"PodPruneReport": null,
37+
"ContainerPruneReports": [
38+
{
39+
"Id": "aec04392e9b2fe7c4a36bc0cfa206dee35d7e403f7189df658ce909ccd598db7",
40+
"Size": 8219
41+
},
42+
{
43+
"Id": "3d8a8789524a0c44a61baa49ceedda7be069b0b3d01255b24013d2fb82168c7e",
44+
"Err": "replacing mount point \"/tmp/CI_7Qsy/podman-e2e-213135586/subtest-1767990215/p/root/overlay/d9f554276b923c07bf708858b5f35774f9d2924fa4094b1583e56b33ae357af1/merged\": directory not empty",
45+
"Size": 7238
46+
},
47+
{
48+
"Id": "e9ef46f3a3cd43c929b19a01013be4d052bcb228333e61dcb8eb7dd270ae44c2",
49+
"Size": 0
50+
}
51+
],
52+
"ImagePruneReports": null,
53+
"NetworkPruneReports": null,
54+
"VolumePruneReports": null,
55+
"ReclaimedSpace": 15457
56+
}`
57+
58+
apiResponse := createMockResponse(jsonResponse, 200)
59+
var report types.SystemPruneReport
60+
61+
err := apiResponse.Process(&report)
62+
Expect(err).ToNot(HaveOccurred())
63+
64+
Expect(report.ContainerPruneReports).To(HaveLen(3))
65+
Expect(report.ReclaimedSpace).To(Equal(uint64(15457)))
66+
67+
first := report.ContainerPruneReports[0]
68+
Expect(first.Id).To(Equal("aec04392e9b2fe7c4a36bc0cfa206dee35d7e403f7189df658ce909ccd598db7"))
69+
Expect(first.Size).To(Equal(uint64(8219)))
70+
Expect(first.Err).To(BeNil())
71+
72+
second := report.ContainerPruneReports[1]
73+
Expect(second.Id).To(Equal("3d8a8789524a0c44a61baa49ceedda7be069b0b3d01255b24013d2fb82168c7e"))
74+
Expect(second.Size).To(Equal(uint64(7238)))
75+
Expect(second.Err).ToNot(BeNil())
76+
expectedErr := `replacing mount point "/tmp/CI_7Qsy/podman-e2e-213135586/subtest-1767990215/p/root/overlay/d9f554276b923c07bf708858b5f35774f9d2924fa4094b1583e56b33ae357af1/merged": directory not empty`
77+
Expect(second.Err.Error()).To(Equal(expectedErr))
78+
79+
third := report.ContainerPruneReports[2]
80+
Expect(third.Id).To(Equal("e9ef46f3a3cd43c929b19a01013be4d052bcb228333e61dcb8eb7dd270ae44c2"))
81+
Expect(third.Size).To(Equal(uint64(0)))
82+
Expect(third.Err).To(BeNil())
83+
})
84+
})
85+
})
86+
})

pkg/domain/entities/reports/prune.go

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,49 @@
11
package reports
22

3+
import (
4+
"encoding/json"
5+
"errors"
6+
)
7+
38
type PruneReport struct {
49
Id string `json:"Id"`
510
Err error `json:"Err,omitempty"`
611
Size uint64 `json:"Size"`
712
}
813

14+
type pruneReportHelper struct {
15+
Id string `json:"Id"`
16+
Err string `json:"Err,omitempty"`
17+
Size uint64 `json:"Size"`
18+
}
19+
20+
func (pr *PruneReport) MarshalJSON() ([]byte, error) {
21+
helper := pruneReportHelper{
22+
Id: pr.Id,
23+
Size: pr.Size,
24+
}
25+
if pr.Err != nil {
26+
helper.Err = pr.Err.Error()
27+
}
28+
return json.Marshal(helper)
29+
}
30+
31+
func (pr *PruneReport) UnmarshalJSON(data []byte) error {
32+
var helper pruneReportHelper
33+
if err := json.Unmarshal(data, &helper); err != nil {
34+
return err
35+
}
36+
37+
pr.Id = helper.Id
38+
pr.Size = helper.Size
39+
if helper.Err != "" {
40+
pr.Err = errors.New(helper.Err)
41+
} else {
42+
pr.Err = nil
43+
}
44+
return nil
45+
}
46+
947
func PruneReportsIds(r []*PruneReport) []string {
1048
ids := make([]string, 0, len(r))
1149
for _, v := range r {
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
package reports
2+
3+
import (
4+
"encoding/json"
5+
"errors"
6+
"testing"
7+
8+
. "github.com/onsi/ginkgo/v2"
9+
. "github.com/onsi/gomega"
10+
)
11+
12+
func TestReports(t *testing.T) {
13+
RegisterFailHandler(Fail)
14+
RunSpecs(t, "Reports Suite")
15+
}
16+
17+
var _ = Describe("PruneReport JSON", func() {
18+
Context("when marshaling and unmarshaling", func() {
19+
tests := []struct {
20+
name string
21+
report *PruneReport
22+
wantErr bool
23+
}{
24+
{
25+
name: "report with error",
26+
report: &PruneReport{
27+
Id: "test-container-id",
28+
Err: errors.New("test error message"),
29+
Size: 1024,
30+
},
31+
},
32+
{
33+
name: "report without error",
34+
report: &PruneReport{
35+
Id: "test-container-id",
36+
Err: nil,
37+
Size: 2048,
38+
},
39+
},
40+
{
41+
name: "empty report",
42+
report: &PruneReport{
43+
Id: "",
44+
Err: nil,
45+
Size: 0,
46+
},
47+
},
48+
{
49+
name: "report with complex error message from failing test case",
50+
report: &PruneReport{
51+
Id: "3d8a8789524a0c44a61baa49ceedda7be069b0b3d01255b24013d2fb82168c7e",
52+
Err: errors.New(`replacing mount point "/tmp/CI_7Qsy/podman-e2e-213135586/subtest-1767990215/p/root/overlay/d9f554276b923c07bf708858b5f35774f9d2924fa4094b1583e56b33ae357af1/merged": directory not empty`),
53+
Size: 7238,
54+
},
55+
},
56+
{
57+
name: "report with special characters in error",
58+
report: &PruneReport{
59+
Id: "container-special",
60+
Err: errors.New(`error with "quotes" and \backslashes\ and newlines`),
61+
Size: 512,
62+
},
63+
},
64+
}
65+
66+
for _, tt := range tests {
67+
It("should handle "+tt.name, func() {
68+
jsonData, err := json.Marshal(tt.report)
69+
Expect(err).ToNot(HaveOccurred(), "Failed to marshal PruneReport")
70+
71+
var unmarshalled PruneReport
72+
err = json.Unmarshal(jsonData, &unmarshalled)
73+
Expect(err).ToNot(HaveOccurred(), "Failed to unmarshal PruneReport")
74+
75+
Expect(unmarshalled.Id).To(Equal(tt.report.Id), "Id should match")
76+
Expect(unmarshalled.Size).To(Equal(tt.report.Size), "Size should match")
77+
78+
if tt.report.Err == nil {
79+
Expect(unmarshalled.Err).To(BeNil(), "Error should be nil")
80+
} else {
81+
Expect(unmarshalled.Err).ToNot(BeNil(), "Error should not be nil")
82+
Expect(unmarshalled.Err.Error()).To(Equal(tt.report.Err.Error()), "Error message should match")
83+
}
84+
})
85+
}
86+
})
87+
})

0 commit comments

Comments
 (0)