watcher.go 10 KB

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