diff --git a/CHANGELOG.md b/CHANGELOG.md index d816552..8c40397 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## v1.4 + +- Time window sent to kubecost API was corrected. Was added a config var to allow send previous month of data to Flexera. + ## v1.3 - Added namespaceLabels field to the exported field "labels" in the CSV file diff --git a/README.md b/README.md index 6eb7283..40ca0e4 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,7 @@ go install github.com/flexera-public/cbi-oi-kubecost-exporter The app is configured using environment variables defined in a .env file. The following configuration options are available: - `KUBECOST_HOST` - the hostname of the Kubecost instance +- `KUBECOST_API_PATH` - the base path for the Kubecost API endpoints - `BILL_CONNECT_ID` - the ID of the bill connect to which to upload the data. To learn more about Bill Connect, and how to obtain your BILL_CONNECT_ID, please refer to [this guide](https://docs.flexera.com/flexera/EN/Optima/CreateKubecostBillConnect.htm) in the Flexera documentation. - `SHARD` - the region of your Flexera One account. Valid values are NAM, EU or AU. - `ORG_ID` - the ID of your Flexera One organization. @@ -27,8 +28,9 @@ The app is configured using environment variables defined in a .env file. The fo - `SHARE_TENANCY_COSTS` - a flag indicating whether to share tenancy costs among clusters - `MULTIPLIER` - a multiplier to apply to the cost data - `IDLE` - whether to include idle resources in the usage data. valid values are true or false. +- `FILE_ROTATION` - whether to delete files generated during the previous month (or the month before the previous month if INCLUDE_PREVIOUS_MONTH is set to true). Valid values are true or false. - `FILE_PATH` - the path where the CSV files are stored -- `UPLOAD_TIMEOUT` - the timeout for uploading the CSV files to Flexera One, in seconds. +- `INCLUDE_PREVIOUS_MONTH` - whether to include data from previous month to export process. Valid values are true or false. To use this app, run: @@ -36,9 +38,9 @@ To use this app, run: flexera-kubecost-exporter ``` -### Helm package manager +### Kubecost exporter helm chart for Kubernetes -There are two different approaches for passing custom Helm config values into the kubecost-exporter: +There are two different ways to transfer custom Helm configuration values to the kubecost-exporter: #### 1. Pass exact parameters via --set command-line flags: @@ -118,18 +120,19 @@ You should see 200/201s in the logs, which indicates that the exporter is workin ## Values | Key | Type | Default | Description | -| --- | --- | --- | --- | +|-----|------|---------|-------------| | cronSchedule | string | `"0 */6 * * *"` | Setting up a cronJob scheduler to run an export task at the right time | | filePath | string | `"/var/kubecost"` | Filepath to mount persistent volume | -| fileRotation | bool | `true` | Delete files generated for the previous month | +| fileRotation | bool | `true` | Delete files generated for the previous month (or the month before the previous month if INCLUDE_PREVIOUS_MONTH is set to true) | | flexera.billConnectId | string | `"cbi-oi-kubecost-1"` | Bill Connect ID | | flexera.orgId | string | `""` | Flexera Organization ID | -| flexera.refreshToken | string | `""` | Refresh Token from FlexeraOne | +| flexera.refreshToken | string | `""` | Refresh Token from FlexeraOne You can provide the refresh token in two ways: 1. Directly as a string: refreshToken: "your_token_here" 2. Reference it from a Kubernetes secret: refreshToken: valueFrom: secretKeyRef: name: flexera-secrets # Name of the Kubernetes secret key: refresh_token # Key in the secret containing the refresh token | | flexera.shard | string | `"NAM"` | Shard ("NAM", "EU", "AU") | | image.pullPolicy | string | `"Always"` | | | image.repository | string | `"public.ecr.aws/flexera/cbi-oi-kubecost-exporter"` | | | image.tag | string | `"latest"` | | | imagePullSecrets | list | `[]` | | +| includePreviousMonth | bool | `false` | Include data from previous month to export process | | kubecost.aggregation | string | `"pod"` | Aggregation Level ("namespace", "controller", "pod") | | kubecost.apiPath | string | `"/model/"` | Base path for the Kubecost API endpoints | | kubecost.host | string | `"kubecost-cost-analyzer.kubecost.svc.cluster.local:9090"` | Default kubecost-cost-analyzer service host on the current cluster. For current cluster is serviceName.namespaceName.svc.cluster.local | diff --git a/helm-chart/Chart.yaml b/helm-chart/Chart.yaml index 94c142a..5dacc34 100644 --- a/helm-chart/Chart.yaml +++ b/helm-chart/Chart.yaml @@ -6,10 +6,10 @@ description: Kubecost exporter helm chart for Kubernetes # This is the chart version. This version number should be incremented each time you make changes # to the chart and its templates, including the app version. # Versions are expected to follow Semantic Versioning (https://semver.org/) -version: 1.3.0 +version: 1.4.0 # This is the version number of the application being deployed. This version number should be # incremented each time you make changes to the application. Versions are not expected to # follow Semantic Versioning. They should reflect the version the application is using. # It is recommended to use it with quotes. -appVersion: "1.3" +appVersion: "1.4" diff --git a/helm-chart/README.md b/helm-chart/README.md index 49bc16d..2934c24 100644 --- a/helm-chart/README.md +++ b/helm-chart/README.md @@ -1,6 +1,6 @@ # cbi-oi-kubecost-exporter -![Version: 1.2.0](https://img.shields.io/badge/Version-1.2.0-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: 1.2](https://img.shields.io/badge/AppVersion-1.2-informational?style=flat-square) +![Version: 1.4.0](https://img.shields.io/badge/Version-1.4.0-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: 1.4](https://img.shields.io/badge/AppVersion-1.4-informational?style=flat-square) ### Kubecost exporter helm chart for Kubernetes @@ -87,15 +87,16 @@ You should see 200/201s in the logs, which indicates that the exporter is workin |-----|------|---------|-------------| | cronSchedule | string | `"0 */6 * * *"` | Setting up a cronJob scheduler to run an export task at the right time | | filePath | string | `"/var/kubecost"` | Filepath to mount persistent volume | -| fileRotation | bool | `true` | Delete files generated for the previous month | +| fileRotation | bool | `true` | Delete files generated for the previous month (or the month before the previous month if INCLUDE_PREVIOUS_MONTH is set to true) | | flexera.billConnectId | string | `"cbi-oi-kubecost-1"` | Bill Connect ID | | flexera.orgId | string | `""` | Flexera Organization ID | -| flexera.refreshToken | string | `""` | Refresh Token from FlexeraOne | +| flexera.refreshToken | string | `""` | Refresh Token from FlexeraOne You can provide the refresh token in two ways: 1. Directly as a string: refreshToken: "your_token_here" 2. Reference it from a Kubernetes secret: refreshToken: valueFrom: secretKeyRef: name: flexera-secrets # Name of the Kubernetes secret key: refresh_token # Key in the secret containing the refresh token | | flexera.shard | string | `"NAM"` | Shard ("NAM", "EU", "AU") | | image.pullPolicy | string | `"Always"` | | | image.repository | string | `"public.ecr.aws/flexera/cbi-oi-kubecost-exporter"` | | | image.tag | string | `"latest"` | | | imagePullSecrets | list | `[]` | | +| includePreviousMonth | bool | `false` | Include data from previous month to export process | | kubecost.aggregation | string | `"pod"` | Aggregation Level ("namespace", "controller", "pod") | | kubecost.apiPath | string | `"/model/"` | Base path for the Kubecost API endpoints | | kubecost.host | string | `"kubecost-cost-analyzer.kubecost.svc.cluster.local:9090"` | Default kubecost-cost-analyzer service host on the current cluster. For current cluster is serviceName.namespaceName.svc.cluster.local | @@ -108,4 +109,4 @@ You should see 200/201s in the logs, which indicates that the exporter is workin | persistentVolume.size | string | `"1Gi"` | Persistent Volume size | ---------------------------------------------- -Autogenerated from chart metadata using [helm-docs v1.11.0](https://github.com/norwoodj/helm-docs/releases/v1.11.0) +Autogenerated from chart metadata using [helm-docs v1.11.3](https://github.com/norwoodj/helm-docs/releases/v1.11.3) diff --git a/helm-chart/cbi-oi-kubecost-exporter-1.4.0.tgz b/helm-chart/cbi-oi-kubecost-exporter-1.4.0.tgz new file mode 100644 index 0000000..8712f21 Binary files /dev/null and b/helm-chart/cbi-oi-kubecost-exporter-1.4.0.tgz differ diff --git a/helm-chart/templates/cronjob.yaml b/helm-chart/templates/cronjob.yaml index fc49bb7..6fa636c 100644 --- a/helm-chart/templates/cronjob.yaml +++ b/helm-chart/templates/cronjob.yaml @@ -53,6 +53,8 @@ spec: value: "{{ .Values.fileRotation }}" - name: FILE_PATH value: "{{ .Values.filePath }}" + - name: INCLUDE_PREVIOUS_MONTH + value: "{{ .Values.includePreviousMonth }}" volumeMounts: - name: persistent-configs mountPath: {{ .Values.filePath }} diff --git a/helm-chart/values.yaml b/helm-chart/values.yaml index c071a4c..5985e50 100644 --- a/helm-chart/values.yaml +++ b/helm-chart/values.yaml @@ -55,8 +55,11 @@ kubecost: # -- Cost multiplier multiplier: 1.0 -# -- Delete files generated for the previous month +# -- Delete files generated for the previous month (or the month before the previous month if INCLUDE_PREVIOUS_MONTH is set to true) fileRotation: true # -- Filepath to mount persistent volume filePath: "/var/kubecost" + +# -- Include data from previous month to export process +includePreviousMonth: false diff --git a/index.yaml b/index.yaml index 11f08b9..9635a59 100644 --- a/index.yaml +++ b/index.yaml @@ -1,11 +1,21 @@ apiVersion: v1 entries: cbi-oi-kubecost-exporter: + - apiVersion: v2 + appVersion: "1.4" + created: "2023-10-13T11:21:44.396278-06:00" + description: Kubecost exporter helm chart for Kubernetes + digest: 671339fe2b3198825e11bad8203b080e896fc04bcdc4020738db2f121d4133e1 + name: cbi-oi-kubecost-exporter + type: application + urls: + - helm-chart/cbi-oi-kubecost-exporter-1.4.0.tgz + version: 1.4.0 - apiVersion: v2 appVersion: "1.3" - created: "2023-09-14T16:04:16.019889-06:00" + created: "2023-10-13T11:21:44.395125-06:00" description: Kubecost exporter helm chart for Kubernetes - digest: a9c88dd6e4e2f52f0cf00f28e4a4760591287c35647ccb529109a945950019d3 + digest: 0d967927bfab5f4bb6d40ce062710679e98b145c4b3f38709c0bf5d216ad8606 name: cbi-oi-kubecost-exporter type: application urls: @@ -13,7 +23,7 @@ entries: version: 1.3.0 - apiVersion: v2 appVersion: "1.2" - created: "2023-09-14T16:04:16.018246-06:00" + created: "2023-10-13T11:21:44.394041-06:00" description: Kubecost exporter helm chart for Kubernetes digest: d0c724c7bb085a1801e27f60ff748bc69aba14425cfd27fd132f99966692ed80 name: cbi-oi-kubecost-exporter @@ -23,7 +33,7 @@ entries: version: 1.2.0 - apiVersion: v2 appVersion: "1.1" - created: "2023-09-14T16:04:16.017805-06:00" + created: "2023-10-13T11:21:44.393271-06:00" description: Kubecost exporter helm chart for Kubernetes digest: c6f2681575b704b5934efea2357921896d55c4ad5e09692f31ab92b9e614cfea name: cbi-oi-kubecost-exporter @@ -31,4 +41,4 @@ entries: urls: - helm-chart/cbi-oi-kubecost-exporter-1.1.0.tgz version: 1.1.0 -generated: "2023-09-14T16:04:16.0155-06:00" +generated: "2023-10-13T11:21:44.392264-06:00" diff --git a/main.go b/main.go index 7131bd5..a62485a 100644 --- a/main.go +++ b/main.go @@ -104,28 +104,30 @@ type ( } Config struct { - RefreshToken string `env:"REFRESH_TOKEN"` - OrgID string `env:"ORG_ID"` - BillConnectID string `env:"BILL_CONNECT_ID"` - Shard string `env:"SHARD" envDefault:"NAM"` - KubecostHost string `env:"KUBECOST_HOST" envDefault:"localhost:9090"` - KubecostAPIPath string `env:"KUBECOST_API_PATH" envDefault:"/model/"` - Aggregation string `env:"AGGREGATION" envDefault:"pod"` - ShareNamespaces string `env:"SHARE_NAMESPACES" envDefault:"kube-system,cadvisor"` - Idle bool `env:"IDLE" envDefault:"true"` - ShareIdle bool `env:"SHARE_IDLE" envDefault:"false"` - ShareTenancyCosts bool `env:"SHARE_TENANCY_COSTS" envDefault:"true"` - Multiplier float64 `env:"MULTIPLIER" envDefault:"1.0"` - FileRotation bool `env:"FILE_ROTATION" envDefault:"true"` - FilePath string `env:"FILE_PATH" envDefault:"/var/kubecost"` + RefreshToken string `env:"REFRESH_TOKEN"` + OrgID string `env:"ORG_ID"` + BillConnectID string `env:"BILL_CONNECT_ID"` + Shard string `env:"SHARD" envDefault:"NAM"` + KubecostHost string `env:"KUBECOST_HOST" envDefault:"localhost:9090"` + KubecostAPIPath string `env:"KUBECOST_API_PATH" envDefault:"/model/"` + Aggregation string `env:"AGGREGATION" envDefault:"pod"` + ShareNamespaces string `env:"SHARE_NAMESPACES" envDefault:"kube-system,cadvisor"` + Idle bool `env:"IDLE" envDefault:"true"` + ShareIdle bool `env:"SHARE_IDLE" envDefault:"false"` + ShareTenancyCosts bool `env:"SHARE_TENANCY_COSTS" envDefault:"true"` + Multiplier float64 `env:"MULTIPLIER" envDefault:"1.0"` + FileRotation bool `env:"FILE_ROTATION" envDefault:"true"` + FilePath string `env:"FILE_PATH" envDefault:"/var/kubecost"` + IncludePreviousMonth bool `env:"INCLUDE_PREVIOUS_MONTH" envDefault:"false"` } App struct { Config - aggregation string - filesToUpload map[string]struct{} - client *http.Client - invoiceYearMonth string + aggregation string + filesToUpload map[string]map[string]struct{} + client *http.Client + lastInvoiceDate time.Time + invoiceMonths []string } ) @@ -140,6 +142,7 @@ func main() { func (e *App) updateFromKubecost() { now := time.Now().Local() + now = time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location()) err := os.MkdirAll(e.FilePath, os.ModePerm) if err != nil { @@ -151,8 +154,8 @@ func (e *App) updateFromKubecost() { log.Fatal(err) } - for d := range dateIter(now.AddDate(0, -1, 0)) { - if d.After(now) || d.Format("2006-01") != e.invoiceYearMonth { + for d := range dateIter(now.AddDate(0, -(len(e.invoiceMonths)), 0)) { + if d.After(now) || !e.dateInInvoiceRange(d) { continue } @@ -190,6 +193,16 @@ func (e *App) updateFromKubecost() { resp.Body.Close() data := j.Data + monthOfData := d.Format("2006-01") + var csvFile = fmt.Sprintf(path.Join(e.FilePath, "kubecost-%v.csv"), d.Format("2006-01-02")) + + // If the data obtained is empty, skip the iteration, because it might overwrite a previously obtained file for the same range time + _, previousFileCreated := e.filesToUpload[monthOfData][csvFile] + if len(data) == 0 && previousFileCreated { + fmt.Printf("File %s has already been created and kubecost no longer has data for this same date range, skipping", csvFile) + continue + } + b := new(bytes.Buffer) writer := csv.NewWriter(b) @@ -216,8 +229,13 @@ func (e *App) updateFromKubecost() { "EndTime", }) + // Logs to validate date range requested and date range gotten in the data + log.Printf("Requested date range, from %s to %s \n", d.Format("2006-01-02T15:04:05Z"), tomorrow.Format("2006-01-02T15:04:05Z")) + mapDatesGotten := make(map[string]string) + for _, allocation := range data { for id, v := range allocation { + mapDatesGotten[v.Start] = v.End labels := extractLabels(v.Properties.Labels, v.Properties.NamespaceLabels) types := []string{"cpuCost", "gpuCost", "ramCost", "pvCost", "networkCost", "sharedCost", "externalCost", "loadBalancerCost"} vals := []float64{v.CPUCost, v.GPUCost, v.RAMCost, v.PVCost, v.NetworkCost, v.SharedCost, v.ExternalCost, v.LoadBalancerCost} @@ -247,7 +265,7 @@ func (e *App) updateFromKubecost() { v.Properties.ControllerKind, v.Properties.ProviderID, labels, - strings.ReplaceAll(e.invoiceYearMonth, "-", ""), + strings.ReplaceAll(monthOfData, "-", ""), v.Window.Start, v.Start, v.End, @@ -255,10 +273,11 @@ func (e *App) updateFromKubecost() { } } } + log.Printf("Gotten dates range: %v \n", mapDatesGotten) writer.Flush() - var csvFile = fmt.Sprintf(path.Join(e.FilePath, "kubecost-%v.csv"), d.Format("2006-01-02")) - e.filesToUpload[csvFile] = struct{}{} + + e.filesToUpload[monthOfData][csvFile] = struct{}{} err = os.WriteFile(csvFile, b.Bytes(), 0644) if err != nil { log.Fatal(err) @@ -282,59 +301,61 @@ func (a *App) uploadToFlexera() { billUploadURL := fmt.Sprintf("https://%s/optima/orgs/%s/billUploads", shardDict[a.Shard], a.OrgID) - authHeaders := map[string]string{"Authorization": "Bearer " + accessToken} - billUpload := map[string]string{"billConnectId": a.BillConnectID, "billingPeriod": a.invoiceYearMonth} - - billUploadJSON, _ := json.Marshal(billUpload) - response := a.doPost(billUploadURL, string(billUploadJSON), authHeaders) - existingID := "" - - switch response.StatusCode { - case 429: - time.Sleep(120 * time.Second) - response = a.doPost(billUploadURL, string(billUploadJSON), authHeaders) - checkForError(response) - case 409: - bodyBytes, err := io.ReadAll(response.Body) - if err != nil { - log.Fatal(err) - } - uuidMatch := uuidPattern.FindStringSubmatch(string(bodyBytes)) - if len(uuidMatch) < 2 { - log.Fatal("billUpload ID not found") + for month, files := range a.filesToUpload { + authHeaders := map[string]string{"Authorization": "Bearer " + accessToken} + billUpload := map[string]string{"billConnectId": a.BillConnectID, "billingPeriod": month} + + billUploadJSON, _ := json.Marshal(billUpload) + response := a.doPost(billUploadURL, string(billUploadJSON), authHeaders) + existingID := "" + + switch response.StatusCode { + case 429: + time.Sleep(120 * time.Second) + response = a.doPost(billUploadURL, string(billUploadJSON), authHeaders) + checkForError(response) + case 409: + bodyBytes, err := io.ReadAll(response.Body) + if err != nil { + log.Fatal(err) + } + uuidMatch := uuidPattern.FindStringSubmatch(string(bodyBytes)) + if len(uuidMatch) < 2 { + log.Fatal("billUpload ID not found") + } + existingID = uuidMatch[1] + default: + checkForError(response) } - existingID = uuidMatch[1] - default: - checkForError(response) - } - var billUploadID string - if existingID != "" { - billUploadID = existingID - } else { - bodyBytes, err := io.ReadAll(response.Body) - if err != nil { - log.Fatal(err) - } - var jsonResponse map[string]interface{} - if err = json.Unmarshal(bodyBytes, &jsonResponse); err != nil { - log.Fatal(err) + var billUploadID string + if existingID != "" { + billUploadID = existingID + } else { + bodyBytes, err := io.ReadAll(response.Body) + if err != nil { + log.Fatal(err) + } + var jsonResponse map[string]interface{} + if err = json.Unmarshal(bodyBytes, &jsonResponse); err != nil { + log.Fatal(err) + } + billUploadID = jsonResponse["id"].(string) } - billUploadID = jsonResponse["id"].(string) - } - for fileName := range a.filesToUpload { - baseName := filepath.Base(fileName) - uploadFileURL := fmt.Sprintf("%s/%s/files/%s", billUploadURL, billUploadID, baseName) + for fileName := range files { + baseName := filepath.Base(fileName) + uploadFileURL := fmt.Sprintf("%s/%s/files/%s", billUploadURL, billUploadID, baseName) - fileData, _ := os.ReadFile(fileName) - response = a.doPost(uploadFileURL, string(fileData), authHeaders) + fileData, _ := os.ReadFile(fileName) + response = a.doPost(uploadFileURL, string(fileData), authHeaders) + checkForError(response) + } + + operationsURL := fmt.Sprintf("%s/%s/operations", billUploadURL, billUploadID) + response = a.doPost(operationsURL, `{"operation":"commit"}`, authHeaders) checkForError(response) } - - operationsURL := fmt.Sprintf("%s/%s/operations", billUploadURL, billUploadID) - response = a.doPost(operationsURL, `{"operation":"commit"}`, authHeaders) - checkForError(response) } func (a *App) doPost(url, data string, headers map[string]string) *http.Response { @@ -402,8 +423,6 @@ func (a *App) generateAccessToken() (string, error) { // update file list and remove old files func (a *App) updateFileList() { - now := time.Now().Local() - lastInvoiceDate := now.AddDate(0, 0, -1) files, err := os.ReadDir(a.FilePath) if err != nil { log.Fatal(err) @@ -412,9 +431,9 @@ func (a *App) updateFileList() { for _, file := range files { if file.Type().IsRegular() { if t, err := time.Parse("kubecost-2006-01-02.csv", file.Name()); err == nil { - if t.Month() == lastInvoiceDate.Month() { - a.filesToUpload[path.Join(a.FilePath, file.Name())] = struct{}{} - } else if a.FileRotation && t.Month() != now.Month() { + if a.dateInInvoiceRange(t) { + a.filesToUpload[t.Format("2006-01")][path.Join(a.FilePath, file.Name())] = struct{}{} + } else if a.FileRotation { if err = os.Remove(path.Join(a.FilePath, file.Name())); err != nil { log.Printf("error removing file %s: %v", file.Name(), err) } @@ -451,16 +470,35 @@ func (a *App) getCurrency() (string, error) { return config.Data.CurrencyCode, nil } +func (a *App) dateInInvoiceRange(date time.Time) bool { + for _, month := range a.invoiceMonths { + if date.Format("2006-01") == month { + return true + } + } + return false +} + func newApp() *App { + lastInvoiceDate := time.Now().Local().AddDate(0, 0, -1) a := App{ - filesToUpload: make(map[string]struct{}), - client: &http.Client{Timeout: 5 * time.Minute}, - invoiceYearMonth: time.Now().Local().AddDate(0, 0, -1).Format("2006-01"), + filesToUpload: make(map[string]map[string]struct{}), + client: &http.Client{Timeout: 5 * time.Minute}, + lastInvoiceDate: lastInvoiceDate, } if err := env.Parse(&a.Config); err != nil { log.Fatal(err) } + a.invoiceMonths = []string{lastInvoiceDate.Format("2006-01")} + if a.IncludePreviousMonth { + a.invoiceMonths = append(a.invoiceMonths, a.lastInvoiceDate.AddDate(0, -1, 0).Format("2006-01")) + } + + for _, month := range a.invoiceMonths { + a.filesToUpload[month] = make(map[string]struct{}) + } + a.Aggregation = strings.ToLower(a.Aggregation) switch a.Aggregation { @@ -497,7 +535,7 @@ func dateIter(startDate time.Time) <-chan time.Time { go func() { defer close(c) - for !time.Now().Before(startDate) { + for !time.Now().Local().Before(startDate) { c <- startDate startDate = startDate.AddDate(0, 0, 1) } diff --git a/main_test.go b/main_test.go index 285d5f3..a1f99e4 100644 --- a/main_test.go +++ b/main_test.go @@ -82,6 +82,7 @@ func Test_newApp(t *testing.T) { os.Setenv("MULTIPLIER", "1") os.Setenv("FILE_ROTATION", "true") os.Setenv("FILE_PATH", "/var/kubecost") + os.Setenv("KUBECOST_API_PATH", "/model/") defer func() { os.Unsetenv("REFRESH_TOKEN") @@ -97,6 +98,7 @@ func Test_newApp(t *testing.T) { os.Unsetenv("MULTIPLIER") os.Unsetenv("FILE_ROTATION") os.Unsetenv("FILE_PATH") + os.Unsetenv("KUBECOST_API_PATH") }() a := newApp() @@ -107,9 +109,6 @@ func Test_newApp(t *testing.T) { if a.client == nil { t.Error("client is not initialized") } - if a.invoiceYearMonth == "" { - t.Error("invoiceYearMonth is not initialized") - } if a.aggregation == "" { t.Error("aggregation is not initialized") } @@ -133,8 +132,57 @@ func Test_newApp(t *testing.T) { Multiplier: 1.0, FileRotation: true, FilePath: "/var/kubecost", + KubecostAPIPath: "/model/", } if !reflect.DeepEqual(a.Config, expectedConfig) { t.Errorf("Config is %+v, expected %+v", a.Config, expectedConfig) } } + +func TestApp_dateInInvoiceRange(t *testing.T) { + type args struct { + includePreviousMonth string + date time.Time + } + tests := []struct { + name string + args args + want bool + }{ + { + name: "success: date in range", + args: args{ + includePreviousMonth: "false", + date: time.Now().Local().AddDate(0, 0, -1), + }, + want: true, + }, + { + name: "success: date in range using previous month env var as true", + args: args{ + includePreviousMonth: "true", + date: time.Now().Local().AddDate(0, -1, 0), + }, + want: true, + }, + { + name: "fail: date out of range using previous month env var as false", + args: args{ + includePreviousMonth: "false", + date: time.Now().Local().AddDate(0, -1, 0), + }, + want: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + os.Setenv("INCLUDE_PREVIOUS_MONTH", tt.args.includePreviousMonth) + a := newApp() + + if got := a.dateInInvoiceRange(tt.args.date); got != tt.want { + t.Errorf("dateInInvoiceRange() = %v, want %v", got, tt.want) + } + os.Unsetenv("INCLUDE_PREVIOUS_MONTH") + }) + } +}