| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346 |
- package costmodel
- import (
- "testing"
- "time"
- "github.com/opencost/opencost/core/pkg/opencost"
- )
- // TestComputeIdleAllocations_ProviderID_ByNode verifies that when computing
- // idle allocations by node, each idle allocation correctly contains the
- // cloud provider instance ID from AssetTotals.ProviderID, not the node name.
- func TestComputeIdleAllocations_ProviderID_ByNode(t *testing.T) {
- // Create test window
- start := time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC)
- end := time.Date(2024, 1, 1, 1, 0, 0, 0, time.UTC)
- window := opencost.NewClosedWindow(start, end)
- // Create AssetSet with nodes that have ProviderIDs
- assetSet := opencost.NewAssetSet(start, end)
- node1 := opencost.NewNode("ip-10-2-1-100.ec2.internal", "cluster1", "aws:///us-east-1a/i-0abc123def456789", start, end, window)
- node1.CPUCost = 10.0
- node1.RAMCost = 5.0
- node1.GPUCost = 0.0
- node2 := opencost.NewNode("gke-cluster-default-pool-node-456", "cluster1", "gce://my-project/us-central1-a/instance-456", start, end, window)
- node2.CPUCost = 8.0
- node2.RAMCost = 4.0
- node2.GPUCost = 0.0
- assetSet.Insert(node1, nil)
- assetSet.Insert(node2, nil)
- // Create AllocationSet with some utilization (not 100% to ensure idle exists)
- allocSet := opencost.NewAllocationSet(start, end)
- // Create allocations that use 50% of each node's resources
- alloc1 := &opencost.Allocation{
- Name: "namespace1/pod1/container1",
- Start: start,
- End: end,
- Window: window.Clone(),
- CPUCost: 5.0, // 50% of node1's CPU
- RAMCost: 2.5, // 50% of node1's RAM
- GPUCost: 0.0,
- Properties: &opencost.AllocationProperties{
- Cluster: "cluster1",
- Node: "ip-10-2-1-100.ec2.internal",
- },
- }
- alloc2 := &opencost.Allocation{
- Name: "namespace2/pod2/container2",
- Start: start,
- End: end,
- Window: window.Clone(),
- CPUCost: 4.0, // 50% of node2's CPU
- RAMCost: 2.0, // 50% of node2's RAM
- GPUCost: 0.0,
- Properties: &opencost.AllocationProperties{
- Cluster: "cluster1",
- Node: "gke-cluster-default-pool-node-456",
- },
- }
- allocSet.Insert(alloc1)
- allocSet.Insert(alloc2)
- // Compute idle allocations by node (idleByNode=true)
- idleSet, err := computeIdleAllocations(allocSet, assetSet, true)
- if err != nil {
- t.Fatalf("Error computing idle allocations: %v", err)
- }
- // Expected idle allocations with ProviderIDs
- expectedIdles := map[string]struct {
- providerID string
- nodeName string
- }{
- "cluster1/ip-10-2-1-100.ec2.internal/__idle__": {
- providerID: "aws:///us-east-1a/i-0abc123def456789",
- nodeName: "ip-10-2-1-100.ec2.internal",
- },
- "cluster1/gke-cluster-default-pool-node-456/__idle__": {
- providerID: "gce://my-project/us-central1-a/instance-456",
- nodeName: "gke-cluster-default-pool-node-456",
- },
- }
- // Verify each idle allocation has the correct ProviderID
- foundCount := 0
- for _, alloc := range idleSet.Allocations {
- if !alloc.IsIdle() {
- continue
- }
- expected, ok := expectedIdles[alloc.Name]
- if !ok {
- t.Errorf("Unexpected idle allocation: %s", alloc.Name)
- continue
- }
- foundCount++
- // Verify ProviderID is the cloud instance ID, not the node name
- if alloc.Properties.ProviderID != expected.providerID {
- t.Errorf("Allocation %s: expected ProviderID %q, got %q",
- alloc.Name, expected.providerID, alloc.Properties.ProviderID)
- }
- // Verify ProviderID is NOT the node name (the bug we're fixing)
- if alloc.Properties.ProviderID == expected.nodeName {
- t.Errorf("Allocation %s: ProviderID should not be node name %q",
- alloc.Name, expected.nodeName)
- }
- // Verify Node field still contains the node name
- if alloc.Properties.Node != expected.nodeName {
- t.Errorf("Allocation %s: expected Node %q, got %q",
- alloc.Name, expected.nodeName, alloc.Properties.Node)
- }
- // Verify costs are non-zero (idle exists)
- if alloc.CPUCost <= 0 && alloc.RAMCost <= 0 {
- t.Errorf("Allocation %s: expected non-zero idle costs", alloc.Name)
- }
- }
- if foundCount != len(expectedIdles) {
- t.Errorf("Expected %d idle allocations, found %d", len(expectedIdles), foundCount)
- }
- }
- // TestComputeIdleAllocations_ProviderID_ByCluster verifies that when computing
- // idle allocations by cluster, the idle allocation has an empty ProviderID
- // because there's no single instance ID for the entire cluster.
- func TestComputeIdleAllocations_ProviderID_ByCluster(t *testing.T) {
- // Create test window
- start := time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC)
- end := time.Date(2024, 1, 1, 1, 0, 0, 0, time.UTC)
- window := opencost.NewClosedWindow(start, end)
- // Create AssetSet with nodes that have ProviderIDs
- assetSet := opencost.NewAssetSet(start, end)
- node1 := opencost.NewNode("node1", "cluster1", "aws:///us-east-1a/i-0abc123", start, end, window)
- node1.CPUCost = 10.0
- node1.RAMCost = 5.0
- node1.GPUCost = 0.0
- node2 := opencost.NewNode("node2", "cluster1", "aws:///us-east-1b/i-0def456", start, end, window)
- node2.CPUCost = 8.0
- node2.RAMCost = 4.0
- node2.GPUCost = 0.0
- assetSet.Insert(node1, nil)
- assetSet.Insert(node2, nil)
- // Create AllocationSet with some utilization
- allocSet := opencost.NewAllocationSet(start, end)
- alloc := &opencost.Allocation{
- Name: "namespace1/pod1/container1",
- Start: start,
- End: end,
- Window: window.Clone(),
- CPUCost: 9.0, // Uses 50% of total cluster CPU (18.0 total)
- RAMCost: 4.5, // Uses 50% of total cluster RAM (9.0 total)
- GPUCost: 0.0,
- Properties: &opencost.AllocationProperties{
- Cluster: "cluster1",
- Node: "node1",
- },
- }
- allocSet.Insert(alloc)
- // Compute idle allocations by cluster (idleByNode=false)
- idleSet, err := computeIdleAllocations(allocSet, assetSet, false)
- if err != nil {
- t.Fatalf("Error computing idle allocations: %v", err)
- }
- // Find the cluster-level idle allocation
- var clusterIdle *opencost.Allocation
- for _, alloc := range idleSet.Allocations {
- if alloc.IsIdle() && alloc.Name == "cluster1/__idle__" {
- clusterIdle = alloc
- break
- }
- }
- if clusterIdle == nil {
- t.Fatal("Expected to find cluster-level idle allocation")
- }
- // Verify ProviderID is empty for cluster-level idle
- if clusterIdle.Properties.ProviderID != "" {
- t.Errorf("Cluster-level idle allocation should have empty ProviderID, got %q",
- clusterIdle.Properties.ProviderID)
- }
- // Verify Node is also empty for cluster-level idle
- if clusterIdle.Properties.Node != "" {
- t.Errorf("Cluster-level idle allocation should have empty Node, got %q",
- clusterIdle.Properties.Node)
- }
- // Verify costs are non-zero
- if clusterIdle.CPUCost <= 0 && clusterIdle.RAMCost <= 0 {
- t.Error("Expected non-zero idle costs for cluster-level idle")
- }
- }
- // TestComputeIdleAllocations_ProviderID_NoProviderID verifies that nodes
- // without a ProviderID (e.g., bare-metal, local clusters) result in idle
- // allocations with an empty ProviderID field.
- func TestComputeIdleAllocations_ProviderID_NoProviderID(t *testing.T) {
- // Create test window
- start := time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC)
- end := time.Date(2024, 1, 1, 1, 0, 0, 0, time.UTC)
- window := opencost.NewClosedWindow(start, end)
- // Create AssetSet with a node that has no ProviderID
- assetSet := opencost.NewAssetSet(start, end)
- node := opencost.NewNode("bare-metal-node-1", "cluster1", "", start, end, window)
- node.CPUCost = 10.0
- node.RAMCost = 5.0
- node.GPUCost = 0.0
- assetSet.Insert(node, nil)
- // Create AllocationSet with partial utilization
- allocSet := opencost.NewAllocationSet(start, end)
- alloc := &opencost.Allocation{
- Name: "namespace1/pod1/container1",
- Start: start,
- End: end,
- Window: window.Clone(),
- CPUCost: 5.0,
- RAMCost: 2.5,
- GPUCost: 0.0,
- Properties: &opencost.AllocationProperties{
- Cluster: "cluster1",
- Node: "bare-metal-node-1",
- },
- }
- allocSet.Insert(alloc)
- // Compute idle allocations by node
- idleSet, err := computeIdleAllocations(allocSet, assetSet, true)
- if err != nil {
- t.Fatalf("Error computing idle allocations: %v", err)
- }
- // Find the idle allocation
- var idle *opencost.Allocation
- for _, alloc := range idleSet.Allocations {
- if alloc.IsIdle() {
- idle = alloc
- break
- }
- }
- if idle == nil {
- t.Fatal("Expected to find idle allocation")
- }
- // Verify ProviderID is empty
- if idle.Properties.ProviderID != "" {
- t.Errorf("Node without ProviderID should result in empty ProviderID in idle allocation, got %q",
- idle.Properties.ProviderID)
- }
- // Verify Node field is still populated
- if idle.Properties.Node != "bare-metal-node-1" {
- t.Errorf("Expected Node %q, got %q", "bare-metal-node-1", idle.Properties.Node)
- }
- }
- // TestComputeIdleAllocations_ProviderID_AzureFormat tests that Azure VM
- // ProviderIDs are correctly propagated to idle allocations.
- func TestComputeIdleAllocations_ProviderID_AzureFormat(t *testing.T) {
- // Create test window
- start := time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC)
- end := time.Date(2024, 1, 1, 1, 0, 0, 0, time.UTC)
- window := opencost.NewClosedWindow(start, end)
- // Create AssetSet with Azure VM
- assetSet := opencost.NewAssetSet(start, end)
- azureProviderID := "azure:///subscriptions/12345678-1234-1234-1234-123456789abc/resourceGroups/my-rg/providers/Microsoft.Compute/virtualMachines/aks-nodepool1-12345678-vmss000000"
- node := opencost.NewNode("aks-nodepool1-12345678-vmss000000", "cluster1", azureProviderID, start, end, window)
- node.CPUCost = 10.0
- node.RAMCost = 5.0
- node.GPUCost = 0.0
- assetSet.Insert(node, nil)
- // Create AllocationSet
- allocSet := opencost.NewAllocationSet(start, end)
- alloc := &opencost.Allocation{
- Name: "namespace1/pod1/container1",
- Start: start,
- End: end,
- Window: window.Clone(),
- CPUCost: 5.0,
- RAMCost: 2.5,
- GPUCost: 0.0,
- Properties: &opencost.AllocationProperties{
- Cluster: "cluster1",
- Node: "aks-nodepool1-12345678-vmss000000",
- },
- }
- allocSet.Insert(alloc)
- // Compute idle allocations
- idleSet, err := computeIdleAllocations(allocSet, assetSet, true)
- if err != nil {
- t.Fatalf("Error computing idle allocations: %v", err)
- }
- // Find the idle allocation
- var idle *opencost.Allocation
- for _, alloc := range idleSet.Allocations {
- if alloc.IsIdle() {
- idle = alloc
- break
- }
- }
- if idle == nil {
- t.Fatal("Expected to find idle allocation")
- }
- // Verify Azure ProviderID is correctly set
- if idle.Properties.ProviderID != azureProviderID {
- t.Errorf("Expected ProviderID %q, got %q", azureProviderID, idle.Properties.ProviderID)
- }
- }
|