ratecard.go 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273
  1. package oracle
  2. import (
  3. "encoding/json"
  4. "fmt"
  5. "io"
  6. "net/http"
  7. "strconv"
  8. "strings"
  9. "github.com/opencost/opencost/pkg/cloud/models"
  10. )
  11. // 1 month = 744 hours by price list documentation.
  12. const hoursPerMonth = 24 * 31
  13. type RateCardStore struct {
  14. shapesEndpoint string
  15. url string
  16. currencyCode string
  17. client *http.Client
  18. prices map[string]Price
  19. }
  20. type Price struct {
  21. ProductName string
  22. Metric string
  23. Model string
  24. UnitPrice float64
  25. }
  26. type PricingResponse struct {
  27. Items []Item `json:"items"`
  28. }
  29. type Item struct {
  30. PartNumber string `json:"partNumber"`
  31. DisplayName string `json:"displayName"`
  32. MetricName string `json:"metricName"`
  33. ServiceCategory string `json:"serviceCategory"`
  34. CurrencyCodeLocalizations []struct {
  35. CurrencyCode string `json:"currencyCode"`
  36. Prices []struct {
  37. Model string `json:"model"`
  38. Value float64 `json:"value"`
  39. } `json:"prices"`
  40. } `json:"currencyCodeLocalizations"`
  41. Description string `json:"description,omitempty"`
  42. }
  43. func NewRateCardStore(url, currencyCode string) *RateCardStore {
  44. return &RateCardStore{
  45. url: url,
  46. currencyCode: currencyCode,
  47. client: &http.Client{},
  48. prices: map[string]Price{},
  49. }
  50. }
  51. func (rcs *RateCardStore) ForLB(defaultPricing DefaultPricing) (*models.LoadBalancer, error) {
  52. var cost float64
  53. rc, ok := rcs.prices[loadBalancerPartNumber]
  54. if ok {
  55. cost = rc.UnitPrice
  56. } else {
  57. c, err := strconv.ParseFloat(defaultPricing.LB, 64)
  58. if err != nil {
  59. return nil, err
  60. }
  61. cost = c
  62. }
  63. return &models.LoadBalancer{
  64. Cost: cost,
  65. }, nil
  66. }
  67. func (rcs *RateCardStore) ForManagedCluster(clusterType string) float64 {
  68. // Basic clusters are free, and do not have a rate card.
  69. if clusterType == "BASIC_CLUSTER" {
  70. return 0.0
  71. }
  72. // Enhanced clusters have a rate card, and require a pricing look up.
  73. rc, ok := rcs.prices[enhancedClusterPartNumber]
  74. if !ok {
  75. return 0.1
  76. }
  77. return rc.UnitPrice
  78. }
  79. func (rcs *RateCardStore) ForEgressRegion(region string, defaultPricing DefaultPricing) (*models.Network, error) {
  80. pn := egressRegionPartNumber(region)
  81. var egressCost float64
  82. if rc, ok := rcs.prices[pn]; ok {
  83. egressCost = rc.UnitPrice
  84. } else if defaultPricing.Egress != "" {
  85. cost, err := strconv.ParseFloat(defaultPricing.Egress, 64)
  86. if err != nil {
  87. return nil, err
  88. }
  89. egressCost = cost
  90. }
  91. return &models.Network{
  92. ZoneNetworkEgressCost: 0,
  93. RegionNetworkEgressCost: egressCost,
  94. InternetNetworkEgressCost: egressCost,
  95. NatGatewayEgressCost: 0,
  96. NatGatewayIngressCost: 0,
  97. }, nil
  98. }
  99. // ForPVK retrieves a Gb/Hour cost for a given PVKey.
  100. func (rcs *RateCardStore) ForPVK(pvk models.PVKey, defaultPricing DefaultPricing) (*models.PV, error) {
  101. features := pvk.Features()
  102. rc, ok := rcs.prices[features]
  103. if !ok {
  104. // Use default storage if no pricing found
  105. return &models.PV{
  106. Cost: defaultPricing.Storage,
  107. }, nil
  108. }
  109. return &models.PV{
  110. Cost: fmt.Sprintf("%f", rc.UnitPrice/hoursPerMonth), // Oracle unit pricing for storage is in Gb/Month
  111. }, nil
  112. }
  113. // ForKey retrieves costing metadata for a key.
  114. func (rcs *RateCardStore) ForKey(key models.Key, defaultPricing DefaultPricing) (*models.Node, models.PricingMetadata, error) {
  115. features := strings.Split(key.Features(), ",")
  116. product := instanceProducts.get(features[0])
  117. var node *models.Node
  118. // Use the default pricing if the instance product is unknown
  119. if product.isEmpty() {
  120. totalCost, err := defaultPricing.TotalInstanceCost()
  121. if err != nil {
  122. return nil, models.PricingMetadata{}, fmt.Errorf("failed to parse default Oracle pricing: %w", err)
  123. }
  124. vcpuCost := defaultPricing.OCPU
  125. if !isARMArch(features) {
  126. // Non-ARM architectures have 2 VCPU per OCPU
  127. vcpuFloat, err := strconv.ParseFloat(vcpuCost, 64)
  128. if err != nil {
  129. return nil, models.PricingMetadata{}, err
  130. }
  131. vcpuFloat /= 2
  132. vcpuCost = fmt.Sprintf("%f", vcpuFloat)
  133. }
  134. node = &models.Node{
  135. Cost: fmt.Sprintf("%f", totalCost),
  136. VCPUCost: vcpuCost,
  137. RAM: defaultPricing.Memory,
  138. GPU: defaultPricing.GPU,
  139. }
  140. } else {
  141. ocpuPrice := rcs.prices[product.OCPU].UnitPrice
  142. if !isARMArch(features) {
  143. // Non-ARM architectures have 2 VCPU per OCPU
  144. ocpuPrice /= 2
  145. }
  146. memoryPrice := rcs.prices[product.Memory].UnitPrice
  147. gpuPrice := rcs.prices[product.GPU].UnitPrice
  148. diskPrice := rcs.prices[product.Disk].UnitPrice
  149. // convert disk price from Tb/hour to Gb/hour
  150. diskPrice /= 1000
  151. totalPrice := diskPrice + ocpuPrice + memoryPrice + gpuPrice
  152. // Add virtual node pricing if it is being used.
  153. if len(features) > 1 && features[1] == "true" {
  154. totalPrice += rcs.prices[virualNodePartNumber].UnitPrice
  155. }
  156. node = &models.Node{
  157. Cost: fmt.Sprintf("%f", totalPrice),
  158. StorageCost: fmt.Sprintf("%f", diskPrice),
  159. VCPUCost: fmt.Sprintf("%f", ocpuPrice),
  160. RAMCost: fmt.Sprintf("%f", memoryPrice),
  161. GPUCost: fmt.Sprintf("%f", gpuPrice),
  162. }
  163. }
  164. return node, models.PricingMetadata{}, nil
  165. }
  166. func (rcs *RateCardStore) Store() map[string]Price {
  167. return rcs.prices
  168. }
  169. func (rcs *RateCardStore) Refresh() (map[string]Price, error) {
  170. if err := rcs.refresh(); err != nil {
  171. return nil, err
  172. }
  173. return rcs.prices, nil
  174. }
  175. // refresh the prices of Price information
  176. func (rcs *RateCardStore) refresh() error {
  177. rcr, err := rcs.getProductPricing()
  178. if err != nil {
  179. return err
  180. }
  181. rcs.prices = rcr.toStore()
  182. return nil
  183. }
  184. func (rcs *RateCardStore) getProductPricing() (*PricingResponse, error) {
  185. url := fmt.Sprintf("%s?currencyCode=%s", rcs.url, rcs.currencyCode)
  186. prBytes, err := rcs.loadMetadata(url)
  187. if err != nil {
  188. return nil, err
  189. }
  190. pr := &PricingResponse{}
  191. if err := json.Unmarshal(prBytes, pr); err != nil {
  192. return nil, err
  193. }
  194. return pr, nil
  195. }
  196. func (rcs *RateCardStore) loadMetadata(url string) ([]byte, error) {
  197. req, err := http.NewRequest("GET", url, nil)
  198. if err != nil {
  199. return nil, err
  200. }
  201. resp, err := rcs.client.Do(req)
  202. if err != nil {
  203. return nil, err
  204. }
  205. defer resp.Body.Close()
  206. if resp.StatusCode != http.StatusOK {
  207. return nil, fmt.Errorf("failed to get pricing metadata data, got code %d", resp.StatusCode)
  208. }
  209. return io.ReadAll(resp.Body)
  210. }
  211. func (rcr *PricingResponse) toStore() map[string]Price {
  212. store := map[string]Price{}
  213. for _, item := range rcr.Items {
  214. store[item.PartNumber] = item.toRateCard()
  215. }
  216. return store
  217. }
  218. func (i Item) hasPrice() bool {
  219. if len(i.CurrencyCodeLocalizations) < 1 {
  220. return false
  221. }
  222. return len(i.CurrencyCodeLocalizations[0].Prices) > 0
  223. }
  224. func (i Item) toRateCard() Price {
  225. var unitPrice float64
  226. var model string
  227. if i.hasPrice() {
  228. for _, price := range i.CurrencyCodeLocalizations[0].Prices {
  229. // Some products have range pricing, we'll take the first non-zero pricing in this case.
  230. if price.Value > 0 {
  231. unitPrice = price.Value
  232. model = price.Model
  233. }
  234. }
  235. }
  236. return Price{
  237. ProductName: i.DisplayName,
  238. Metric: i.MetricName,
  239. UnitPrice: unitPrice,
  240. Model: model,
  241. }
  242. }
  243. func (p Product) isEmpty() bool {
  244. return p.GPU == "" && p.OCPU == "" && p.Memory == "" && p.Disk == ""
  245. }
  246. func isARMArch(features []string) bool {
  247. if len(features) < 3 {
  248. return false
  249. }
  250. return strings.HasPrefix(strings.ToLower(features[2]), "arm")
  251. }