2
0

idle_providerid_test.go 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346
  1. package costmodel
  2. import (
  3. "testing"
  4. "time"
  5. "github.com/opencost/opencost/core/pkg/opencost"
  6. )
  7. // TestComputeIdleAllocations_ProviderID_ByNode verifies that when computing
  8. // idle allocations by node, each idle allocation correctly contains the
  9. // cloud provider instance ID from AssetTotals.ProviderID, not the node name.
  10. func TestComputeIdleAllocations_ProviderID_ByNode(t *testing.T) {
  11. // Create test window
  12. start := time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC)
  13. end := time.Date(2024, 1, 1, 1, 0, 0, 0, time.UTC)
  14. window := opencost.NewClosedWindow(start, end)
  15. // Create AssetSet with nodes that have ProviderIDs
  16. assetSet := opencost.NewAssetSet(start, end)
  17. node1 := opencost.NewNode("ip-10-2-1-100.ec2.internal", "cluster1", "aws:///us-east-1a/i-0abc123def456789", start, end, window)
  18. node1.CPUCost = 10.0
  19. node1.RAMCost = 5.0
  20. node1.GPUCost = 0.0
  21. node2 := opencost.NewNode("gke-cluster-default-pool-node-456", "cluster1", "gce://my-project/us-central1-a/instance-456", start, end, window)
  22. node2.CPUCost = 8.0
  23. node2.RAMCost = 4.0
  24. node2.GPUCost = 0.0
  25. assetSet.Insert(node1, nil)
  26. assetSet.Insert(node2, nil)
  27. // Create AllocationSet with some utilization (not 100% to ensure idle exists)
  28. allocSet := opencost.NewAllocationSet(start, end)
  29. // Create allocations that use 50% of each node's resources
  30. alloc1 := &opencost.Allocation{
  31. Name: "namespace1/pod1/container1",
  32. Start: start,
  33. End: end,
  34. Window: window.Clone(),
  35. CPUCost: 5.0, // 50% of node1's CPU
  36. RAMCost: 2.5, // 50% of node1's RAM
  37. GPUCost: 0.0,
  38. Properties: &opencost.AllocationProperties{
  39. Cluster: "cluster1",
  40. Node: "ip-10-2-1-100.ec2.internal",
  41. },
  42. }
  43. alloc2 := &opencost.Allocation{
  44. Name: "namespace2/pod2/container2",
  45. Start: start,
  46. End: end,
  47. Window: window.Clone(),
  48. CPUCost: 4.0, // 50% of node2's CPU
  49. RAMCost: 2.0, // 50% of node2's RAM
  50. GPUCost: 0.0,
  51. Properties: &opencost.AllocationProperties{
  52. Cluster: "cluster1",
  53. Node: "gke-cluster-default-pool-node-456",
  54. },
  55. }
  56. allocSet.Insert(alloc1)
  57. allocSet.Insert(alloc2)
  58. // Compute idle allocations by node (idleByNode=true)
  59. idleSet, err := computeIdleAllocations(allocSet, assetSet, true)
  60. if err != nil {
  61. t.Fatalf("Error computing idle allocations: %v", err)
  62. }
  63. // Expected idle allocations with ProviderIDs
  64. expectedIdles := map[string]struct {
  65. providerID string
  66. nodeName string
  67. }{
  68. "cluster1/ip-10-2-1-100.ec2.internal/__idle__": {
  69. providerID: "aws:///us-east-1a/i-0abc123def456789",
  70. nodeName: "ip-10-2-1-100.ec2.internal",
  71. },
  72. "cluster1/gke-cluster-default-pool-node-456/__idle__": {
  73. providerID: "gce://my-project/us-central1-a/instance-456",
  74. nodeName: "gke-cluster-default-pool-node-456",
  75. },
  76. }
  77. // Verify each idle allocation has the correct ProviderID
  78. foundCount := 0
  79. for _, alloc := range idleSet.Allocations {
  80. if !alloc.IsIdle() {
  81. continue
  82. }
  83. expected, ok := expectedIdles[alloc.Name]
  84. if !ok {
  85. t.Errorf("Unexpected idle allocation: %s", alloc.Name)
  86. continue
  87. }
  88. foundCount++
  89. // Verify ProviderID is the cloud instance ID, not the node name
  90. if alloc.Properties.ProviderID != expected.providerID {
  91. t.Errorf("Allocation %s: expected ProviderID %q, got %q",
  92. alloc.Name, expected.providerID, alloc.Properties.ProviderID)
  93. }
  94. // Verify ProviderID is NOT the node name (the bug we're fixing)
  95. if alloc.Properties.ProviderID == expected.nodeName {
  96. t.Errorf("Allocation %s: ProviderID should not be node name %q",
  97. alloc.Name, expected.nodeName)
  98. }
  99. // Verify Node field still contains the node name
  100. if alloc.Properties.Node != expected.nodeName {
  101. t.Errorf("Allocation %s: expected Node %q, got %q",
  102. alloc.Name, expected.nodeName, alloc.Properties.Node)
  103. }
  104. // Verify costs are non-zero (idle exists)
  105. if alloc.CPUCost <= 0 && alloc.RAMCost <= 0 {
  106. t.Errorf("Allocation %s: expected non-zero idle costs", alloc.Name)
  107. }
  108. }
  109. if foundCount != len(expectedIdles) {
  110. t.Errorf("Expected %d idle allocations, found %d", len(expectedIdles), foundCount)
  111. }
  112. }
  113. // TestComputeIdleAllocations_ProviderID_ByCluster verifies that when computing
  114. // idle allocations by cluster, the idle allocation has an empty ProviderID
  115. // because there's no single instance ID for the entire cluster.
  116. func TestComputeIdleAllocations_ProviderID_ByCluster(t *testing.T) {
  117. // Create test window
  118. start := time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC)
  119. end := time.Date(2024, 1, 1, 1, 0, 0, 0, time.UTC)
  120. window := opencost.NewClosedWindow(start, end)
  121. // Create AssetSet with nodes that have ProviderIDs
  122. assetSet := opencost.NewAssetSet(start, end)
  123. node1 := opencost.NewNode("node1", "cluster1", "aws:///us-east-1a/i-0abc123", start, end, window)
  124. node1.CPUCost = 10.0
  125. node1.RAMCost = 5.0
  126. node1.GPUCost = 0.0
  127. node2 := opencost.NewNode("node2", "cluster1", "aws:///us-east-1b/i-0def456", start, end, window)
  128. node2.CPUCost = 8.0
  129. node2.RAMCost = 4.0
  130. node2.GPUCost = 0.0
  131. assetSet.Insert(node1, nil)
  132. assetSet.Insert(node2, nil)
  133. // Create AllocationSet with some utilization
  134. allocSet := opencost.NewAllocationSet(start, end)
  135. alloc := &opencost.Allocation{
  136. Name: "namespace1/pod1/container1",
  137. Start: start,
  138. End: end,
  139. Window: window.Clone(),
  140. CPUCost: 9.0, // Uses 50% of total cluster CPU (18.0 total)
  141. RAMCost: 4.5, // Uses 50% of total cluster RAM (9.0 total)
  142. GPUCost: 0.0,
  143. Properties: &opencost.AllocationProperties{
  144. Cluster: "cluster1",
  145. Node: "node1",
  146. },
  147. }
  148. allocSet.Insert(alloc)
  149. // Compute idle allocations by cluster (idleByNode=false)
  150. idleSet, err := computeIdleAllocations(allocSet, assetSet, false)
  151. if err != nil {
  152. t.Fatalf("Error computing idle allocations: %v", err)
  153. }
  154. // Find the cluster-level idle allocation
  155. var clusterIdle *opencost.Allocation
  156. for _, alloc := range idleSet.Allocations {
  157. if alloc.IsIdle() && alloc.Name == "cluster1/__idle__" {
  158. clusterIdle = alloc
  159. break
  160. }
  161. }
  162. if clusterIdle == nil {
  163. t.Fatal("Expected to find cluster-level idle allocation")
  164. }
  165. // Verify ProviderID is empty for cluster-level idle
  166. if clusterIdle.Properties.ProviderID != "" {
  167. t.Errorf("Cluster-level idle allocation should have empty ProviderID, got %q",
  168. clusterIdle.Properties.ProviderID)
  169. }
  170. // Verify Node is also empty for cluster-level idle
  171. if clusterIdle.Properties.Node != "" {
  172. t.Errorf("Cluster-level idle allocation should have empty Node, got %q",
  173. clusterIdle.Properties.Node)
  174. }
  175. // Verify costs are non-zero
  176. if clusterIdle.CPUCost <= 0 && clusterIdle.RAMCost <= 0 {
  177. t.Error("Expected non-zero idle costs for cluster-level idle")
  178. }
  179. }
  180. // TestComputeIdleAllocations_ProviderID_NoProviderID verifies that nodes
  181. // without a ProviderID (e.g., bare-metal, local clusters) result in idle
  182. // allocations with an empty ProviderID field.
  183. func TestComputeIdleAllocations_ProviderID_NoProviderID(t *testing.T) {
  184. // Create test window
  185. start := time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC)
  186. end := time.Date(2024, 1, 1, 1, 0, 0, 0, time.UTC)
  187. window := opencost.NewClosedWindow(start, end)
  188. // Create AssetSet with a node that has no ProviderID
  189. assetSet := opencost.NewAssetSet(start, end)
  190. node := opencost.NewNode("bare-metal-node-1", "cluster1", "", start, end, window)
  191. node.CPUCost = 10.0
  192. node.RAMCost = 5.0
  193. node.GPUCost = 0.0
  194. assetSet.Insert(node, nil)
  195. // Create AllocationSet with partial utilization
  196. allocSet := opencost.NewAllocationSet(start, end)
  197. alloc := &opencost.Allocation{
  198. Name: "namespace1/pod1/container1",
  199. Start: start,
  200. End: end,
  201. Window: window.Clone(),
  202. CPUCost: 5.0,
  203. RAMCost: 2.5,
  204. GPUCost: 0.0,
  205. Properties: &opencost.AllocationProperties{
  206. Cluster: "cluster1",
  207. Node: "bare-metal-node-1",
  208. },
  209. }
  210. allocSet.Insert(alloc)
  211. // Compute idle allocations by node
  212. idleSet, err := computeIdleAllocations(allocSet, assetSet, true)
  213. if err != nil {
  214. t.Fatalf("Error computing idle allocations: %v", err)
  215. }
  216. // Find the idle allocation
  217. var idle *opencost.Allocation
  218. for _, alloc := range idleSet.Allocations {
  219. if alloc.IsIdle() {
  220. idle = alloc
  221. break
  222. }
  223. }
  224. if idle == nil {
  225. t.Fatal("Expected to find idle allocation")
  226. }
  227. // Verify ProviderID is empty
  228. if idle.Properties.ProviderID != "" {
  229. t.Errorf("Node without ProviderID should result in empty ProviderID in idle allocation, got %q",
  230. idle.Properties.ProviderID)
  231. }
  232. // Verify Node field is still populated
  233. if idle.Properties.Node != "bare-metal-node-1" {
  234. t.Errorf("Expected Node %q, got %q", "bare-metal-node-1", idle.Properties.Node)
  235. }
  236. }
  237. // TestComputeIdleAllocations_ProviderID_AzureFormat tests that Azure VM
  238. // ProviderIDs are correctly propagated to idle allocations.
  239. func TestComputeIdleAllocations_ProviderID_AzureFormat(t *testing.T) {
  240. // Create test window
  241. start := time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC)
  242. end := time.Date(2024, 1, 1, 1, 0, 0, 0, time.UTC)
  243. window := opencost.NewClosedWindow(start, end)
  244. // Create AssetSet with Azure VM
  245. assetSet := opencost.NewAssetSet(start, end)
  246. azureProviderID := "azure:///subscriptions/12345678-1234-1234-1234-123456789abc/resourceGroups/my-rg/providers/Microsoft.Compute/virtualMachines/aks-nodepool1-12345678-vmss000000"
  247. node := opencost.NewNode("aks-nodepool1-12345678-vmss000000", "cluster1", azureProviderID, start, end, window)
  248. node.CPUCost = 10.0
  249. node.RAMCost = 5.0
  250. node.GPUCost = 0.0
  251. assetSet.Insert(node, nil)
  252. // Create AllocationSet
  253. allocSet := opencost.NewAllocationSet(start, end)
  254. alloc := &opencost.Allocation{
  255. Name: "namespace1/pod1/container1",
  256. Start: start,
  257. End: end,
  258. Window: window.Clone(),
  259. CPUCost: 5.0,
  260. RAMCost: 2.5,
  261. GPUCost: 0.0,
  262. Properties: &opencost.AllocationProperties{
  263. Cluster: "cluster1",
  264. Node: "aks-nodepool1-12345678-vmss000000",
  265. },
  266. }
  267. allocSet.Insert(alloc)
  268. // Compute idle allocations
  269. idleSet, err := computeIdleAllocations(allocSet, assetSet, true)
  270. if err != nil {
  271. t.Fatalf("Error computing idle allocations: %v", err)
  272. }
  273. // Find the idle allocation
  274. var idle *opencost.Allocation
  275. for _, alloc := range idleSet.Allocations {
  276. if alloc.IsIdle() {
  277. idle = alloc
  278. break
  279. }
  280. }
  281. if idle == nil {
  282. t.Fatal("Expected to find idle allocation")
  283. }
  284. // Verify Azure ProviderID is correctly set
  285. if idle.Properties.ProviderID != azureProviderID {
  286. t.Errorf("Expected ProviderID %q, got %q", azureProviderID, idle.Properties.ProviderID)
  287. }
  288. }