| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624 |
- package costmodel
- import (
- "fmt"
- "testing"
- "time"
- "github.com/opencost/opencost/core/pkg/opencost"
- "github.com/opencost/opencost/core/pkg/source"
- "github.com/opencost/opencost/core/pkg/util"
- )
- const Ki = 1024
- const Mi = Ki * 1024
- const Gi = Mi * 1024
- const second = 1.0
- const minute = second * 60.0
- const hour = minute * 60.0
- var windowStart = time.Date(2020, 6, 16, 0, 0, 0, 0, time.UTC)
- var windowEnd = time.Date(2020, 6, 17, 0, 0, 0, 0, time.UTC)
- var window = opencost.NewWindow(&windowStart, &windowEnd)
- var startFloat = float64(windowStart.Unix())
- var endFloat = float64(windowEnd.Unix())
- var podKey1 = podKey{
- namespaceKey: namespaceKey{
- Cluster: "cluster1",
- Namespace: "namespace1",
- },
- Pod: "pod1",
- }
- var podKey2 = podKey{
- namespaceKey: namespaceKey{
- Cluster: "cluster1",
- Namespace: "namespace1",
- },
- Pod: "pod2",
- }
- var podKey3 = podKey{
- namespaceKey: namespaceKey{
- Cluster: "cluster2",
- Namespace: "namespace2",
- },
- Pod: "pod3",
- }
- var podKey4 = podKey{
- namespaceKey: namespaceKey{
- Cluster: "cluster2",
- Namespace: "namespace2",
- },
- Pod: "pod4",
- }
- var podKeyUnmounted = podKey{
- namespaceKey: namespaceKey{
- Cluster: "cluster2",
- Namespace: opencost.UnmountedSuffix,
- },
- Pod: opencost.UnmountedSuffix,
- }
- var kcPVKey1 = opencost.PVKey{
- Cluster: "cluster1",
- Name: "pv1",
- }
- var kcPVKey2 = opencost.PVKey{
- Cluster: "cluster1",
- Name: "pv2",
- }
- var kcPVKey3 = opencost.PVKey{
- Cluster: "cluster2",
- Name: "pv3",
- }
- var kcPVKey4 = opencost.PVKey{
- Cluster: "cluster2",
- Name: "pv4",
- }
- var podMap1 = map[podKey]*pod{
- podKey1: {
- Window: window.Clone(),
- Start: time.Date(2020, 6, 16, 0, 0, 0, 0, time.UTC),
- End: time.Date(2020, 6, 17, 0, 0, 0, 0, time.UTC),
- Key: podKey1,
- Allocations: nil,
- },
- podKey2: {
- Window: window.Clone(),
- Start: time.Date(2020, 6, 16, 12, 0, 0, 0, time.UTC),
- End: time.Date(2020, 6, 17, 0, 0, 0, 0, time.UTC),
- Key: podKey2,
- Allocations: nil,
- },
- podKey3: {
- Window: window.Clone(),
- Start: time.Date(2020, 6, 16, 6, 30, 0, 0, time.UTC),
- End: time.Date(2020, 6, 17, 18, 12, 33, 0, time.UTC),
- Key: podKey3,
- Allocations: nil,
- },
- podKey4: {
- Window: window.Clone(),
- Start: time.Date(2020, 6, 16, 0, 0, 0, 0, time.UTC),
- End: time.Date(2020, 6, 17, 13, 0, 0, 0, time.UTC),
- Key: podKey4,
- Allocations: nil,
- },
- podKeyUnmounted: {
- Window: window.Clone(),
- Start: *window.Start(),
- End: *window.End(),
- Key: podKeyUnmounted,
- Allocations: map[string]*opencost.Allocation{
- opencost.UnmountedSuffix: {
- Name: fmt.Sprintf("%s/%s/%s/%s", podKeyUnmounted.Cluster, podKeyUnmounted.Namespace, podKeyUnmounted.Pod, opencost.UnmountedSuffix),
- Properties: &opencost.AllocationProperties{
- Cluster: podKeyUnmounted.Cluster,
- Node: "",
- Container: opencost.UnmountedSuffix,
- Namespace: podKeyUnmounted.Namespace,
- Pod: podKeyUnmounted.Pod,
- Services: []string{"LB1"},
- },
- Window: window,
- Start: *window.Start(),
- End: *window.End(),
- LoadBalancerCost: 0.60,
- LoadBalancerCostAdjustment: 0,
- PVs: opencost.PVAllocations{
- kcPVKey2: &opencost.PVAllocation{
- ByteHours: 24 * Gi,
- Cost: 2.25,
- },
- },
- },
- },
- },
- }
- var pvKey1 = pvKey{
- Cluster: "cluster1",
- PersistentVolume: "pv1",
- }
- var pvKey2 = pvKey{
- Cluster: "cluster1",
- PersistentVolume: "pv2",
- }
- var pvKey3 = pvKey{
- Cluster: "cluster2",
- PersistentVolume: "pv3",
- }
- var pvKey4 = pvKey{
- Cluster: "cluster2",
- PersistentVolume: "pv4",
- }
- var pvMap1 = map[pvKey]*pv{
- pvKey1: {
- Start: windowStart,
- End: windowEnd.Add(time.Hour * -6),
- Bytes: 20 * Gi,
- CostPerGiBHour: 0.05,
- Cluster: "cluster1",
- Name: "pv1",
- StorageClass: "class1",
- },
- pvKey2: {
- Start: windowStart,
- End: windowEnd,
- Bytes: 100 * Gi,
- CostPerGiBHour: 0.05,
- Cluster: "cluster1",
- Name: "pv2",
- StorageClass: "class1",
- },
- pvKey3: {
- Start: windowStart.Add(time.Hour * 6),
- End: windowEnd.Add(time.Hour * -6),
- Bytes: 50 * Gi,
- CostPerGiBHour: 0.03,
- Cluster: "cluster2",
- Name: "pv3",
- StorageClass: "class2",
- },
- pvKey4: {
- Start: windowStart,
- End: windowEnd.Add(time.Hour * -6),
- Bytes: 30 * Gi,
- CostPerGiBHour: 0.05,
- Cluster: "cluster2",
- Name: "pv4",
- StorageClass: "class1",
- },
- }
- func TestBuildPVMap(t *testing.T) {
- pvMap1NoBytes := make(map[pvKey]*pv, len(pvMap1))
- for thisPVKey, thisPV := range pvMap1 {
- clonePV := thisPV.clone()
- clonePV.Bytes = 0.0
- clonePV.StorageClass = ""
- pvMap1NoBytes[thisPVKey] = clonePV
- }
- testCases := map[string]struct {
- resolution time.Duration
- resultsPVCostPerGiBHour []*source.QueryResult
- resultsActiveMinutes []*source.QueryResult
- expected map[pvKey]*pv
- }{
- "pvMap1": {
- resolution: time.Hour * 6,
- resultsPVCostPerGiBHour: []*source.QueryResult{
- source.NewQueryResult(
- map[string]interface{}{
- "cluster_id": "cluster1",
- "volumename": "pv1",
- },
- []*util.Vector{
- {
- Value: 0.05,
- },
- },
- source.DefaultResultKeys(),
- ),
- source.NewQueryResult(
- map[string]interface{}{
- "cluster_id": "cluster1",
- "volumename": "pv2",
- },
- []*util.Vector{
- {
- Value: 0.05,
- },
- },
- source.DefaultResultKeys(),
- ),
- source.NewQueryResult(
- map[string]interface{}{
- "cluster_id": "cluster2",
- "volumename": "pv3",
- },
- []*util.Vector{
- {
- Value: 0.03,
- },
- },
- source.DefaultResultKeys(),
- ),
- source.NewQueryResult(
- map[string]interface{}{
- "cluster_id": "cluster2",
- "volumename": "pv4",
- },
- []*util.Vector{
- {
- Value: 0.05,
- },
- },
- source.DefaultResultKeys(),
- ),
- },
- resultsActiveMinutes: []*source.QueryResult{
- source.NewQueryResult(
- map[string]interface{}{
- "cluster_id": "cluster1",
- "persistentvolume": "pv1",
- },
- []*util.Vector{
- {
- Timestamp: startFloat,
- },
- {
- Timestamp: startFloat + (hour * 6),
- },
- {
- Timestamp: startFloat + (hour * 12),
- },
- {
- Timestamp: startFloat + (hour * 18),
- },
- },
- source.DefaultResultKeys(),
- ),
- source.NewQueryResult(
- map[string]interface{}{
- "cluster_id": "cluster1",
- "persistentvolume": "pv2",
- },
- []*util.Vector{
- {
- Timestamp: startFloat,
- },
- {
- Timestamp: startFloat + (hour * 6),
- },
- {
- Timestamp: startFloat + (hour * 12),
- },
- {
- Timestamp: startFloat + (hour * 18),
- },
- {
- Timestamp: startFloat + (hour * 24),
- },
- },
- source.DefaultResultKeys(),
- ),
- source.NewQueryResult(
- map[string]interface{}{
- "cluster_id": "cluster2",
- "persistentvolume": "pv3",
- },
- []*util.Vector{
- {
- Timestamp: startFloat + (hour * 6),
- },
- {
- Timestamp: startFloat + (hour * 12),
- },
- {
- Timestamp: startFloat + (hour * 18),
- },
- },
- source.DefaultResultKeys(),
- ),
- source.NewQueryResult(
- map[string]interface{}{
- "cluster_id": "cluster2",
- "persistentvolume": "pv4",
- },
- []*util.Vector{
- {
- Timestamp: startFloat,
- },
- {
- Timestamp: startFloat + (hour * 6),
- },
- {
- Timestamp: startFloat + (hour * 12),
- },
- {
- Timestamp: startFloat + (hour * 18),
- },
- },
- source.DefaultResultKeys(),
- ),
- },
- expected: pvMap1NoBytes,
- },
- }
- for name, testCase := range testCases {
- t.Run(name, func(t *testing.T) {
- pvMap := make(map[pvKey]*pv)
- pvCostResults := source.DecodeAll(testCase.resultsPVCostPerGiBHour, source.DecodePVPricePerGiBHourResult)
- pvActiveMinsResults := source.DecodeAll(testCase.resultsActiveMinutes, source.DecodePVActiveMinutesResult)
- pvInfoResult := []*source.PVInfoResult{}
- buildPVMap(testCase.resolution, pvMap, pvCostResults, pvActiveMinsResults, pvInfoResult, window)
- if len(pvMap) != len(testCase.expected) {
- t.Errorf("pv map does not have the expected length %d : %d", len(pvMap), len(testCase.expected))
- }
- for thisPVKey, expectedPV := range testCase.expected {
- actualPV, ok := pvMap[thisPVKey]
- if !ok {
- t.Errorf("pv map is missing key %s", thisPVKey)
- }
- if !actualPV.equal(expectedPV) {
- t.Errorf("pv does not match with key %s: %s != %s", thisPVKey, opencost.NewClosedWindow(actualPV.Start, actualPV.End), opencost.NewClosedWindow(expectedPV.Start, expectedPV.End))
- }
- }
- })
- }
- }
- func TestGetUnmountedPodForCluster(t *testing.T) {
- testCases := map[string]struct {
- window opencost.Window
- podMap map[podKey]*pod
- cluster string
- expected *pod
- }{
- "create new": {
- window: window.Clone(),
- podMap: podMap1,
- cluster: "cluster1",
- expected: &pod{
- Window: window.Clone(),
- Start: *window.Start(),
- End: *window.End(),
- Key: getUnmountedPodKey("cluster1"),
- Allocations: map[string]*opencost.Allocation{
- opencost.UnmountedSuffix: {
- Name: fmt.Sprintf("%s/%s/%s/%s", "cluster1", opencost.UnmountedSuffix, opencost.UnmountedSuffix, opencost.UnmountedSuffix),
- Properties: &opencost.AllocationProperties{
- Cluster: "cluster1",
- Node: "",
- Container: opencost.UnmountedSuffix,
- Namespace: opencost.UnmountedSuffix,
- Pod: opencost.UnmountedSuffix,
- },
- Window: window,
- Start: *window.Start(),
- End: *window.End(),
- },
- },
- },
- },
- "get existing": {
- window: window.Clone(),
- podMap: podMap1,
- cluster: "cluster2",
- expected: &pod{
- Window: window.Clone(),
- Start: *window.Start(),
- End: *window.End(),
- Key: getUnmountedPodKey("cluster2"),
- Allocations: map[string]*opencost.Allocation{
- opencost.UnmountedSuffix: {
- Name: fmt.Sprintf("%s/%s/%s/%s", "cluster2", opencost.UnmountedSuffix, opencost.UnmountedSuffix, opencost.UnmountedSuffix),
- Properties: &opencost.AllocationProperties{
- Cluster: "cluster2",
- Node: "",
- Container: opencost.UnmountedSuffix,
- Namespace: opencost.UnmountedSuffix,
- Pod: opencost.UnmountedSuffix,
- Services: []string{"LB1"},
- },
- Window: window,
- Start: *window.Start(),
- End: *window.End(),
- LoadBalancerCost: .60,
- LoadBalancerCostAdjustment: 0,
- PVs: opencost.PVAllocations{
- kcPVKey2: &opencost.PVAllocation{
- ByteHours: 24 * Gi,
- Cost: 2.25,
- },
- },
- },
- },
- },
- },
- }
- for name, testCase := range testCases {
- t.Run(name, func(t *testing.T) {
- actual := getUnmountedPodForCluster(testCase.window, testCase.podMap, testCase.cluster)
- if !actual.equal(testCase.expected) {
- t.Errorf("Unmounted pod does not match expectation")
- }
- })
- }
- }
- func TestCalculateStartAndEnd(t *testing.T) {
- testCases := map[string]struct {
- resolution time.Duration // User defined config when querying Prometheus
- expectedStart time.Time
- expectedEnd time.Time
- result *source.QueryResult
- }{
- // Example: avg(node_total_hourly_cost{}) by (node, provider_id)[1h:1h]
- "1 hour resolution, 1 hour window": {
- resolution: time.Hour,
- expectedStart: windowStart,
- expectedEnd: windowStart.Add(time.Hour),
- result: &source.QueryResult{
- Values: []*util.Vector{
- {
- Timestamp: startFloat,
- },
- {
- Timestamp: startFloat + (minute * 60),
- },
- },
- },
- },
- // Example: avg(node_total_hourly_cost{}) by (node, provider_id)[1h:30m]
- "30 minute resolution, 1 hour window": {
- resolution: time.Minute * 30,
- expectedStart: windowStart,
- expectedEnd: windowStart.Add(time.Hour),
- result: &source.QueryResult{
- Values: []*util.Vector{
- {
- Timestamp: startFloat,
- },
- {
- Timestamp: startFloat + (minute * 30),
- },
- {
- Timestamp: startFloat + (minute * 60),
- },
- },
- },
- },
- // Example: avg(node_total_hourly_cost{}) by (node, provider_id)[45m:15m]
- "15 minute resolution, 45 minute window": {
- resolution: time.Minute * 15,
- expectedStart: windowStart,
- expectedEnd: windowStart.Add(time.Minute * 45),
- result: &source.QueryResult{
- Values: []*util.Vector{
- {
- Timestamp: startFloat + (minute * 0),
- },
- {
- Timestamp: startFloat + (minute * 15),
- },
- {
- Timestamp: startFloat + (minute * 30),
- },
- {
- Timestamp: startFloat + (minute * 45),
- },
- },
- },
- },
- "1 minute resolution, 5 minute window": {
- resolution: time.Minute,
- expectedStart: windowStart.Add(time.Minute * 15),
- expectedEnd: windowStart.Add(time.Minute * 20),
- result: &source.QueryResult{
- Values: []*util.Vector{
- {
- Timestamp: startFloat + (minute * 15),
- },
- {
- Timestamp: startFloat + (minute * 16),
- },
- {
- Timestamp: startFloat + (minute * 17),
- },
- {
- Timestamp: startFloat + (minute * 18),
- },
- {
- Timestamp: startFloat + (minute * 19),
- },
- {
- Timestamp: startFloat + (minute * 20),
- },
- },
- },
- },
- // Example: avg(node_total_hourly_cost{}) by (node, provider_id)[1m:1m]
- "1 minute resolution, 1 minute window": {
- resolution: time.Minute,
- expectedStart: windowStart.Add(time.Minute * 15),
- expectedEnd: windowStart.Add(time.Minute * 16),
- result: &source.QueryResult{
- Values: []*util.Vector{
- {
- Timestamp: startFloat + (minute * 15),
- },
- },
- },
- },
- // Example: avg(node_total_hourly_cost{}) by (node, provider_id)[1m:1m]
- "1 minute resolution, 1 minute window, at window start": {
- resolution: time.Minute,
- expectedStart: windowStart,
- expectedEnd: windowStart.Add(time.Minute),
- result: &source.QueryResult{
- Values: []*util.Vector{
- {
- Timestamp: startFloat,
- },
- },
- },
- },
- // Example: avg(node_total_hourly_cost{}) by (node, provider_id)[1m:1m]
- "1 minute resolution, 1 minute window, at window end": {
- resolution: time.Minute,
- expectedStart: windowEnd,
- expectedEnd: windowEnd,
- result: &source.QueryResult{
- Values: []*util.Vector{
- {
- Timestamp: endFloat,
- },
- },
- },
- },
- // Example: avg(node_total_hourly_cost{}) by (node, provider_id)[1m:1m]
- "1 minute resolution, 1 minute window, near window end": {
- resolution: time.Minute,
- expectedStart: windowEnd.Add(-time.Second * 15),
- expectedEnd: windowEnd,
- result: &source.QueryResult{
- Values: []*util.Vector{
- {
- Timestamp: endFloat - 15.0,
- },
- },
- },
- },
- }
- for name, testCase := range testCases {
- t.Run(name, func(t *testing.T) {
- start, end := calculateStartAndEnd(testCase.result.Values, testCase.resolution, window)
- if !start.Equal(testCase.expectedStart) {
- t.Errorf("start does not match: expected %v; got %v", testCase.expectedStart, start)
- }
- if !end.Equal(testCase.expectedEnd) {
- t.Errorf("end does not match: expected %v; got %v", testCase.expectedEnd, end)
- }
- })
- }
- }
|