2
0

module.go 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439
  1. package basic
  2. import (
  3. "context"
  4. "errors"
  5. "fmt"
  6. "slices"
  7. "sync"
  8. "github.com/opencost/opencost/core/pkg/pricing"
  9. "github.com/opencost/opencost/core/pkg/reader"
  10. "github.com/opencost/opencost/core/pkg/unit"
  11. )
  12. // PricingModule must satisfy the pricing.PricingModule interface
  13. var _ pricing.PricingModule = (*PricingModule)(nil)
  14. type PricingModule struct {
  15. mu sync.RWMutex
  16. store pricing.PricingStore
  17. }
  18. func NewBasicPricingModule(store pricing.PricingStore) (*PricingModule, error) {
  19. pricingSet, err := store.GetPricingSet(context.Background())
  20. if err != nil {
  21. return nil, fmt.Errorf("checking pricing store: %w", err)
  22. }
  23. if pricingSet.IsEmpty() {
  24. // Populate store with a default pricing set.
  25. err := store.SetPricingSet(context.Background(), GetDefaultPricingSet())
  26. if err != nil {
  27. return nil, fmt.Errorf("setting default pricing: %w", err)
  28. }
  29. pricingSet, err = store.GetPricingSet(context.Background())
  30. if err != nil {
  31. return nil, fmt.Errorf("checking default pricing: %w", err)
  32. }
  33. if pricingSet.IsEmpty() {
  34. return nil, errors.New("unable to initialize store")
  35. }
  36. }
  37. pm := &PricingModule{
  38. store: store,
  39. }
  40. return pm, nil
  41. }
  42. func (pm *PricingModule) GetClusterPricing(ctx context.Context, props pricing.ClusterPricingProperties) (*pricing.ClusterPricing, error) {
  43. pm.mu.RLock()
  44. defer pm.mu.RUnlock()
  45. cp, err := pm.getClusterPricing(ctx)
  46. if err != nil {
  47. return nil, err
  48. }
  49. if cp != nil {
  50. return cp, nil
  51. }
  52. return nil, errors.New("no cluster pricing")
  53. }
  54. func (pm *PricingModule) NewClusterPricingReader(ctx context.Context) (reader.Reader[*pricing.ClusterPricing], error) {
  55. pm.mu.RLock()
  56. defer pm.mu.RUnlock()
  57. cp, err := pm.getClusterPricing(ctx)
  58. if err != nil {
  59. return nil, fmt.Errorf("getting node pricing: %w", err)
  60. }
  61. return reader.NewSliceReader([]*pricing.ClusterPricing{cp}), nil
  62. }
  63. func (pm *PricingModule) GetNetworkPricing(ctx context.Context, props pricing.NetworkPricingProperties) (*pricing.NetworkPricing, error) {
  64. pm.mu.RLock()
  65. defer pm.mu.RUnlock()
  66. nps, err := pm.getNetworkPricing(ctx)
  67. if err != nil {
  68. return nil, err
  69. }
  70. // Search through the mock data for a matching network pricing entry
  71. for _, np := range nps {
  72. if np.Properties.Provider == props.Provider &&
  73. np.Properties.TrafficDirection == props.TrafficDirection &&
  74. np.Properties.TrafficType == props.TrafficType &&
  75. np.Properties.IsNatGateway == props.IsNatGateway {
  76. return np, nil
  77. }
  78. }
  79. return nil, fmt.Errorf("network pricing not found for provider=%s, trafficDirection=%s, trafficType=%s, isNatGateway=%t",
  80. props.Provider, props.TrafficDirection, props.TrafficType, props.IsNatGateway)
  81. }
  82. func (pm *PricingModule) NewNetworkPricingReader(ctx context.Context) (reader.Reader[*pricing.NetworkPricing], error) {
  83. pm.mu.RLock()
  84. defer pm.mu.RUnlock()
  85. np, err := pm.getNetworkPricing(ctx)
  86. if err != nil {
  87. return nil, fmt.Errorf("getting node pricing: %w", err)
  88. }
  89. return reader.NewSliceReader(slices.Clone(np)), nil
  90. }
  91. func (pm *PricingModule) GetNodePricing(ctx context.Context, props pricing.NodePricingProperties) (*pricing.NodePricing, error) {
  92. pm.mu.RLock()
  93. defer pm.mu.RUnlock()
  94. np, err := pm.getNodePricing(ctx)
  95. if err != nil {
  96. return nil, err
  97. }
  98. if np != nil {
  99. return np, nil
  100. }
  101. return nil, errors.New("no node pricing")
  102. }
  103. func (pm *PricingModule) NewNodePricingReader(ctx context.Context) (reader.Reader[*pricing.NodePricing], error) {
  104. pm.mu.RLock()
  105. defer pm.mu.RUnlock()
  106. np, err := pm.getNodePricing(ctx)
  107. if err != nil {
  108. return nil, fmt.Errorf("getting node pricing: %w", err)
  109. }
  110. return reader.NewSliceReader([]*pricing.NodePricing{np}), nil
  111. }
  112. func (pm *PricingModule) GetPersistentVolumePricing(ctx context.Context, props pricing.PersistentVolumePricingProperties) (*pricing.PersistentVolumePricing, error) {
  113. pm.mu.RLock()
  114. defer pm.mu.RUnlock()
  115. pvp, err := pm.getPersistentVolumePricing(ctx)
  116. if err != nil {
  117. return nil, err
  118. }
  119. if pvp != nil {
  120. return pvp, nil
  121. }
  122. return nil, errors.New("no persistent volume pricing")
  123. }
  124. func (pm *PricingModule) NewPersistentVolumePricingReader(ctx context.Context) (reader.Reader[*pricing.PersistentVolumePricing], error) {
  125. pm.mu.RLock()
  126. defer pm.mu.RUnlock()
  127. pvp, err := pm.getPersistentVolumePricing(ctx)
  128. if err != nil {
  129. return nil, fmt.Errorf("getting volume pricing: %w", err)
  130. }
  131. return reader.NewSliceReader([]*pricing.PersistentVolumePricing{pvp}), nil
  132. }
  133. func (pm *PricingModule) GetServicePricing(ctx context.Context, props pricing.ServicePricingProperties) (*pricing.ServicePricing, error) {
  134. pm.mu.RLock()
  135. defer pm.mu.RUnlock()
  136. sp, err := pm.getServicePricing(ctx)
  137. if err != nil {
  138. return nil, err
  139. }
  140. if sp != nil {
  141. return sp, nil
  142. }
  143. return nil, errors.New("no service pricing")
  144. }
  145. func (pm *PricingModule) NewServicePricingReader(ctx context.Context) (reader.Reader[*pricing.ServicePricing], error) {
  146. pm.mu.RLock()
  147. defer pm.mu.RUnlock()
  148. sp, err := pm.getServicePricing(ctx)
  149. if err != nil {
  150. return nil, fmt.Errorf("getting service pricing: %w", err)
  151. }
  152. return reader.NewSliceReader([]*pricing.ServicePricing{sp}), nil
  153. }
  154. func (pm *PricingModule) GetPricingSet(ctx context.Context) (*pricing.PricingSet, error) {
  155. pm.mu.RLock()
  156. defer pm.mu.RUnlock()
  157. return pm.store.GetPricingSet(ctx)
  158. }
  159. func (pm *PricingModule) SourceKind() string {
  160. return "basic"
  161. }
  162. func (pm *PricingModule) SourceName() string {
  163. return "basic"
  164. }
  165. func (pm *PricingModule) Checksum(ctx context.Context) (string, error) {
  166. pricingSet, err := pm.store.GetPricingSet(ctx)
  167. if err != nil {
  168. return "", fmt.Errorf("basic pricing module: error getting pricing set: %w", err)
  169. }
  170. checksum, err := pricingSet.Checksum()
  171. if err != nil {
  172. return "", fmt.Errorf("basic pricing module: error computing checksum: %s", err)
  173. }
  174. return checksum, nil
  175. }
  176. // Public CRUD functions
  177. func (pm *PricingModule) SetNodePricePerCPUCoreHour(ctx context.Context, price float64) error {
  178. pm.mu.Lock()
  179. defer pm.mu.Unlock()
  180. return pm.setNodePrice(ctx, pricing.ResourceCPU, unit.VCPUHour, price)
  181. }
  182. func (pm *PricingModule) SetNodePricePerRAMGiBHour(ctx context.Context, price float64) error {
  183. pm.mu.Lock()
  184. defer pm.mu.Unlock()
  185. return pm.setNodePrice(ctx, pricing.ResourceRAM, unit.GiBHour, price)
  186. }
  187. func (pm *PricingModule) SetNodePricePerGPUHour(ctx context.Context, price float64) error {
  188. pm.mu.Lock()
  189. defer pm.mu.Unlock()
  190. return pm.setNodePrice(ctx, pricing.ResourceGPU, unit.GPUHour, price)
  191. }
  192. func (pm *PricingModule) SetNodePricePerLocalDiskGiBHour(ctx context.Context, price float64) error {
  193. pm.mu.Lock()
  194. defer pm.mu.Unlock()
  195. return pm.setNodePrice(ctx, pricing.ResourceStorage, unit.GiBHour, price)
  196. }
  197. func (pm *PricingModule) SetVolumePricePerStorageGiBHour(ctx context.Context, price float64) error {
  198. pm.mu.Lock()
  199. defer pm.mu.Unlock()
  200. return pm.setVolumePrice(ctx, pricing.ResourceStorage, unit.GiBHour, price)
  201. }
  202. // Private functions to set a price by resource and unit
  203. func (pm *PricingModule) setNodePrice(ctx context.Context, resource pricing.Resource, unit unit.Unit, price float64) error {
  204. np, err := pm.getNodePricing(ctx)
  205. if err != nil {
  206. return fmt.Errorf("getting node pricing: %w", err)
  207. }
  208. if np.Prices == nil {
  209. np.Prices = pricing.Prices{}
  210. }
  211. np.Prices[resource] = pricing.Price{Unit: unit, Price: price}
  212. err = pm.setNodePricing(ctx, np)
  213. if err != nil {
  214. return fmt.Errorf("setting node pricing: %w", err)
  215. }
  216. return nil
  217. }
  218. func (pm *PricingModule) setVolumePrice(ctx context.Context, resource pricing.Resource, unit unit.Unit, price float64) error {
  219. vp, err := pm.getPersistentVolumePricing(ctx)
  220. if err != nil {
  221. return fmt.Errorf("getting volume pricing: %w", err)
  222. }
  223. if vp.Prices == nil {
  224. vp.Prices = pricing.Prices{}
  225. }
  226. vp.Prices[resource] = pricing.Price{Unit: unit, Price: price}
  227. err = pm.setPersistentVolumePricing(ctx, vp)
  228. if err != nil {
  229. return fmt.Errorf("setting volume pricing: %w", err)
  230. }
  231. return nil
  232. }
  233. // Private functions to get and set pricing
  234. func (pm *PricingModule) getClusterPricing(ctx context.Context) (*pricing.ClusterPricing, error) {
  235. ps, err := pm.store.GetPricingSet(ctx)
  236. if err != nil {
  237. return nil, fmt.Errorf("getting pricing: %w", err)
  238. }
  239. if len(ps.ClusterPricing) == 0 {
  240. return nil, errors.New("not found")
  241. }
  242. // Only one default ClusterPricing is allowed in basic pricing.
  243. // If multiple exist, return only the first one.
  244. return ps.ClusterPricing[0], nil
  245. }
  246. func (pm *PricingModule) setClusterPricing(ctx context.Context, cp *pricing.ClusterPricing) error {
  247. // TODO
  248. return errors.New("not implemented")
  249. }
  250. func (pm *PricingModule) getNetworkPricing(ctx context.Context) ([]*pricing.NetworkPricing, error) {
  251. ps, err := pm.store.GetPricingSet(ctx)
  252. if err != nil {
  253. return nil, fmt.Errorf("getting pricing: %w", err)
  254. }
  255. if len(ps.NetworkPricing) == 0 {
  256. return nil, errors.New("not found")
  257. }
  258. return ps.NetworkPricing, nil
  259. }
  260. func (pm *PricingModule) setNetworkPricing(ctx context.Context, np *pricing.NetworkPricing) error {
  261. // TODO
  262. return errors.New("not implemented")
  263. }
  264. func (pm *PricingModule) getNodePricing(ctx context.Context) (*pricing.NodePricing, error) {
  265. ps, err := pm.store.GetPricingSet(ctx)
  266. if err != nil {
  267. return nil, fmt.Errorf("getting pricing: %w", err)
  268. }
  269. if len(ps.NodePricing) == 0 {
  270. return nil, errors.New("not found")
  271. }
  272. // Only one default NodePricing is allowed in basic pricing.
  273. // If multiple exist, return only the first one.
  274. return ps.NodePricing[0], nil
  275. }
  276. func (pm *PricingModule) setNodePricing(ctx context.Context, np *pricing.NodePricing) error {
  277. if np == nil {
  278. return errors.New("nil node pricing")
  279. }
  280. // Get the pricing set
  281. ps, err := pm.store.GetPricingSet(ctx)
  282. if err != nil {
  283. return fmt.Errorf("getting pricing: %w", err)
  284. }
  285. // Only one default NodePricing is allowed in basic pricing.
  286. ps.NodePricing = []*pricing.NodePricing{np}
  287. // Set the new pricing set
  288. err = pm.store.SetPricingSet(ctx, ps)
  289. if err != nil {
  290. return fmt.Errorf("setting pricing: %w", err)
  291. }
  292. return nil
  293. }
  294. func (pm *PricingModule) getPersistentVolumePricing(ctx context.Context) (*pricing.PersistentVolumePricing, error) {
  295. ps, err := pm.store.GetPricingSet(ctx)
  296. if err != nil {
  297. return nil, fmt.Errorf("getting pricing: %w", err)
  298. }
  299. if len(ps.PersistentVolumePricing) == 0 {
  300. return nil, errors.New("not found")
  301. }
  302. // Only one default VolumePricing is allowed in basic pricing.
  303. // If multiple exist, return only the first one.
  304. return ps.PersistentVolumePricing[0], nil
  305. }
  306. func (pm *PricingModule) setPersistentVolumePricing(ctx context.Context, vp *pricing.PersistentVolumePricing) error {
  307. if vp == nil {
  308. return errors.New("nil volume pricing")
  309. }
  310. // Get the pricing set
  311. ps, err := pm.store.GetPricingSet(ctx)
  312. if err != nil {
  313. return fmt.Errorf("getting pricing: %w", err)
  314. }
  315. // Only one default VolumePricing is allowed in basic pricing.
  316. ps.PersistentVolumePricing = []*pricing.PersistentVolumePricing{vp}
  317. // Set the new pricing set
  318. err = pm.store.SetPricingSet(ctx, ps)
  319. if err != nil {
  320. return fmt.Errorf("setting pricing: %w", err)
  321. }
  322. return nil
  323. }
  324. func (pm *PricingModule) getServicePricing(ctx context.Context) (*pricing.ServicePricing, error) {
  325. ps, err := pm.store.GetPricingSet(ctx)
  326. if err != nil {
  327. return nil, fmt.Errorf("getting pricing: %w", err)
  328. }
  329. if len(ps.ServicePricing) == 0 {
  330. return nil, errors.New("not found")
  331. }
  332. // Only one default ServicePricing is allowed in basic pricing.
  333. // If multiple exist, return only the first one.
  334. return ps.ServicePricing[0], nil
  335. }
  336. func (pm *PricingModule) setServicePricing(ctx context.Context, sp *pricing.ServicePricing) error {
  337. // TODO
  338. return errors.New("not implemented")
  339. }