| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618 |
- package costmodel
- import (
- "io"
- "testing"
- "github.com/opencost/opencost/core/pkg/clustercache"
- "github.com/opencost/opencost/core/pkg/source"
- "github.com/opencost/opencost/core/pkg/util"
- "github.com/opencost/opencost/pkg/cloud/models"
- )
- // mockProvider is a mock implementation of the Provider interface for testing
- type mockProvider struct {
- network *models.Network
- err error
- }
- func (m *mockProvider) NetworkPricing() (*models.Network, error) {
- return m.network, m.err
- }
- func (m *mockProvider) GetKey(map[string]string, *clustercache.Node) models.Key {
- return nil
- }
- func (m *mockProvider) PVPricing(models.PVKey) (*models.PV, error) {
- return nil, nil
- }
- func (m *mockProvider) NodePricing(models.Key) (*models.Node, models.PricingMetadata, error) {
- return nil, models.PricingMetadata{}, nil
- }
- func (m *mockProvider) LoadBalancerPricing() (*models.LoadBalancer, error) {
- return nil, nil
- }
- func (m *mockProvider) AllNodePricing() (interface{}, error) {
- return nil, nil
- }
- func (m *mockProvider) ClusterInfo() (map[string]string, error) {
- return nil, nil
- }
- func (m *mockProvider) GetAddresses() ([]byte, error) {
- return nil, nil
- }
- func (m *mockProvider) GetDisks() ([]byte, error) {
- return nil, nil
- }
- func (m *mockProvider) GetOrphanedResources() ([]models.OrphanedResource, error) {
- return nil, nil
- }
- func (m *mockProvider) GpuPricing(map[string]string) (string, error) {
- return "", nil
- }
- func (m *mockProvider) DownloadPricingData() error {
- return nil
- }
- func (m *mockProvider) GetPVKey(*clustercache.PersistentVolume, map[string]string, string) models.PVKey {
- return nil
- }
- func (m *mockProvider) UpdateConfig(io.Reader, string) (*models.CustomPricing, error) {
- return nil, nil
- }
- func (m *mockProvider) UpdateConfigFromConfigMap(map[string]string) (*models.CustomPricing, error) {
- return nil, nil
- }
- func (m *mockProvider) GetConfig() (*models.CustomPricing, error) {
- return nil, nil
- }
- func (m *mockProvider) GetManagementPlatform() (string, error) {
- return "", nil
- }
- func (m *mockProvider) ApplyReservedInstancePricing(map[string]*models.Node) {
- }
- func (m *mockProvider) ServiceAccountStatus() *models.ServiceAccountStatus {
- return nil
- }
- func (m *mockProvider) PricingSourceStatus() map[string]*models.PricingSource {
- return nil
- }
- func (m *mockProvider) ClusterManagementPricing() (string, float64, error) {
- return "", 0.0, nil
- }
- func (m *mockProvider) CombinedDiscountForNode(string, bool, float64, float64) float64 {
- return 0.0
- }
- func (m *mockProvider) Regions() []string {
- return nil
- }
- func (m *mockProvider) PricingSourceSummary() interface{} {
- return nil
- }
- // TestGetNetworkUsageData tests the aggregation of NAT Gateway egress and ingress data
- func TestGetNetworkUsageData(t *testing.T) {
- defaultClusterID := "default-cluster"
- testCases := []struct {
- name string
- zoneResults []*source.NetZoneGiBResult
- regionResults []*source.NetRegionGiBResult
- internetResults []*source.NetInternetGiBResult
- natGatewayEgressResults []*source.NetNatGatewayGiBResult
- natGatewayIngressResults []*source.NetNatGatewayIngressGiBResult
- expectedKeys []string
- validateFunc func(t *testing.T, result map[string]*NetworkUsageData)
- }{
- {
- name: "NAT Gateway egress only",
- zoneResults: nil,
- regionResults: nil,
- internetResults: nil,
- natGatewayEgressResults: []*source.NetNatGatewayGiBResult{
- {
- Pod: "pod1",
- Namespace: "ns1",
- Cluster: "cluster1",
- Data: []*util.Vector{
- {Value: 10.5, Timestamp: 1000},
- {Value: 20.3, Timestamp: 2000},
- },
- },
- },
- natGatewayIngressResults: nil,
- expectedKeys: []string{"ns1,pod1,cluster1"},
- validateFunc: func(t *testing.T, result map[string]*NetworkUsageData) {
- key := "ns1,pod1,cluster1"
- if data, ok := result[key]; ok {
- if len(data.NetworkNatGatewayEgress) != 2 {
- t.Errorf("expected 2 NAT Gateway egress vectors, got %d", len(data.NetworkNatGatewayEgress))
- }
- if data.NetworkNatGatewayEgress[0].Value != 10.5 {
- t.Errorf("expected first egress value 10.5, got %f", data.NetworkNatGatewayEgress[0].Value)
- }
- if len(data.NetworkNatGatewayIngress) != 0 {
- t.Errorf("expected 0 NAT Gateway ingress vectors, got %d", len(data.NetworkNatGatewayIngress))
- }
- } else {
- t.Errorf("expected key %s not found in result", key)
- }
- },
- },
- {
- name: "NAT Gateway ingress only",
- zoneResults: nil,
- regionResults: nil,
- internetResults: nil,
- natGatewayEgressResults: nil,
- natGatewayIngressResults: []*source.NetNatGatewayIngressGiBResult{
- {
- Pod: "pod2",
- Namespace: "ns2",
- Cluster: "cluster2",
- Data: []*util.Vector{
- {Value: 5.2, Timestamp: 1000},
- },
- },
- },
- expectedKeys: []string{"ns2,pod2,cluster2"},
- validateFunc: func(t *testing.T, result map[string]*NetworkUsageData) {
- key := "ns2,pod2,cluster2"
- if data, ok := result[key]; ok {
- if len(data.NetworkNatGatewayIngress) != 1 {
- t.Errorf("expected 1 NAT Gateway ingress vector, got %d", len(data.NetworkNatGatewayIngress))
- }
- if data.NetworkNatGatewayIngress[0].Value != 5.2 {
- t.Errorf("expected ingress value 5.2, got %f", data.NetworkNatGatewayIngress[0].Value)
- }
- if len(data.NetworkNatGatewayEgress) != 0 {
- t.Errorf("expected 0 NAT Gateway egress vectors, got %d", len(data.NetworkNatGatewayEgress))
- }
- } else {
- t.Errorf("expected key %s not found in result", key)
- }
- },
- },
- {
- name: "NAT Gateway egress and ingress for same pod",
- zoneResults: nil,
- regionResults: nil,
- internetResults: nil,
- natGatewayEgressResults: []*source.NetNatGatewayGiBResult{
- {
- Pod: "pod3",
- Namespace: "ns3",
- Cluster: "cluster3",
- Data: []*util.Vector{
- {Value: 15.0, Timestamp: 1000},
- },
- },
- },
- natGatewayIngressResults: []*source.NetNatGatewayIngressGiBResult{
- {
- Pod: "pod3",
- Namespace: "ns3",
- Cluster: "cluster3",
- Data: []*util.Vector{
- {Value: 8.5, Timestamp: 1000},
- },
- },
- },
- expectedKeys: []string{"ns3,pod3,cluster3"},
- validateFunc: func(t *testing.T, result map[string]*NetworkUsageData) {
- key := "ns3,pod3,cluster3"
- if data, ok := result[key]; ok {
- if len(data.NetworkNatGatewayEgress) != 1 {
- t.Errorf("expected 1 NAT Gateway egress vector, got %d", len(data.NetworkNatGatewayEgress))
- }
- if data.NetworkNatGatewayEgress[0].Value != 15.0 {
- t.Errorf("expected egress value 15.0, got %f", data.NetworkNatGatewayEgress[0].Value)
- }
- if len(data.NetworkNatGatewayIngress) != 1 {
- t.Errorf("expected 1 NAT Gateway ingress vector, got %d", len(data.NetworkNatGatewayIngress))
- }
- if data.NetworkNatGatewayIngress[0].Value != 8.5 {
- t.Errorf("expected ingress value 8.5, got %f", data.NetworkNatGatewayIngress[0].Value)
- }
- } else {
- t.Errorf("expected key %s not found in result", key)
- }
- },
- },
- {
- name: "Mixed network traffic with NAT Gateway",
- zoneResults: []*source.NetZoneGiBResult{
- {
- Pod: "pod4",
- Namespace: "ns4",
- Cluster: "cluster4",
- Data: []*util.Vector{
- {Value: 3.0, Timestamp: 1000},
- },
- },
- },
- regionResults: []*source.NetRegionGiBResult{
- {
- Pod: "pod4",
- Namespace: "ns4",
- Cluster: "cluster4",
- Data: []*util.Vector{
- {Value: 7.0, Timestamp: 1000},
- },
- },
- },
- internetResults: []*source.NetInternetGiBResult{
- {
- Pod: "pod4",
- Namespace: "ns4",
- Cluster: "cluster4",
- Data: []*util.Vector{
- {Value: 12.0, Timestamp: 1000},
- },
- },
- },
- natGatewayEgressResults: []*source.NetNatGatewayGiBResult{
- {
- Pod: "pod4",
- Namespace: "ns4",
- Cluster: "cluster4",
- Data: []*util.Vector{
- {Value: 18.0, Timestamp: 1000},
- },
- },
- },
- natGatewayIngressResults: []*source.NetNatGatewayIngressGiBResult{
- {
- Pod: "pod4",
- Namespace: "ns4",
- Cluster: "cluster4",
- Data: []*util.Vector{
- {Value: 9.0, Timestamp: 1000},
- },
- },
- },
- expectedKeys: []string{"ns4,pod4,cluster4"},
- validateFunc: func(t *testing.T, result map[string]*NetworkUsageData) {
- key := "ns4,pod4,cluster4"
- if data, ok := result[key]; ok {
- // Verify all network types are present
- if len(data.NetworkZoneEgress) != 1 {
- t.Errorf("expected 1 zone egress vector, got %d", len(data.NetworkZoneEgress))
- }
- if len(data.NetworkRegionEgress) != 1 {
- t.Errorf("expected 1 region egress vector, got %d", len(data.NetworkRegionEgress))
- }
- if len(data.NetworkInternetEgress) != 1 {
- t.Errorf("expected 1 internet egress vector, got %d", len(data.NetworkInternetEgress))
- }
- if len(data.NetworkNatGatewayEgress) != 1 {
- t.Errorf("expected 1 NAT Gateway egress vector, got %d", len(data.NetworkNatGatewayEgress))
- }
- if len(data.NetworkNatGatewayIngress) != 1 {
- t.Errorf("expected 1 NAT Gateway ingress vector, got %d", len(data.NetworkNatGatewayIngress))
- }
- // Verify values
- if data.NetworkNatGatewayEgress[0].Value != 18.0 {
- t.Errorf("expected NAT Gateway egress 18.0, got %f", data.NetworkNatGatewayEgress[0].Value)
- }
- if data.NetworkNatGatewayIngress[0].Value != 9.0 {
- t.Errorf("expected NAT Gateway ingress 9.0, got %f", data.NetworkNatGatewayIngress[0].Value)
- }
- } else {
- t.Errorf("expected key %s not found in result", key)
- }
- },
- },
- {
- name: "Default cluster ID fallback for NAT Gateway",
- zoneResults: nil,
- regionResults: nil,
- internetResults: nil,
- natGatewayEgressResults: []*source.NetNatGatewayGiBResult{
- {
- Pod: "pod5",
- Namespace: "ns5",
- Cluster: "", // Empty cluster ID should use default
- Data: []*util.Vector{
- {Value: 5.0, Timestamp: 1000},
- },
- },
- },
- natGatewayIngressResults: nil,
- expectedKeys: []string{"ns5,pod5,default-cluster"},
- validateFunc: func(t *testing.T, result map[string]*NetworkUsageData) {
- key := "ns5,pod5,default-cluster"
- if data, ok := result[key]; ok {
- if data.ClusterID != "default-cluster" {
- t.Errorf("expected cluster ID 'default-cluster', got %s", data.ClusterID)
- }
- } else {
- t.Errorf("expected key %s not found in result", key)
- }
- },
- },
- }
- for _, tc := range testCases {
- t.Run(tc.name, func(t *testing.T) {
- result, err := GetNetworkUsageData(
- tc.zoneResults,
- tc.regionResults,
- tc.internetResults,
- tc.natGatewayEgressResults,
- tc.natGatewayIngressResults,
- defaultClusterID,
- )
- if err != nil {
- t.Fatalf("unexpected error: %v", err)
- }
- if len(result) != len(tc.expectedKeys) {
- t.Errorf("expected %d keys, got %d", len(tc.expectedKeys), len(result))
- }
- for _, key := range tc.expectedKeys {
- if _, ok := result[key]; !ok {
- t.Errorf("expected key %s not found in result", key)
- }
- }
- if tc.validateFunc != nil {
- tc.validateFunc(t, result)
- }
- })
- }
- }
- // TestGetNetworkCost tests the calculation of NAT Gateway costs
- func TestGetNetworkCost(t *testing.T) {
- testCases := []struct {
- name string
- usage *NetworkUsageData
- pricing *models.Network
- expectedCost float64
- expectedLength int
- }{
- {
- name: "NAT Gateway egress cost only",
- usage: &NetworkUsageData{
- ClusterID: "cluster1",
- PodName: "pod1",
- Namespace: "ns1",
- NetworkNatGatewayEgress: []*util.Vector{
- {Value: 10.0, Timestamp: 1000}, // 10 GiB
- },
- },
- pricing: &models.Network{
- NatGatewayEgressCost: 0.05, // $0.05 per GiB
- },
- expectedCost: 0.50, // 10 * 0.05 = 0.50
- expectedLength: 1,
- },
- {
- name: "NAT Gateway ingress cost only",
- usage: &NetworkUsageData{
- ClusterID: "cluster1",
- PodName: "pod1",
- Namespace: "ns1",
- NetworkNatGatewayIngress: []*util.Vector{
- {Value: 20.0, Timestamp: 1000}, // 20 GiB
- },
- },
- pricing: &models.Network{
- NatGatewayIngressCost: 0.02, // $0.02 per GiB
- },
- expectedCost: 0.40, // 20 * 0.02 = 0.40
- expectedLength: 1,
- },
- {
- name: "NAT Gateway egress and ingress costs",
- usage: &NetworkUsageData{
- ClusterID: "cluster1",
- PodName: "pod1",
- Namespace: "ns1",
- NetworkNatGatewayEgress: []*util.Vector{
- {Value: 10.0, Timestamp: 1000},
- },
- NetworkNatGatewayIngress: []*util.Vector{
- {Value: 5.0, Timestamp: 1000},
- },
- },
- pricing: &models.Network{
- NatGatewayEgressCost: 0.05, // $0.05 per GiB
- NatGatewayIngressCost: 0.02, // $0.02 per GiB
- },
- expectedCost: 0.60, // (10 * 0.05) + (5 * 0.02) = 0.50 + 0.10 = 0.60
- expectedLength: 1,
- },
- {
- name: "Mixed network costs with NAT Gateway",
- usage: &NetworkUsageData{
- ClusterID: "cluster1",
- PodName: "pod1",
- Namespace: "ns1",
- NetworkZoneEgress: []*util.Vector{
- {Value: 5.0, Timestamp: 1000},
- },
- NetworkRegionEgress: []*util.Vector{
- {Value: 8.0, Timestamp: 1000},
- },
- NetworkInternetEgress: []*util.Vector{
- {Value: 12.0, Timestamp: 1000},
- },
- NetworkNatGatewayEgress: []*util.Vector{
- {Value: 15.0, Timestamp: 1000},
- },
- NetworkNatGatewayIngress: []*util.Vector{
- {Value: 10.0, Timestamp: 1000},
- },
- },
- pricing: &models.Network{
- ZoneNetworkEgressCost: 0.01,
- RegionNetworkEgressCost: 0.02,
- InternetNetworkEgressCost: 0.09,
- NatGatewayEgressCost: 0.05,
- NatGatewayIngressCost: 0.02,
- },
- expectedCost: 2.24, // (5*0.01) + (8*0.02) + (12*0.09) + (15*0.05) + (10*0.02) = 0.05 + 0.16 + 1.08 + 0.75 + 0.20 = 2.24
- expectedLength: 1,
- },
- {
- name: "Multiple time points with NAT Gateway",
- usage: &NetworkUsageData{
- ClusterID: "cluster1",
- PodName: "pod1",
- Namespace: "ns1",
- NetworkNatGatewayEgress: []*util.Vector{
- {Value: 10.0, Timestamp: 1000},
- {Value: 15.0, Timestamp: 2000},
- {Value: 20.0, Timestamp: 3000},
- },
- NetworkNatGatewayIngress: []*util.Vector{
- {Value: 5.0, Timestamp: 1000},
- {Value: 8.0, Timestamp: 2000},
- {Value: 12.0, Timestamp: 3000},
- },
- },
- pricing: &models.Network{
- NatGatewayEgressCost: 0.05,
- NatGatewayIngressCost: 0.02,
- },
- expectedLength: 3,
- },
- {
- name: "Zero NAT Gateway costs",
- usage: &NetworkUsageData{
- ClusterID: "cluster1",
- PodName: "pod1",
- Namespace: "ns1",
- NetworkNatGatewayEgress: []*util.Vector{
- {Value: 100.0, Timestamp: 1000},
- },
- NetworkNatGatewayIngress: []*util.Vector{
- {Value: 50.0, Timestamp: 1000},
- },
- },
- pricing: &models.Network{
- NatGatewayEgressCost: 0.0,
- NatGatewayIngressCost: 0.0,
- },
- expectedCost: 0.0,
- expectedLength: 1,
- },
- }
- for _, tc := range testCases {
- t.Run(tc.name, func(t *testing.T) {
- provider := &mockProvider{
- network: tc.pricing,
- }
- result, err := GetNetworkCost(tc.usage, provider)
- if err != nil {
- t.Fatalf("unexpected error: %v", err)
- }
- if len(result) != tc.expectedLength {
- t.Errorf("expected %d result vectors, got %d", tc.expectedLength, len(result))
- }
- if tc.expectedLength > 0 {
- totalCost := 0.0
- for _, v := range result {
- totalCost += v.Value
- }
- if tc.expectedCost > 0 {
- if diff := totalCost - tc.expectedCost; diff > 0.001 || diff < -0.001 {
- t.Errorf("expected total cost %f, got %f", tc.expectedCost, totalCost)
- }
- }
- }
- })
- }
- }
- // TestGetNetworkCost_NATGatewayMisalignedVectors tests NAT Gateway cost calculation with different vector lengths
- func TestGetNetworkCost_NATGatewayMisalignedVectors(t *testing.T) {
- usage := &NetworkUsageData{
- ClusterID: "cluster1",
- PodName: "pod1",
- Namespace: "ns1",
- NetworkZoneEgress: []*util.Vector{
- {Value: 5.0, Timestamp: 1000},
- {Value: 6.0, Timestamp: 2000},
- },
- NetworkNatGatewayEgress: []*util.Vector{
- {Value: 10.0, Timestamp: 1000},
- {Value: 15.0, Timestamp: 2000},
- {Value: 20.0, Timestamp: 3000}, // Extra NAT Gateway data point
- },
- NetworkNatGatewayIngress: []*util.Vector{
- {Value: 5.0, Timestamp: 1000}, // Only one ingress data point
- },
- }
- pricing := &models.Network{
- ZoneNetworkEgressCost: 0.01,
- NatGatewayEgressCost: 0.05,
- NatGatewayIngressCost: 0.02,
- }
- provider := &mockProvider{
- network: pricing,
- }
- result, err := GetNetworkCost(usage, provider)
- if err != nil {
- t.Fatalf("unexpected error: %v", err)
- }
- // Should have 3 result vectors (max of all vector lengths including NAT Gateway)
- if len(result) != 3 {
- t.Errorf("expected 3 result vectors (max of all vectors including NAT Gateway), got %d", len(result))
- }
- // First vector: zone (5*0.01) + natEgress (10*0.05) + natIngress (5*0.02) = 0.05 + 0.50 + 0.10 = 0.65
- expectedFirst := (5.0 * 0.01) + (10.0 * 0.05) + (5.0 * 0.02)
- if diff := result[0].Value - expectedFirst; diff > 0.001 || diff < -0.001 {
- t.Errorf("expected first vector cost %f, got %f", expectedFirst, result[0].Value)
- }
- // Second vector: zone (6*0.01) + natEgress (15*0.05) = 0.06 + 0.75 = 0.81
- // (no NAT ingress for second time point)
- expectedSecond := (6.0 * 0.01) + (15.0 * 0.05)
- if diff := result[1].Value - expectedSecond; diff > 0.001 || diff < -0.001 {
- t.Errorf("expected second vector cost %f, got %f", expectedSecond, result[1].Value)
- }
- // Third vector: only natEgress (20*0.05) = 1.00
- // (no zone, region, internet, or NAT ingress for third time point)
- expectedThird := 20.0 * 0.05
- if diff := result[2].Value - expectedThird; diff > 0.001 || diff < -0.001 {
- t.Errorf("expected third vector cost %f, got %f", expectedThird, result[2].Value)
- }
- }
|