cloudcostprops.go 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225
  1. package kubecost
  2. import (
  3. "strings"
  4. "github.com/opencost/opencost/pkg/log"
  5. )
  6. const (
  7. CloudCostInvoiceEntityIDProp string = "invoiceEntityID"
  8. CloudCostAccountIDProp string = "accountID"
  9. CloudCostProviderProp string = "provider"
  10. CloudCostProviderIDProp string = "providerID"
  11. CloudCostCategoryProp string = "category"
  12. CloudCostServiceProp string = "service"
  13. CloudCostLabelProp string = "label"
  14. )
  15. const (
  16. // CloudCostClusterManagementCategory describes CloudCost representing Hosted Kubernetes Fees
  17. CloudCostClusterManagementCategory string = "Cluster Management"
  18. // CloudCostDiskCategory describes CloudCost representing Disk usage
  19. CloudCostDiskCategory string = "Disk"
  20. // CloudCostLoadBalancerCategory describes CloudCost representing Load Balancer usage
  21. CloudCostLoadBalancerCategory string = "Load Balancer"
  22. // CloudCostNetworkCategory describes CloudCost representing Network usage
  23. CloudCostNetworkCategory string = "Network"
  24. // CloudCostVirtualMachineCategory describes CloudCost representing VM usage
  25. CloudCostVirtualMachineCategory string = "Virtual Machine"
  26. // CloudCostOtherCategory describes CloudCost that do not belong to a defined category
  27. CloudCostOtherCategory string = "Other"
  28. )
  29. type CloudCostLabels map[string]string
  30. func (ccl CloudCostLabels) Clone() CloudCostLabels {
  31. result := make(map[string]string, len(ccl))
  32. for k, v := range ccl {
  33. result[k] = v
  34. }
  35. return result
  36. }
  37. func (ccl CloudCostLabels) Equal(that CloudCostLabels) bool {
  38. if len(ccl) != len(that) {
  39. return false
  40. }
  41. // Maps are of equal length, so if all keys are in both maps, we don't
  42. // have to check the keys of the other map.
  43. for k, val := range ccl {
  44. if thatVal, ok := that[k]; !ok || val != thatVal {
  45. return false
  46. }
  47. }
  48. return true
  49. }
  50. // Intersection returns the set of labels that have the same key and value in the receiver and arg
  51. func (ccl CloudCostLabels) Intersection(that CloudCostLabels) CloudCostLabels {
  52. intersection := make(map[string]string)
  53. if len(ccl) == 0 || len(that) == 0 {
  54. return intersection
  55. }
  56. // Pick the smaller of the two label sets
  57. smallerLabels := ccl
  58. largerLabels := that
  59. if len(ccl) > len(that) {
  60. smallerLabels = that
  61. largerLabels = ccl
  62. }
  63. // Loop through the smaller label set
  64. for k, sVal := range smallerLabels {
  65. if lVal, ok := largerLabels[k]; ok && sVal == lVal {
  66. intersection[k] = sVal
  67. }
  68. }
  69. return intersection
  70. }
  71. type CloudCostProperties struct {
  72. ProviderID string `json:"providerID,omitempty"`
  73. Provider string `json:"provider,omitempty"`
  74. AccountID string `json:"accountID,omitempty"`
  75. InvoiceEntityID string `json:"invoiceEntityID,omitempty"`
  76. Service string `json:"service,omitempty"`
  77. Category string `json:"category,omitempty"`
  78. Labels CloudCostLabels `json:"labels,omitempty"`
  79. }
  80. func (ccp *CloudCostProperties) Equal(that *CloudCostProperties) bool {
  81. return ccp.ProviderID == that.ProviderID &&
  82. ccp.Provider == that.Provider &&
  83. ccp.AccountID == that.AccountID &&
  84. ccp.InvoiceEntityID == that.InvoiceEntityID &&
  85. ccp.Service == that.Service &&
  86. ccp.Category == that.Category &&
  87. ccp.Labels.Equal(that.Labels)
  88. }
  89. func (ccp *CloudCostProperties) Clone() *CloudCostProperties {
  90. return &CloudCostProperties{
  91. ProviderID: ccp.ProviderID,
  92. Provider: ccp.Provider,
  93. AccountID: ccp.AccountID,
  94. InvoiceEntityID: ccp.InvoiceEntityID,
  95. Service: ccp.Service,
  96. Category: ccp.Category,
  97. Labels: ccp.Labels.Clone(),
  98. }
  99. }
  100. // Intersection ensure the values of two CloudCostAggregateProperties are maintain only if they are equal
  101. func (ccp *CloudCostProperties) Intersection(that *CloudCostProperties) *CloudCostProperties {
  102. if ccp == nil || that == nil {
  103. return nil
  104. }
  105. if ccp.Equal(that) {
  106. return ccp
  107. }
  108. intersectionCCP := &CloudCostProperties{}
  109. if ccp.Equal(intersectionCCP) || that.Equal(intersectionCCP) {
  110. return intersectionCCP
  111. }
  112. if ccp.Provider == that.Provider {
  113. intersectionCCP.Provider = ccp.Provider
  114. }
  115. if ccp.ProviderID == that.ProviderID {
  116. intersectionCCP.ProviderID = ccp.ProviderID
  117. }
  118. if ccp.AccountID == that.AccountID {
  119. intersectionCCP.AccountID = ccp.AccountID
  120. }
  121. if ccp.InvoiceEntityID == that.InvoiceEntityID {
  122. intersectionCCP.InvoiceEntityID = ccp.InvoiceEntityID
  123. }
  124. if ccp.Service == that.Service {
  125. intersectionCCP.Service = ccp.Service
  126. }
  127. if ccp.Category == that.Category {
  128. intersectionCCP.Category = ccp.Category
  129. }
  130. intersectionCCP.Labels = ccp.Labels.Intersection(that.Labels)
  131. return intersectionCCP
  132. }
  133. var cloudCostDefaultKeyProperties = []string{
  134. CloudCostProviderProp,
  135. CloudCostInvoiceEntityIDProp,
  136. CloudCostAccountIDProp,
  137. CloudCostCategoryProp,
  138. CloudCostServiceProp,
  139. CloudCostProviderIDProp,
  140. }
  141. // GenerateKey takes a list of properties and creates a "/" seperated key based on the values of the requested properties.
  142. // Invalid values are ignored with a warning. A nil input returns the default key, while an empty slice returns the empty string
  143. func (ccp *CloudCostProperties) GenerateKey(props []string) string {
  144. // nil props replaced with default property list
  145. if props == nil {
  146. props = cloudCostDefaultKeyProperties
  147. }
  148. values := make([]string, len(props))
  149. for i, prop := range props {
  150. propVal := UnallocatedSuffix
  151. switch true {
  152. case prop == CloudCostProviderProp:
  153. if ccp.Provider != "" {
  154. propVal = ccp.Provider
  155. }
  156. case prop == CloudCostProviderIDProp:
  157. if ccp.ProviderID != "" {
  158. propVal = ccp.ProviderID
  159. }
  160. case prop == CloudCostCategoryProp:
  161. if ccp.Category != "" {
  162. propVal = ccp.Category
  163. }
  164. case prop == CloudCostInvoiceEntityIDProp:
  165. if ccp.InvoiceEntityID != "" {
  166. propVal = ccp.InvoiceEntityID
  167. }
  168. case prop == CloudCostAccountIDProp:
  169. if ccp.AccountID != "" {
  170. propVal = ccp.AccountID
  171. }
  172. case prop == CloudCostServiceProp:
  173. if ccp.Service != "" {
  174. propVal = ccp.Service
  175. }
  176. case strings.HasPrefix(prop, "label:"):
  177. labels := ccp.Labels
  178. if labels != nil {
  179. labelName := strings.TrimPrefix(prop, "label:")
  180. if labelValue, ok := labels[labelName]; ok && labelValue != "" {
  181. propVal = labelValue
  182. }
  183. }
  184. default:
  185. // This case should never be reached, as input up until this point
  186. // should be checked and rejected if invalid. But if we do get a
  187. // value we don't recognize, log a warning.
  188. log.Warnf("CloudCost: GenerateKey: illegal aggregation parameter: %s", prop)
  189. }
  190. values[i] = propVal
  191. }
  192. return strings.Join(values, "/")
  193. }