diff --git a/pkg/common/types.go b/pkg/common/types.go index 4e8b3b77..ea2fcef1 100644 --- a/pkg/common/types.go +++ b/pkg/common/types.go @@ -242,20 +242,40 @@ type Region struct { DisplayName string `json:"display_name"` } -// ComputeDetails represents compute-specific details (EC2, VM, Compute Engine) +// ComputeDetails represents compute-specific details (EC2, VM, Compute Engine). +// +// VCPU + MemoryGB are populated by per-provider catalogue lookups when +// available (Azure: armcompute.ResourceSKU.Capabilities; AWS: +// ec2:DescribeInstanceTypes; GCP: machine-type catalogue). They are +// optional — converters that don't yet wire a catalogue leave them at the +// zero value, and the JSON tag uses omitempty so unknown values don't +// pollute the API payload. type ComputeDetails struct { - InstanceType string `json:"instance_type"` - Platform string `json:"platform"` // linux, windows - Tenancy string `json:"tenancy"` // default, dedicated, host - Scope string `json:"scope"` // regional, zonal + InstanceType string `json:"instance_type"` + Platform string `json:"platform"` // linux, windows + Tenancy string `json:"tenancy"` // default, dedicated, host + Scope string `json:"scope"` // regional, zonal + VCPU int `json:"vcpu,omitempty"` // 0 = unknown + MemoryGB float64 `json:"memory_gb,omitempty"` // 0 = unknown } func (d ComputeDetails) GetServiceType() ServiceType { return ServiceCompute } +// GetDetailDescription returns a short human description of the compute +// recommendation. The base form is "/"; when both VCPU +// and MemoryGB are populated (>0) the size is appended as +// " ( vCPU / GB)" to give the UI a one-line summary +// without forcing the caller to inspect the struct. func (d ComputeDetails) GetDetailDescription() string { - return d.Platform + "/" + d.Tenancy + base := d.Platform + "/" + d.Tenancy + if d.VCPU > 0 && d.MemoryGB > 0 { + // %g trims trailing zeros (16 GB, not 16.000000 GB) but keeps + // fractional sizes (e.g. 0.5 GB for the smallest Azure SKUs). + return fmt.Sprintf("%s (%d vCPU / %g GB)", base, d.VCPU, d.MemoryGB) + } + return base } // DatabaseDetails represents database-specific details (RDS, Azure SQL, Cloud SQL) diff --git a/pkg/common/types_test.go b/pkg/common/types_test.go index 453d48ec..22eca957 100644 --- a/pkg/common/types_test.go +++ b/pkg/common/types_test.go @@ -115,6 +115,51 @@ func TestComputeDetails_GetDetailDescription(t *testing.T) { }, expected: "windows/dedicated", }, + { + // vCPU alone is insufficient — both fields must be populated for + // the size suffix to appear, otherwise we'd surface "16 vCPU / + // 0 GB" which is misleading. + name: "VCPU populated but MemoryGB zero — base description only", + details: ComputeDetails{ + Platform: "linux", + Tenancy: "default", + VCPU: 16, + }, + expected: "linux/default", + }, + { + // Symmetric guard for the MemoryGB-only case. + name: "MemoryGB populated but VCPU zero — base description only", + details: ComputeDetails{ + Platform: "linux", + Tenancy: "default", + MemoryGB: 32, + }, + expected: "linux/default", + }, + { + // Whole-number GB renders without trailing zeros (16 GB, + // not 16.000000 GB). + name: "Both fields populated — integer memory", + details: ComputeDetails{ + Platform: "linux", + Tenancy: "default", + VCPU: 4, + MemoryGB: 16, + }, + expected: "linux/default (4 vCPU / 16 GB)", + }, + { + // Fractional GB (Azure has 0.5 GB SKUs) renders verbatim. + name: "Both fields populated — fractional memory", + details: ComputeDetails{ + Platform: "linux", + Tenancy: "default", + VCPU: 1, + MemoryGB: 0.5, + }, + expected: "linux/default (1 vCPU / 0.5 GB)", + }, } for _, tt := range tests {