watcher.go 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359
  1. package config
  2. import (
  3. "fmt"
  4. "io/ioutil"
  5. "path"
  6. "github.com/opencost/opencost/pkg/cloud"
  7. "github.com/opencost/opencost/pkg/cloud/alibaba"
  8. "github.com/opencost/opencost/pkg/cloud/aws"
  9. "github.com/opencost/opencost/pkg/cloud/azure"
  10. "github.com/opencost/opencost/pkg/cloud/gcp"
  11. "github.com/opencost/opencost/pkg/cloud/models"
  12. "github.com/opencost/opencost/pkg/env"
  13. "github.com/opencost/opencost/pkg/log"
  14. "github.com/opencost/opencost/pkg/util/fileutil"
  15. "github.com/opencost/opencost/pkg/util/json"
  16. )
  17. const authSecretPath = "/var/secrets/service-key.json"
  18. const storageConfigSecretPath = "/var/azure-storage-config/azure-storage-config.json"
  19. const cloudIntegrationSecretPath = "/cloud-integration/cloud-integration.json"
  20. type HelmWatcher struct {
  21. providerConfig models.ProviderConfig
  22. }
  23. // GetConfigs checks secret files and config map set via the helm chart for Cloud Billing integrations. Returns
  24. // only one billing integration due to values being shared by different configuration types.
  25. func (hw *HelmWatcher) GetConfigs() []cloud.KeyedConfig {
  26. var configs []cloud.KeyedConfig
  27. log.Info("+++HelmWatcher GetConfigs")
  28. customPricing, _ := hw.providerConfig.GetCustomPricingData()
  29. // check for Azure Storage config in secret file
  30. exists, err := fileutil.FileExists(storageConfigSecretPath)
  31. if err != nil {
  32. log.Errorf("HelmWatcher: AzureStorage: error checking file at '%s': %s", storageConfigSecretPath, err.Error())
  33. }
  34. // If file does not exist implies that this configuration method was not used
  35. if exists {
  36. result, err2 := ioutil.ReadFile(storageConfigSecretPath)
  37. if err2 != nil {
  38. log.Errorf("HelmWatcher: AzureStorage: Error reading file: %s", err2.Error())
  39. return nil
  40. }
  41. asc := &azure.AzureStorageConfig{}
  42. err2 = json.Unmarshal(result, asc)
  43. if err2 != nil {
  44. log.Errorf("HelmWatcher: AzureStorage: Error reading json: %s", err2.Error())
  45. return nil
  46. }
  47. if asc != nil && !asc.IsEmpty() {
  48. // If subscription id is not set it may be present in the rate card API
  49. if asc.SubscriptionId == "" {
  50. ask := &azure.AzureServiceKey{}
  51. err3 := loadFile(authSecretPath, ask)
  52. if err3 != nil {
  53. log.Errorf("HelmWatcher: AzureStorage: AzureRateCard: %s", err3)
  54. }
  55. if ask != nil {
  56. asc.SubscriptionId = ask.SubscriptionID
  57. }
  58. }
  59. // If SubscriptionID is still empty check the customPricing
  60. if asc.SubscriptionId == "" {
  61. asc.SubscriptionId = customPricing.AzureSubscriptionID
  62. }
  63. kc := azure.ConvertAzureStorageConfigToConfig(*asc)
  64. configs = append(configs, kc)
  65. return configs
  66. }
  67. }
  68. exists, err = fileutil.FileExists(authSecretPath)
  69. if err != nil {
  70. log.Errorf("HelmWatcher: error checking file at '%s': %s", authSecretPath, err.Error())
  71. }
  72. // If the Auth Secret is not set then the config file watch will be responsible for providing the configurer for the
  73. // config values present in the CustomPricing object
  74. if exists {
  75. if customPricing.BillingDataDataset != "" {
  76. // Big Query Configuration
  77. bqc := gcp.BigQueryConfig{
  78. ProjectID: customPricing.ProjectID,
  79. BillingDataDataset: customPricing.BillingDataDataset,
  80. }
  81. key := make(map[string]string)
  82. err2 := loadFile(authSecretPath, &key)
  83. if err2 != nil {
  84. log.Errorf("HelmWatcher: GCP: %s", err2)
  85. }
  86. if key != nil && len(key) != 0 {
  87. bqc.Key = key
  88. }
  89. kc := gcp.ConvertBigQueryConfigToConfig(bqc)
  90. configs = append(configs, kc)
  91. return configs
  92. }
  93. if customPricing.AthenaBucketName != "" {
  94. aai := aws.AwsAthenaInfo{
  95. AthenaBucketName: customPricing.AthenaBucketName,
  96. AthenaRegion: customPricing.AthenaRegion,
  97. AthenaDatabase: customPricing.AthenaDatabase,
  98. AthenaTable: customPricing.AthenaTable,
  99. AthenaWorkgroup: customPricing.AthenaWorkgroup,
  100. AccountID: customPricing.AthenaProjectID,
  101. MasterPayerARN: customPricing.MasterPayerARN,
  102. }
  103. // If Account ID is blank check ProjectID
  104. if aai.AccountID == "" {
  105. aai.AccountID = customPricing.ProjectID
  106. }
  107. var accessKey aws.AWSAccessKey
  108. err2 := loadFile(authSecretPath, &accessKey)
  109. if err2 != nil {
  110. log.Errorf("HelmWatcher: AWS: %s", err2)
  111. }
  112. aai.ServiceKeyName = accessKey.AccessKeyID
  113. aai.ServiceKeySecret = accessKey.SecretAccessKey
  114. kc := aws.ConvertAwsAthenaInfoToConfig(aai)
  115. configs = append(configs, kc)
  116. return configs
  117. }
  118. }
  119. return configs
  120. }
  121. type ConfigFileWatcher struct {
  122. providerConfig models.ProviderConfig
  123. }
  124. // GetConfigs checks secret files and config map set via the helm chart for Cloud Billing integrations. Returns
  125. // only one billing integration due to values being shared by different configuration types.
  126. func (cfw *ConfigFileWatcher) GetConfigs() []cloud.KeyedConfig {
  127. var configs []cloud.KeyedConfig
  128. log.Info("+++ConfigFileWatcher GetConfigs")
  129. customPricing, _ := cfw.providerConfig.GetCustomPricingData()
  130. // Detect Azure Storage configuration
  131. if customPricing.AzureSubscriptionID != "" {
  132. asc := azure.AzureStorageConfig{
  133. SubscriptionId: customPricing.AzureSubscriptionID,
  134. AccountName: customPricing.AzureStorageAccount,
  135. AccessKey: customPricing.AzureStorageAccessKey,
  136. ContainerName: customPricing.AzureStorageContainer,
  137. ContainerPath: customPricing.AzureContainerPath,
  138. AzureCloud: customPricing.AzureCloud,
  139. }
  140. kc := azure.ConvertAzureStorageConfigToConfig(asc)
  141. configs = append(configs, kc)
  142. return configs
  143. }
  144. // Detect Big Query Configuration
  145. if customPricing.BillingDataDataset != "" {
  146. bqc := gcp.BigQueryConfig{
  147. ProjectID: customPricing.ProjectID,
  148. BillingDataDataset: customPricing.BillingDataDataset,
  149. }
  150. var key map[string]string
  151. err2 := loadFile(env.GetConfigPathWithDefault("/models/")+"key.json", &key)
  152. if err2 != nil {
  153. log.Errorf("ConfigFileWatcher: GCP: %s", err2)
  154. }
  155. if key != nil && len(key) != 0 {
  156. bqc.Key = key
  157. }
  158. kc := gcp.ConvertBigQueryConfigToConfig(bqc)
  159. configs = append(configs, kc)
  160. return configs
  161. }
  162. // Detect AWS configuration
  163. if customPricing.AthenaBucketName != "" {
  164. aai := aws.AwsAthenaInfo{
  165. AthenaBucketName: customPricing.AthenaBucketName,
  166. AthenaRegion: customPricing.AthenaRegion,
  167. AthenaDatabase: customPricing.AthenaDatabase,
  168. AthenaTable: customPricing.AthenaTable,
  169. AthenaWorkgroup: customPricing.AthenaWorkgroup,
  170. ServiceKeyName: customPricing.ServiceKeyName,
  171. ServiceKeySecret: customPricing.ServiceKeySecret,
  172. AccountID: customPricing.AthenaProjectID,
  173. MasterPayerARN: customPricing.MasterPayerARN,
  174. }
  175. // If Account ID is blank check ProjectID
  176. if aai.AccountID == "" {
  177. aai.AccountID = customPricing.ProjectID
  178. }
  179. // If the sample nil service key name is set, zero it out so that it is not
  180. // misinterpreted as a real service key.
  181. if aai.ServiceKeyName == "AKIXXX" {
  182. aai.ServiceKeyName = ""
  183. }
  184. kc := aws.ConvertAwsAthenaInfoToConfig(aai)
  185. configs = append(configs, kc)
  186. return configs
  187. }
  188. //detect Alibaba Configuration
  189. if customPricing.AlibabaClusterRegion != "" {
  190. aliCloudInfo := alibaba.AlibabaInfo{
  191. AlibabaClusterRegion: customPricing.AlibabaClusterRegion,
  192. AlibabaServiceKeyName: customPricing.AlibabaServiceKeyName,
  193. AlibabaServiceKeySecret: customPricing.AlibabaServiceKeySecret,
  194. AlibabaAccountID: customPricing.ProjectID,
  195. }
  196. kc := alibaba.ConvertAlibabaInfoToConfig(aliCloudInfo)
  197. configs = append(configs, kc)
  198. return configs
  199. }
  200. return configs
  201. }
  202. // MultiCloudWatcher ingests values a MultiCloudConfig from the file pulled in from the secret by the helm chart
  203. type MultiCloudWatcher struct {
  204. }
  205. func (mcw *MultiCloudWatcher) GetConfigs() []cloud.KeyedConfig {
  206. log.Info("+++MultiCloudWatcher GetConfigs")
  207. multiConfigPath := path.Join(env.GetConfigPathWithDefault("/var/configs"), cloudIntegrationSecretPath)
  208. log.Infof("+++ConfigFileWatcher GetConfigs: multiConfigPath: %s", multiConfigPath)
  209. exists, err := fileutil.FileExists(multiConfigPath)
  210. if err != nil {
  211. log.Errorf("MultiCloudWatcher: error checking file at '%s': %s", multiConfigPath, err.Error())
  212. }
  213. // If config does not exist implies that this configuration method was not used
  214. if !exists {
  215. // check the original location of secret mount
  216. multiConfigPath = path.Join("/var", cloudIntegrationSecretPath)
  217. exists, err = fileutil.FileExists(multiConfigPath)
  218. if err != nil {
  219. log.Errorf("MultiCloudWatcher: error checking file at '%s': %s", multiConfigPath, err.Error())
  220. }
  221. // If config does not exist implies that this configuration method was not used
  222. if !exists {
  223. return nil
  224. }
  225. }
  226. configurations := &Configurations{}
  227. err = loadFile(multiConfigPath, configurations)
  228. if err != nil {
  229. log.Errorf("MultiCloudWatcher: Error getting file '%s': %s", multiConfigPath, err.Error())
  230. return nil
  231. }
  232. return configurations.ToSlice()
  233. }
  234. func GetCloudBillingWatchers(providerConfig models.ProviderConfig) map[ConfigSource]cloud.KeyedConfigWatcher {
  235. watchers := make(map[ConfigSource]cloud.KeyedConfigWatcher, 3)
  236. watchers[MultiCloudSource] = &MultiCloudWatcher{}
  237. if providerConfig != nil {
  238. watchers[HelmSource] = &HelmWatcher{providerConfig: providerConfig}
  239. watchers[ConfigFileSource] = &ConfigFileWatcher{providerConfig: providerConfig}
  240. }
  241. return watchers
  242. }
  243. // loadFile unmarshals the json content of a file into the provided object
  244. // an empty return with no error indicates that the file did not exist.
  245. func loadFile[T any](path string, content T) error {
  246. exists, err := fileutil.FileExists(path)
  247. if err != nil {
  248. return fmt.Errorf("loadFile: error checking file at '%s': %s", path, err.Error())
  249. }
  250. // If file does not exist implies that this configuration method was not used
  251. if !exists {
  252. return nil
  253. }
  254. result, err := ioutil.ReadFile(path)
  255. if err != nil {
  256. return fmt.Errorf("loadFile: Error reading file: %s", err.Error())
  257. }
  258. err = json.Unmarshal(result, content)
  259. if err != nil {
  260. return fmt.Errorf("loadFile: Error reading json: %s", err.Error())
  261. }
  262. return nil
  263. }
  264. // ConfigSource is an Enum of the sources int value of the Source determines its priority
  265. type ConfigSource int
  266. const (
  267. UnknownSource ConfigSource = iota
  268. ConfigControllerSource
  269. MultiCloudSource
  270. ConfigFileSource
  271. HelmSource
  272. )
  273. func GetConfigSource(str string) ConfigSource {
  274. switch str {
  275. case "configController":
  276. return ConfigControllerSource
  277. case "configfile":
  278. return ConfigFileSource
  279. case "helm":
  280. return HelmSource
  281. case "multicloud":
  282. return MultiCloudSource
  283. default:
  284. return UnknownSource
  285. }
  286. }
  287. func (cs ConfigSource) String() string {
  288. switch cs {
  289. case ConfigControllerSource:
  290. return "configController"
  291. case ConfigFileSource:
  292. return "configfile"
  293. case HelmSource:
  294. return "helm"
  295. case MultiCloudSource:
  296. return "multicloud"
  297. case UnknownSource:
  298. return "unknown"
  299. default:
  300. return "unknown"
  301. }
  302. }