providerconfig.go 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228
  1. package cloud
  2. import (
  3. "fmt"
  4. "io/ioutil"
  5. "reflect"
  6. "strconv"
  7. "strings"
  8. "sync"
  9. "github.com/kubecost/cost-model/pkg/env"
  10. "github.com/kubecost/cost-model/pkg/util/fileutil"
  11. "github.com/kubecost/cost-model/pkg/util/json"
  12. "github.com/microcosm-cc/bluemonday"
  13. "k8s.io/klog"
  14. )
  15. var sanitizePolicy = bluemonday.UGCPolicy()
  16. // ProviderConfig is a utility class that provides a thread-safe configuration
  17. // storage/cache for all Provider implementations
  18. type ProviderConfig struct {
  19. lock *sync.Mutex
  20. fileName string
  21. configPath string
  22. customPricing *CustomPricing
  23. }
  24. // Creates a new ProviderConfig instance
  25. func NewProviderConfig(file string) *ProviderConfig {
  26. return &ProviderConfig{
  27. lock: new(sync.Mutex),
  28. fileName: file,
  29. configPath: configPathFor(file),
  30. customPricing: nil,
  31. }
  32. }
  33. // Non-ThreadSafe logic to load the config file if a cache does not exist. Flag to write
  34. // the default config if the config file doesn't exist.
  35. func (pc *ProviderConfig) loadConfig(writeIfNotExists bool) (*CustomPricing, error) {
  36. if pc.customPricing != nil {
  37. return pc.customPricing, nil
  38. }
  39. exists, err := fileExists(pc.configPath)
  40. // File Error other than NotExists
  41. if err != nil {
  42. klog.Infof("Custom Pricing file at path '%s' read error: '%s'", pc.configPath, err.Error())
  43. return DefaultPricing(), err
  44. }
  45. // File Doesn't Exist
  46. if !exists {
  47. klog.Infof("Could not find Custom Pricing file at path '%s'", pc.configPath)
  48. pc.customPricing = DefaultPricing()
  49. // Only write the file if flag enabled
  50. if writeIfNotExists {
  51. cj, err := json.Marshal(pc.customPricing)
  52. if err != nil {
  53. return pc.customPricing, err
  54. }
  55. err = ioutil.WriteFile(pc.configPath, cj, 0644)
  56. if err != nil {
  57. klog.Infof("Could not write Custom Pricing file to path '%s'", pc.configPath)
  58. return pc.customPricing, err
  59. }
  60. }
  61. return pc.customPricing, nil
  62. }
  63. // File Exists - Read all contents of file, unmarshal json
  64. byteValue, err := ioutil.ReadFile(pc.configPath)
  65. if err != nil {
  66. klog.Infof("Could not read Custom Pricing file at path %s", pc.configPath)
  67. // If read fails, we don't want to cache default, assuming that the file is valid
  68. return DefaultPricing(), err
  69. }
  70. var customPricing CustomPricing
  71. err = json.Unmarshal(byteValue, &customPricing)
  72. if err != nil {
  73. klog.Infof("Could not decode Custom Pricing file at path %s", pc.configPath)
  74. return DefaultPricing(), err
  75. }
  76. pc.customPricing = &customPricing
  77. if pc.customPricing.SpotGPU == "" {
  78. pc.customPricing.SpotGPU = DefaultPricing().SpotGPU // Migration for users without this value set by default.
  79. }
  80. if pc.customPricing.ShareTenancyCosts == "" {
  81. pc.customPricing.ShareTenancyCosts = defaultShareTenancyCost
  82. }
  83. return pc.customPricing, nil
  84. }
  85. // ThreadSafe method for retrieving the custom pricing config.
  86. func (pc *ProviderConfig) GetCustomPricingData() (*CustomPricing, error) {
  87. pc.lock.Lock()
  88. defer pc.lock.Unlock()
  89. return pc.loadConfig(true)
  90. }
  91. // Allows a call to manually update the configuration while maintaining proper thread-safety
  92. // for read/write methods.
  93. func (pc *ProviderConfig) Update(updateFunc func(*CustomPricing) error) (*CustomPricing, error) {
  94. pc.lock.Lock()
  95. defer pc.lock.Unlock()
  96. // Load Config, set flag to _not_ write if failure to find file.
  97. // We're about to write the updated values, so we don't want to double write.
  98. c, _ := pc.loadConfig(false)
  99. // Execute Update - On error, return the in-memory config but don't update cache
  100. // explicitly
  101. err := updateFunc(c)
  102. if err != nil {
  103. return c, err
  104. }
  105. // Cache Update (possible the ptr already references the cached value)
  106. pc.customPricing = c
  107. cj, err := json.Marshal(c)
  108. if err != nil {
  109. return c, err
  110. }
  111. err = ioutil.WriteFile(pc.configPath, cj, 0644)
  112. if err != nil {
  113. return c, err
  114. }
  115. return c, nil
  116. }
  117. // ThreadSafe update of the config using a string map
  118. func (pc *ProviderConfig) UpdateFromMap(a map[string]string) (*CustomPricing, error) {
  119. // Run our Update() method using SetCustomPricingField logic
  120. return pc.Update(func(c *CustomPricing) error {
  121. for k, v := range a {
  122. // Just so we consistently supply / receive the same values, uppercase the first letter.
  123. kUpper := strings.Title(k)
  124. if kUpper == "CPU" || kUpper == "SpotCPU" || kUpper == "RAM" || kUpper == "SpotRAM" || kUpper == "GPU" || kUpper == "Storage" {
  125. val, err := strconv.ParseFloat(v, 64)
  126. if err != nil {
  127. return fmt.Errorf("Unable to parse CPU from string to float: %s", err.Error())
  128. }
  129. v = fmt.Sprintf("%f", val/730)
  130. }
  131. err := SetCustomPricingField(c, kUpper, v)
  132. if err != nil {
  133. return err
  134. }
  135. }
  136. return nil
  137. })
  138. }
  139. // DefaultPricing should be returned so we can do computation even if no file is supplied.
  140. func DefaultPricing() *CustomPricing {
  141. return &CustomPricing{
  142. Provider: "base",
  143. Description: "Default prices based on GCP us-central1",
  144. CPU: "0.031611",
  145. SpotCPU: "0.006655",
  146. RAM: "0.004237",
  147. SpotRAM: "0.000892",
  148. GPU: "0.95",
  149. SpotGPU: "0.308",
  150. Storage: "0.00005479452",
  151. ZoneNetworkEgress: "0.01",
  152. RegionNetworkEgress: "0.01",
  153. InternetNetworkEgress: "0.12",
  154. CustomPricesEnabled: "false",
  155. ShareTenancyCosts: "true",
  156. }
  157. }
  158. func SetCustomPricingField(obj *CustomPricing, name string, value string) error {
  159. structValue := reflect.ValueOf(obj).Elem()
  160. structFieldValue := structValue.FieldByName(name)
  161. if !structFieldValue.IsValid() {
  162. return fmt.Errorf("No such field: %s in obj", name)
  163. }
  164. if !structFieldValue.CanSet() {
  165. return fmt.Errorf("Cannot set %s field value", name)
  166. }
  167. structFieldType := structFieldValue.Type()
  168. value = sanitizePolicy.Sanitize(value)
  169. val := reflect.ValueOf(value)
  170. if structFieldType != val.Type() {
  171. return fmt.Errorf("Provided value type didn't match custom pricing field type")
  172. }
  173. structFieldValue.Set(val)
  174. return nil
  175. }
  176. // File exists has three different return cases that should be handled:
  177. // 1. File exists and is not a directory (true, nil)
  178. // 2. File does not exist (false, nil)
  179. // 3. File may or may not exist. Error occurred during stat (false, error)
  180. // The third case represents the scenario where the stat returns an error,
  181. // but the error isn't relevant to the path. This can happen when the current
  182. // user doesn't have permission to access the file.
  183. func fileExists(filename string) (bool, error) {
  184. return fileutil.FileExists(filename) // delegate to utility method
  185. }
  186. // Returns the configuration directory concatenated with a specific config file name
  187. func configPathFor(filename string) string {
  188. path := env.GetConfigPathWithDefault("/models/")
  189. return path + filename
  190. }