providerconfig.go 5.8 KB

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