2
0

watcher.go 10 KB

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