storagebillingparser.go 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186
  1. package azure
  2. import (
  3. "bytes"
  4. "context"
  5. "encoding/csv"
  6. "fmt"
  7. "io"
  8. "strings"
  9. "time"
  10. "github.com/Azure/azure-sdk-for-go/sdk/storage/azblob"
  11. "github.com/Azure/azure-sdk-for-go/sdk/storage/azblob/container"
  12. "github.com/opencost/opencost/pkg/cloud"
  13. "github.com/opencost/opencost/pkg/log"
  14. )
  15. // AzureStorageBillingParser accesses billing data stored in CSV files in Azure Storage
  16. type AzureStorageBillingParser struct {
  17. StorageConnection
  18. }
  19. func (asbp *AzureStorageBillingParser) Equals(config cloud.Config) bool {
  20. thatConfig, ok := config.(*AzureStorageBillingParser)
  21. if !ok {
  22. return false
  23. }
  24. return asbp.StorageConnection.Equals(&thatConfig.StorageConnection)
  25. }
  26. type AzureBillingResultFunc func(*BillingRowValues) error
  27. func (asbp *AzureStorageBillingParser) ParseBillingData(start, end time.Time, resultFn AzureBillingResultFunc) error {
  28. err := asbp.Validate()
  29. if err != nil {
  30. asbp.ConnectionStatus = cloud.InvalidConfiguration
  31. return err
  32. }
  33. serviceURL := fmt.Sprintf(asbp.StorageConnection.getBlobURLTemplate(), asbp.Account, "")
  34. client, err := asbp.Authorizer.GetBlobClient(serviceURL)
  35. if err != nil {
  36. asbp.ConnectionStatus = cloud.FailedConnection
  37. return err
  38. }
  39. ctx := context.Background()
  40. blobNames, err := asbp.getMostRecentBlobs(start, end, client, ctx)
  41. if err != nil {
  42. asbp.ConnectionStatus = cloud.FailedConnection
  43. return err
  44. }
  45. if len(blobNames) == 0 && asbp.ConnectionStatus != cloud.SuccessfulConnection {
  46. asbp.ConnectionStatus = cloud.MissingData
  47. return nil
  48. }
  49. for _, blobName := range blobNames {
  50. blobBytes, err2 := asbp.DownloadBlob(blobName, client, ctx)
  51. if err2 != nil {
  52. asbp.ConnectionStatus = cloud.FailedConnection
  53. return err2
  54. }
  55. err2 = asbp.parseCSV(start, end, csv.NewReader(bytes.NewReader(blobBytes)), resultFn)
  56. if err2 != nil {
  57. asbp.ConnectionStatus = cloud.ParseError
  58. return err2
  59. }
  60. }
  61. asbp.ConnectionStatus = cloud.SuccessfulConnection
  62. return nil
  63. }
  64. func (asbp *AzureStorageBillingParser) parseCSV(start, end time.Time, reader *csv.Reader, resultFn AzureBillingResultFunc) error {
  65. headers, err := reader.Read()
  66. if err != nil {
  67. return err
  68. }
  69. abp, err := NewBillingParseSchema(headers)
  70. if err != nil {
  71. return err
  72. }
  73. for {
  74. var record, err = reader.Read()
  75. if err == io.EOF {
  76. break
  77. }
  78. if err != nil {
  79. return err
  80. }
  81. abv := abp.ParseRow(start, end, record)
  82. if abv == nil {
  83. continue
  84. }
  85. err = resultFn(abv)
  86. if err != nil {
  87. return err
  88. }
  89. }
  90. return nil
  91. }
  92. func (asbp *AzureStorageBillingParser) getMostRecentBlobs(start, end time.Time, client *azblob.Client, ctx context.Context) ([]string, error) {
  93. log.Infof("Azure Storage: retrieving most recent reports from: %v - %v", start, end)
  94. // Get list of month substrings for months contained in the start to end range
  95. monthStrs, err := asbp.getMonthStrings(start, end)
  96. if err != nil {
  97. return nil, err
  98. }
  99. mostResentBlobs := make(map[string]container.BlobItem)
  100. pager := client.NewListBlobsFlatPager(asbp.Container, &azblob.ListBlobsFlatOptions{
  101. Include: container.ListBlobsInclude{Deleted: false, Versions: false},
  102. })
  103. for pager.More() {
  104. resp, err := pager.NextPage(ctx)
  105. if err != nil {
  106. return nil, err
  107. }
  108. // Using the list of months strings find the most resent blob for each month in the range
  109. for _, blobInfo := range resp.Segment.BlobItems {
  110. if blobInfo.Name == nil {
  111. continue
  112. }
  113. // If Container Path configuration exists, check if it is in the blobs name
  114. if asbp.Path != "" && !strings.Contains(*blobInfo.Name, asbp.Path) {
  115. continue
  116. }
  117. for _, month := range monthStrs {
  118. if strings.Contains(*blobInfo.Name, month) {
  119. // check if blob is the newest seen for this month
  120. if prevBlob, ok := mostResentBlobs[month]; ok {
  121. if prevBlob.Properties.CreationTime.After(*blobInfo.Properties.CreationTime) {
  122. continue
  123. }
  124. }
  125. mostResentBlobs[month] = *blobInfo
  126. }
  127. }
  128. }
  129. }
  130. // convert blob names into blob urls and move from map into ordered list of blob names
  131. var blobNames []string
  132. for _, month := range monthStrs {
  133. if blob, ok := mostResentBlobs[month]; ok {
  134. blobNames = append(blobNames, *blob.Name)
  135. }
  136. }
  137. return blobNames, nil
  138. }
  139. func (asbp *AzureStorageBillingParser) getMonthStrings(start, end time.Time) ([]string, error) {
  140. if start.After(end) {
  141. return []string{}, fmt.Errorf("start date must be before end date")
  142. }
  143. if end.After(time.Now()) {
  144. end = time.Now()
  145. }
  146. var monthStrs []string
  147. monthStr := asbp.timeToMonthString(start)
  148. endStr := asbp.timeToMonthString(end)
  149. monthStrs = append(monthStrs, monthStr)
  150. currMonth := start.AddDate(0, 0, -start.Day()+1)
  151. for monthStr != endStr {
  152. currMonth = currMonth.AddDate(0, 1, 0)
  153. monthStr = asbp.timeToMonthString(currMonth)
  154. monthStrs = append(monthStrs, monthStr)
  155. }
  156. return monthStrs, nil
  157. }
  158. func (asbp *AzureStorageBillingParser) timeToMonthString(input time.Time) string {
  159. format := "20060102"
  160. startOfMonth := input.AddDate(0, 0, -input.Day()+1)
  161. endOfMonth := input.AddDate(0, 1, -input.Day())
  162. return startOfMonth.Format(format) + "-" + endOfMonth.Format(format)
  163. }