providerconfig.go 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221
  1. package cloud
  2. import (
  3. "encoding/json"
  4. "fmt"
  5. "io/ioutil"
  6. "reflect"
  7. "strconv"
  8. "strings"
  9. "sync"
  10. "github.com/kubecost/cost-model/pkg/env"
  11. "github.com/kubecost/cost-model/pkg/util"
  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. return pc.customPricing, nil
  81. }
  82. // ThreadSafe method for retrieving the custom pricing config.
  83. func (pc *ProviderConfig) GetCustomPricingData() (*CustomPricing, error) {
  84. pc.lock.Lock()
  85. defer pc.lock.Unlock()
  86. return pc.loadConfig(true)
  87. }
  88. // Allows a call to manually update the configuration while maintaining proper thread-safety
  89. // for read/write methods.
  90. func (pc *ProviderConfig) Update(updateFunc func(*CustomPricing) error) (*CustomPricing, error) {
  91. pc.lock.Lock()
  92. defer pc.lock.Unlock()
  93. // Load Config, set flag to _not_ write if failure to find file.
  94. // We're about to write the updated values, so we don't want to double write.
  95. c, _ := pc.loadConfig(false)
  96. // Execute Update - On error, return the in-memory config but don't update cache
  97. // explicitly
  98. err := updateFunc(c)
  99. if err != nil {
  100. return c, err
  101. }
  102. // Cache Update (possible the ptr already references the cached value)
  103. pc.customPricing = c
  104. cj, err := json.Marshal(c)
  105. if err != nil {
  106. return c, err
  107. }
  108. err = ioutil.WriteFile(pc.configPath, cj, 0644)
  109. if err != nil {
  110. return c, err
  111. }
  112. return c, nil
  113. }
  114. // ThreadSafe update of the config using a string map
  115. func (pc *ProviderConfig) UpdateFromMap(a map[string]string) (*CustomPricing, error) {
  116. // Run our Update() method using SetCustomPricingField logic
  117. return pc.Update(func(c *CustomPricing) error {
  118. for k, v := range a {
  119. // Just so we consistently supply / receive the same values, uppercase the first letter.
  120. kUpper := strings.Title(k)
  121. if kUpper == "CPU" || kUpper == "SpotCPU" || kUpper == "RAM" || kUpper == "SpotRAM" || kUpper == "GPU" || kUpper == "Storage" {
  122. val, err := strconv.ParseFloat(v, 64)
  123. if err != nil {
  124. return fmt.Errorf("Unable to parse CPU from string to float: %s", err.Error())
  125. }
  126. v = fmt.Sprintf("%f", val/730)
  127. }
  128. err := SetCustomPricingField(c, kUpper, v)
  129. if err != nil {
  130. return err
  131. }
  132. }
  133. return nil
  134. })
  135. }
  136. // DefaultPricing should be returned so we can do computation even if no file is supplied.
  137. func DefaultPricing() *CustomPricing {
  138. return &CustomPricing{
  139. Provider: "base",
  140. Description: "Default prices based on GCP us-central1",
  141. CPU: "0.031611",
  142. SpotCPU: "0.006655",
  143. RAM: "0.004237",
  144. SpotRAM: "0.000892",
  145. GPU: "0.95",
  146. SpotGPU: "0.308",
  147. Storage: "0.00005479452",
  148. ZoneNetworkEgress: "0.01",
  149. RegionNetworkEgress: "0.01",
  150. InternetNetworkEgress: "0.12",
  151. CustomPricesEnabled: "false",
  152. }
  153. }
  154. func SetCustomPricingField(obj *CustomPricing, name string, value string) error {
  155. structValue := reflect.ValueOf(obj).Elem()
  156. structFieldValue := structValue.FieldByName(name)
  157. if !structFieldValue.IsValid() {
  158. return fmt.Errorf("No such field: %s in obj", name)
  159. }
  160. if !structFieldValue.CanSet() {
  161. return fmt.Errorf("Cannot set %s field value", name)
  162. }
  163. structFieldType := structFieldValue.Type()
  164. value = sanitizePolicy.Sanitize(value)
  165. val := reflect.ValueOf(value)
  166. if structFieldType != val.Type() {
  167. return fmt.Errorf("Provided value type didn't match custom pricing field type")
  168. }
  169. structFieldValue.Set(val)
  170. return nil
  171. }
  172. // File exists has three different return cases that should be handled:
  173. // 1. File exists and is not a directory (true, nil)
  174. // 2. File does not exist (false, nil)
  175. // 3. File may or may not exist. Error occurred during stat (false, error)
  176. // The third case represents the scenario where the stat returns an error,
  177. // but the error isn't relevant to the path. This can happen when the current
  178. // user doesn't have permission to access the file.
  179. func fileExists(filename string) (bool, error) {
  180. return util.FileExists(filename) // delegate to utility method
  181. }
  182. // Returns the configuration directory concatenated with a specific config file name
  183. func configPathFor(filename string) string {
  184. path := env.GetConfigPathWithDefault("/models/")
  185. return path + filename
  186. }