Skip to content

Commit 4c6e54c

Browse files
committed
fix(api): Add custom JSON marshaling for InstancePrototype interface fields
Instance provisioning fails with "Expected only one oneOf fields to be set: got 0" errors when using InstancePrototype types with interface-typed fields. Go's default JSON marshaling omits interface-typed fields in certain contexts. oneOf discriminator fields (image, zone, vpc, primary_network_attachment) are missing from the JSON payload, causing VPC API validation to fail. This commit add custom MarshalJSON methods to 8 InstancePrototype variant types that explicitly serialize all fields including interface types. Fixes: #133 Signed-off-by: Josephine Pfeiffer <[email protected]>
1 parent 166095d commit 4c6e54c

File tree

3 files changed

+875
-3
lines changed

3 files changed

+875
-3
lines changed
Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,205 @@
1+
package vpcv1_test
2+
3+
import (
4+
"encoding/json"
5+
"strings"
6+
"testing"
7+
8+
"github.com/IBM/vpc-go-sdk/vpcv1"
9+
)
10+
11+
func TestInstancePrototypeMarshalBug(t *testing.T) {
12+
// Create the prototype exactly like Karpenter does
13+
imageID := "r010-test-image-id"
14+
zoneName := "eu-de-2"
15+
profileName := "bx2-2x8"
16+
vpcID := "r010-test-vpc-id"
17+
instanceName := "test-instance"
18+
subnetID := "test-subnet-id"
19+
20+
// Create VNI prototype
21+
vniPrototype := &vpcv1.InstanceNetworkAttachmentPrototypeVirtualNetworkInterfaceVirtualNetworkInterfacePrototypeInstanceNetworkAttachmentContext{
22+
Subnet: &vpcv1.SubnetIdentityByID{
23+
ID: &subnetID,
24+
},
25+
}
26+
27+
primaryNetworkAttachment := &vpcv1.InstanceNetworkAttachmentPrototype{
28+
VirtualNetworkInterface: vniPrototype,
29+
}
30+
31+
// Create instance prototype
32+
instancePrototype := &vpcv1.InstancePrototypeInstanceByImageInstanceByImageInstanceByNetworkAttachment{
33+
Image: &vpcv1.ImageIdentityByID{ID: &imageID},
34+
Zone: &vpcv1.ZoneIdentityByName{Name: &zoneName},
35+
Profile: &vpcv1.InstanceProfileIdentityByName{Name: &profileName},
36+
VPC: &vpcv1.VPCIdentityByID{ID: &vpcID},
37+
Name: &instanceName,
38+
PrimaryNetworkAttachment: primaryNetworkAttachment,
39+
}
40+
41+
// Verify fields are set
42+
if instancePrototype.Image == nil {
43+
t.Fatal("Image field should not be nil")
44+
}
45+
if instancePrototype.Zone == nil {
46+
t.Fatal("Zone field should not be nil")
47+
}
48+
if instancePrototype.VPC == nil {
49+
t.Fatal("VPC field should not be nil")
50+
}
51+
if instancePrototype.Profile == nil {
52+
t.Fatal("Profile field should not be nil")
53+
}
54+
55+
// Marshal to JSON
56+
jsonBytes, err := json.MarshalIndent(instancePrototype, "", " ")
57+
if err != nil {
58+
t.Fatalf("JSON marshaling failed: %v", err)
59+
}
60+
61+
jsonString := string(jsonBytes)
62+
t.Logf("Marshaled JSON:\n%s\n", jsonString)
63+
64+
// These assertions will FAIL if the bug exists
65+
if !strings.Contains(jsonString, `"image"`) {
66+
t.Error("JSON should contain image field - BUG CONFIRMED")
67+
}
68+
if !strings.Contains(jsonString, `"zone"`) {
69+
t.Error("JSON should contain zone field - BUG CONFIRMED")
70+
}
71+
if !strings.Contains(jsonString, `"vpc"`) {
72+
t.Error("JSON should contain vpc field - BUG CONFIRMED")
73+
}
74+
if !strings.Contains(jsonString, `"profile"`) {
75+
t.Error("JSON should contain profile field - BUG CONFIRMED")
76+
}
77+
78+
// Verify actual values
79+
if !strings.Contains(jsonString, imageID) {
80+
t.Error("JSON should contain image ID value")
81+
}
82+
if !strings.Contains(jsonString, zoneName) {
83+
t.Error("JSON should contain zone name value")
84+
}
85+
if !strings.Contains(jsonString, vpcID) {
86+
t.Error("JSON should contain VPC ID value")
87+
}
88+
if !strings.Contains(jsonString, profileName) {
89+
t.Error("JSON should contain profile name value")
90+
}
91+
}
92+
93+
func TestInstancePrototypeInterfaceMarshalBug(t *testing.T) {
94+
// This test verifies that marshaling through the InstancePrototypeIntf interface
95+
// correctly calls the concrete type's MarshalJSON method.
96+
// This is the CRITICAL fix - without it, interface marshaling drops fields.
97+
98+
imageID := "r010-test-image-id"
99+
zoneName := "eu-de-2"
100+
profileName := "bx2-2x8"
101+
vpcID := "r010-test-vpc-id"
102+
instanceName := "test-instance"
103+
subnetID := "test-subnet-id"
104+
105+
// Create VNI prototype
106+
vniPrototype := &vpcv1.InstanceNetworkAttachmentPrototypeVirtualNetworkInterfaceVirtualNetworkInterfacePrototypeInstanceNetworkAttachmentContext{
107+
Subnet: &vpcv1.SubnetIdentityByID{
108+
ID: &subnetID,
109+
},
110+
}
111+
112+
primaryNetworkAttachment := &vpcv1.InstanceNetworkAttachmentPrototype{
113+
VirtualNetworkInterface: vniPrototype,
114+
}
115+
116+
// Create concrete prototype
117+
concretePrototype := &vpcv1.InstancePrototypeInstanceByImageInstanceByImageInstanceByNetworkAttachment{
118+
Image: &vpcv1.ImageIdentityByID{ID: &imageID},
119+
Zone: &vpcv1.ZoneIdentityByName{Name: &zoneName},
120+
Profile: &vpcv1.InstanceProfileIdentityByName{Name: &profileName},
121+
VPC: &vpcv1.VPCIdentityByID{ID: &vpcID},
122+
Name: &instanceName,
123+
PrimaryNetworkAttachment: primaryNetworkAttachment,
124+
}
125+
126+
// Cast to interface type (THIS is how the SDK uses it)
127+
var interfacePrototype vpcv1.InstancePrototypeIntf = concretePrototype
128+
129+
// Marshal through the INTERFACE (critical test)
130+
jsonBytes, err := json.MarshalIndent(interfacePrototype, "", " ")
131+
if err != nil {
132+
t.Fatalf("JSON marshaling through interface failed: %v", err)
133+
}
134+
135+
jsonString := string(jsonBytes)
136+
t.Logf("Marshaled JSON through interface:\n%s\n", jsonString)
137+
138+
// These assertions verify the interface marshaling works
139+
if !strings.Contains(jsonString, `"image"`) {
140+
t.Error("JSON should contain image field when marshaled through interface - INTERFACE BUG CONFIRMED")
141+
}
142+
if !strings.Contains(jsonString, `"zone"`) {
143+
t.Error("JSON should contain zone field when marshaled through interface - INTERFACE BUG CONFIRMED")
144+
}
145+
if !strings.Contains(jsonString, `"vpc"`) {
146+
t.Error("JSON should contain vpc field when marshaled through interface - INTERFACE BUG CONFIRMED")
147+
}
148+
if !strings.Contains(jsonString, `"profile"`) {
149+
t.Error("JSON should contain profile field when marshaled through interface - INTERFACE BUG CONFIRMED")
150+
}
151+
152+
// Verify actual values
153+
if !strings.Contains(jsonString, imageID) {
154+
t.Error("JSON should contain image ID value when marshaled through interface")
155+
}
156+
if !strings.Contains(jsonString, zoneName) {
157+
t.Error("JSON should contain zone name value when marshaled through interface")
158+
}
159+
if !strings.Contains(jsonString, vpcID) {
160+
t.Error("JSON should contain VPC ID value when marshaled through interface")
161+
}
162+
if !strings.Contains(jsonString, profileName) {
163+
t.Error("JSON should contain profile name value when marshaled through interface")
164+
}
165+
}
166+
167+
func TestBaseInstancePrototypeMarshal(t *testing.T) {
168+
// Test the base InstancePrototype type marshaling
169+
imageID := "r010-test-image-id"
170+
zoneName := "eu-de-2"
171+
profileName := "bx2-2x8"
172+
vpcID := "r010-test-vpc-id"
173+
instanceName := "test-instance"
174+
175+
basePrototype := &vpcv1.InstancePrototype{
176+
Image: &vpcv1.ImageIdentityByID{ID: &imageID},
177+
Zone: &vpcv1.ZoneIdentityByName{Name: &zoneName},
178+
Profile: &vpcv1.InstanceProfileIdentityByName{Name: &profileName},
179+
VPC: &vpcv1.VPCIdentityByID{ID: &vpcID},
180+
Name: &instanceName,
181+
}
182+
183+
// Marshal to JSON
184+
jsonBytes, err := json.MarshalIndent(basePrototype, "", " ")
185+
if err != nil {
186+
t.Fatalf("JSON marshaling failed: %v", err)
187+
}
188+
189+
jsonString := string(jsonBytes)
190+
t.Logf("Marshaled base prototype JSON:\n%s\n", jsonString)
191+
192+
// Verify interface fields are present
193+
if !strings.Contains(jsonString, `"image"`) {
194+
t.Error("Base prototype JSON should contain image field")
195+
}
196+
if !strings.Contains(jsonString, `"zone"`) {
197+
t.Error("Base prototype JSON should contain zone field")
198+
}
199+
if !strings.Contains(jsonString, `"vpc"`) {
200+
t.Error("Base prototype JSON should contain vpc field")
201+
}
202+
if !strings.Contains(jsonString, `"profile"`) {
203+
t.Error("Base prototype JSON should contain profile field")
204+
}
205+
}

0 commit comments

Comments
 (0)