providerconfig.go 6.1 KB

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