From 410c46193fadca00148b1b577782d94f0ee74fac Mon Sep 17 00:00:00 2001 From: Coker Richard <82083946+cokerrd@users.noreply.github.com> Date: Fri, 10 Jul 2026 11:03:50 +0100 Subject: [PATCH 1/4] fix: billing cycle output on instance get --- internal/commands/instance.go | 4 ++-- pkg/api/instance/instance.go | 9 ++++++++- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/internal/commands/instance.go b/internal/commands/instance.go index 52bcc3f..7d710cd 100644 --- a/internal/commands/instance.go +++ b/internal/commands/instance.go @@ -202,8 +202,8 @@ func runInstanceGet(cmd *cobra.Command, slug string) error { regionName = vm.Region.Name } billingCycle := "" - if vm.BillingCycle != nil { - billingCycle = vm.BillingCycle.Name + if vm.Offering != nil { + billingCycle = vm.Offering.BillingCycle.Name } storageName := "" if vm.StorageSetting != nil { diff --git a/pkg/api/instance/instance.go b/pkg/api/instance/instance.go index ce6d515..1f883fb 100644 --- a/pkg/api/instance/instance.go +++ b/pkg/api/instance/instance.go @@ -71,6 +71,7 @@ type VirtualMachine struct { IsVNF bool `json:"is_vnf"` ConsoleURL *string `json:"console_url"` Template *VMTemplate `json:"template"` + Offering *Offering `json:"offering"` BillingCycle *BillingCycle `json:"billing_cycle"` Region *Region `json:"region"` CloudProvider *CloudProvider `json:"cloud_provider"` @@ -162,6 +163,10 @@ type BillingCycle struct { Duration int `json:"duration"` Unit string `json:"unit"` } +type Offering struct { + ID string `json:"id"` + BillingCycle *BillingCycle `json:"billing_cycle"` +} // Region represents a cloud region. type Region struct { @@ -345,7 +350,9 @@ func (s *Service) List(ctx context.Context, region, project string) ([]VirtualMa // resolution) see the full set rather than just the first page. var all []VirtualMachine for page := 1; ; page++ { - q := url.Values{} + q := url.Values{ + "include": {"networks,ipaddresses"}, + } if region != "" { q.Set("filter[region]", region) } From 9801e807a019fb3f632ecc3663d2ac675730ba37 Mon Sep 17 00:00:00 2001 From: Coker Richard <82083946+cokerrd@users.noreply.github.com> Date: Fri, 10 Jul 2026 11:29:10 +0100 Subject: [PATCH 2/4] test: add error case test for instance list and get --- pkg/api/instance/instance_test.go | 35 +++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/pkg/api/instance/instance_test.go b/pkg/api/instance/instance_test.go index f799c20..735c91c 100644 --- a/pkg/api/instance/instance_test.go +++ b/pkg/api/instance/instance_test.go @@ -49,6 +49,26 @@ func TestList(t *testing.T) { } } +func TestListEmptyResponse(t *testing.T) { + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + json.NewEncoder(w).Encode(map[string]interface{}{ + "status": "Success", + "data": []interface{}{}, + "total": 0, + }) + })) + defer srv.Close() + + svc := instance.NewService(newClient(srv.URL)) + vms, err := svc.List(context.Background(), "", "") + if err != nil { + t.Fatalf("List() error = %v", err) + } + if len(vms) != 0 { + t.Errorf("got %d VMs, want 0", len(vms)) + } +} + func TestListPaginatesAllPages(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/virtual-machines" { @@ -121,6 +141,21 @@ func TestGet(t *testing.T) { } } +func TestGetNotFound(t *testing.T) { + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + json.NewEncoder(w).Encode(map[string]interface{}{ + "status": "Error", + "message": "not found", + }) + })) + defer srv.Close() + + svc := instance.NewService(newClient(srv.URL)) + _, err := svc.Get(context.Background(), "nonexistent") + if err == nil { + t.Error("expected error for not found") + } +} func TestStart(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPut || r.URL.Path != "/virtual-machines/test-vm/start" { From 1917418080053766dfeb24586e1881ac1f214a26 Mon Sep 17 00:00:00 2001 From: Coker Richard <82083946+cokerrd@users.noreply.github.com> Date: Fri, 10 Jul 2026 11:30:28 +0100 Subject: [PATCH 3/4] fix: show ips on instance list and get --- internal/commands/instance.go | 9 ++++++--- pkg/api/instance/instance.go | 33 +++++++++++++++++++++++++++++++-- 2 files changed, 37 insertions(+), 5 deletions(-) diff --git a/internal/commands/instance.go b/internal/commands/instance.go index 7d710cd..c15b7ba 100644 --- a/internal/commands/instance.go +++ b/internal/commands/instance.go @@ -108,12 +108,13 @@ func runInstanceList(cmd *cobra.Command) error { if privateIP == "" { privateIP = vm.NetworkPrivateIP() } + publicIP := vm.GetPublicIPAddress() row := []string{ instanceDisplayID(vm), vm.Name, vm.State, privateIP, - instance.StringVal(vm.PublicIP), + publicIP, regionName, } if expandedOutput { @@ -123,7 +124,7 @@ func runInstanceList(cmd *cobra.Command) error { vm.Name, vm.State, privateIP, - instance.StringVal(vm.PublicIP), + publicIP, regionName, templateName, vm.CreatedAt, @@ -215,6 +216,8 @@ func runInstanceGet(cmd *cobra.Command, slug string) error { privateIP = vm.NetworkPrivateIP() } + pubicIP := vm.GetPublicIPAddress() + headers := []string{"FIELD", "VALUE"} rows := [][]string{ {"ID", instanceDisplayID(*vm)}, @@ -224,7 +227,7 @@ func runInstanceGet(cmd *cobra.Command, slug string) error { {"State", vm.State}, {"Username", vm.Username}, {"Private IP", privateIP}, - {"Public IP", instance.StringVal(vm.PublicIP)}, + {"Public IP", pubicIP}, {"Region", regionName}, {"Template", templateName}, {"OS Family", osFamily}, diff --git a/pkg/api/instance/instance.go b/pkg/api/instance/instance.go index 1f883fb..026caa8 100644 --- a/pkg/api/instance/instance.go +++ b/pkg/api/instance/instance.go @@ -60,6 +60,7 @@ type VirtualMachine struct { Hostname string `json:"hostname"` Username string `json:"username"` State string `json:"state"` + IPAddresses []IPAddresses `json:"ipaddresses"` PublicIP *string `json:"public_ip"` PrivateIP *string `json:"private_ip"` FrozenAt *string `json:"frozen_at"` @@ -108,7 +109,7 @@ type VMNetworkIP struct { // NetworkPrivateIP returns the private IP from the default network, falling back // to the first network with an IP if no default is set. func (vm *VirtualMachine) NetworkPrivateIP() string { - var fallback string + fallback := "-" for _, n := range vm.Networks { if n.Pivot == nil || n.Pivot.IPAddress == "" { continue @@ -116,13 +117,34 @@ func (vm *VirtualMachine) NetworkPrivateIP() string { if n.IsDefault || n.Pivot.IsDefault { return n.Pivot.IPAddress } - if fallback == "" { + if fallback == "-" { fallback = n.Pivot.IPAddress } } return fallback } +// Returns the PublicIP if a VM is assigned one +func (vm *VirtualMachine) GetPublicIPAddress() string { + fallback := "-" + + for _, ip := range vm.IPAddresses { + if ip.IPAddress == "" { + continue + } + + if ip.Type != "" { + return ip.IPAddress + } + + if fallback == "-" { + fallback = ip.IPAddress + } + } + + return fallback +} + // VMTemplate represents the template/OS info on a VM. type VMTemplate struct { ID string `json:"id"` @@ -155,6 +177,13 @@ type OSVersion struct { PricingType string `json:"pricing_type"` } +// IPAddresses represents the IP address of a VM +type IPAddresses struct { + ID string `json:"id"` + IPAddress string `json:"ipaddress"` + Type string `json:"type"` +} + // BillingCycle represents a billing period. type BillingCycle struct { ID string `json:"id"` From 3cafc9a282fb47a79ced76c5e30edd5863a7fbdc Mon Sep 17 00:00:00 2001 From: Coker Richard <82083946+cokerrd@users.noreply.github.com> Date: Fri, 10 Jul 2026 12:08:58 +0100 Subject: [PATCH 4/4] fix: show ips on instance list and get --- internal/commands/instance.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/internal/commands/instance.go b/internal/commands/instance.go index c15b7ba..1693fcb 100644 --- a/internal/commands/instance.go +++ b/internal/commands/instance.go @@ -203,7 +203,7 @@ func runInstanceGet(cmd *cobra.Command, slug string) error { regionName = vm.Region.Name } billingCycle := "" - if vm.Offering != nil { + if vm.Offering != nil && vm.Offering.BillingCycle != nil { billingCycle = vm.Offering.BillingCycle.Name } storageName := "" @@ -216,7 +216,7 @@ func runInstanceGet(cmd *cobra.Command, slug string) error { privateIP = vm.NetworkPrivateIP() } - pubicIP := vm.GetPublicIPAddress() + publicIP := vm.GetPublicIPAddress() headers := []string{"FIELD", "VALUE"} rows := [][]string{ @@ -227,7 +227,7 @@ func runInstanceGet(cmd *cobra.Command, slug string) error { {"State", vm.State}, {"Username", vm.Username}, {"Private IP", privateIP}, - {"Public IP", pubicIP}, + {"Public IP", publicIP}, {"Region", regionName}, {"Template", templateName}, {"OS Family", osFamily},