providerconfig.go 5.8 KB

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