Skip to content

Commit 07d900c

Browse files
authored
fix: send clusterId in inline-create boot disk (#3)
The Fluence API's VmBootDisk oneOf create variant is a CreateUserStorageRequest, which requires clusterId. The inline-create path built a CreateUserStorageInline without it, so `terraform apply` of a cloudless_vm with an inline boot_disk failed with: 400 Input validation error: bootDisk: data did not match any variant of untagged enum VmBootDisk Thread the VM's cluster_id into the inline boot disk so the body matches the create variant. The mock server accepted the malformed body (it treats any non-string bootDisk as an inline create and synthesizes an ID), so this only surfaced against the real API. Add a bootDiskToAPI unit test asserting clusterId is populated and marshaled.
1 parent eae3349 commit 07d900c

3 files changed

Lines changed: 55 additions & 3 deletions

File tree

internal/client/client.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -374,6 +374,7 @@ func (b VMBootDisk) MarshalJSON() ([]byte, error) {
374374
}
375375

376376
type CreateUserStorageInline struct {
377+
ClusterID string `json:"clusterId"`
377378
Name string `json:"name"`
378379
StorageType string `json:"storageType"`
379380
VolumeGb uint32 `json:"volumeGb"`

internal/provider/vm_resource.go

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -146,8 +146,10 @@ func (r *vmResource) Configure(_ context.Context, req resource.ConfigureRequest,
146146

147147
// bootDiskToAPI translates the Terraform boot_disk block into a VMBootDisk for
148148
// the Fluence API: existing storage_id wins; otherwise the inline-create
149-
// fields are required as a set.
150-
func bootDiskToAPI(d *vmBootDiskModel) (client.VMBootDisk, error) {
149+
// fields are required as a set. The inline-create variant is a
150+
// CreateUserStorageRequest, which carries its own clusterId — it inherits the
151+
// VM's cluster, passed in here.
152+
func bootDiskToAPI(d *vmBootDiskModel, clusterID string) (client.VMBootDisk, error) {
151153
if d == nil {
152154
return client.VMBootDisk{}, fmt.Errorf("boot_disk block is required")
153155
}
@@ -159,6 +161,7 @@ func bootDiskToAPI(d *vmBootDiskModel) (client.VMBootDisk, error) {
159161
return client.VMBootDisk{}, fmt.Errorf("inline boot_disk requires name, storage_type, volume_gb, and replicated")
160162
}
161163
return client.VMBootDisk{Create: &client.CreateUserStorageInline{
164+
ClusterID: clusterID,
162165
Name: d.Name.ValueString(),
163166
StorageType: d.StorageType.ValueString(),
164167
VolumeGb: uint32(d.VolumeGb.ValueInt64()),
@@ -176,7 +179,7 @@ func (r *vmResource) Create(ctx context.Context, req resource.CreateRequest, res
176179
return
177180
}
178181

179-
bd, err := bootDiskToAPI(plan.BootDisk)
182+
bd, err := bootDiskToAPI(plan.BootDisk, plan.ClusterID.ValueString())
180183
if err != nil {
181184
resp.Diagnostics.AddAttributeError(path.Root("boot_disk"), "Invalid boot_disk", err.Error())
182185
return
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
package provider
2+
3+
import (
4+
"encoding/json"
5+
"strings"
6+
"testing"
7+
8+
"github.com/hashicorp/terraform-plugin-framework/types"
9+
)
10+
11+
// TestBootDiskToAPI_InlineCarriesClusterID guards a regression where the
12+
// inline-create boot disk was sent without clusterId. The real Fluence API's
13+
// VmBootDisk oneOf create variant is a CreateUserStorageRequest, which requires
14+
// clusterId; omitting it produced a 400 "data did not match any variant of
15+
// untagged enum VmBootDisk" at apply time. The mock server accepted the
16+
// malformed body, so the gap was only visible against the real API.
17+
func TestBootDiskToAPI_InlineCarriesClusterID(t *testing.T) {
18+
const clusterID = "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa"
19+
d := &vmBootDiskModel{
20+
StorageID: types.StringNull(),
21+
Name: types.StringValue("boot"),
22+
StorageType: types.StringValue("NVME"),
23+
VolumeGb: types.Int64Value(40),
24+
Replicated: types.BoolValue(false),
25+
OSImage: types.StringValue("https://example.com/img.qcow2"),
26+
}
27+
28+
bd, err := bootDiskToAPI(d, clusterID)
29+
if err != nil {
30+
t.Fatalf("bootDiskToAPI returned error: %v", err)
31+
}
32+
if bd.Create == nil {
33+
t.Fatalf("expected an inline-create boot disk, got %+v", bd)
34+
}
35+
if bd.Create.ClusterID != clusterID {
36+
t.Errorf("inline boot disk ClusterID = %q, want %q", bd.Create.ClusterID, clusterID)
37+
}
38+
39+
// The clusterId must survive marshaling so it reaches the API in the
40+
// oneOf create variant.
41+
out, err := json.Marshal(bd)
42+
if err != nil {
43+
t.Fatalf("marshal boot disk: %v", err)
44+
}
45+
if want := `"clusterId":"` + clusterID + `"`; !strings.Contains(string(out), want) {
46+
t.Errorf("marshaled boot disk missing %s\n got: %s", want, out)
47+
}
48+
}

0 commit comments

Comments
 (0)