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/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.GetConfigPathWithDefault("/models/")+"key.json", &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. // If the sample nil service key name is set, zero it out so that it is not
  178. // misinterpreted as a real service key.
  179. if aai.ServiceKeyName == "AKIXXX" {
  180. aai.ServiceKeyName = ""
  181. }
  182. kc := aws.ConvertAwsAthenaInfoToConfig(aai)
  183. configs = append(configs, kc)
  184. return configs
  185. }
  186. //detect Alibaba Configuration
  187. if customPricing.AlibabaClusterRegion != "" {
  188. aliCloudInfo := alibaba.AlibabaInfo{
  189. AlibabaClusterRegion: customPricing.AlibabaClusterRegion,
  190. AlibabaServiceKeyName: customPricing.AlibabaServiceKeyName,
  191. AlibabaServiceKeySecret: customPricing.AlibabaServiceKeySecret,
  192. AlibabaAccountID: customPricing.ProjectID,
  193. }
  194. kc := alibaba.ConvertAlibabaInfoToConfig(aliCloudInfo)
  195. configs = append(configs, kc)
  196. return configs
  197. }
  198. return configs
  199. }
  200. // MultiCloudWatcher ingests values a MultiCloudConfig from the file pulled in from the secret by the helm chart
  201. type MultiCloudWatcher struct {
  202. }
  203. func (mcw *MultiCloudWatcher) GetConfigs() []cloud.KeyedConfig {
  204. var multiConfigPath string
  205. if env.IsKubernetesEnabled() {
  206. multiConfigPath = path.Join(env.GetConfigPathWithDefault("/var/configs"), cloudIntegrationSecretPath)
  207. } else {
  208. multiConfigPath = env.GetCloudCostConfigPath()
  209. }
  210. exists, err := fileutil.FileExists(multiConfigPath)
  211. if err != nil {
  212. log.Errorf("MultiCloudWatcher: error checking file at '%s': %s", multiConfigPath, err.Error())
  213. }
  214. // If config does not exist implies that this configuration method was not used
  215. if !exists {
  216. // check the original location of secret mount
  217. multiConfigPath = path.Join("/var", cloudIntegrationSecretPath)
  218. exists, err = fileutil.FileExists(multiConfigPath)
  219. if err != nil {
  220. log.Errorf("MultiCloudWatcher: error checking file at '%s': %s", multiConfigPath, err.Error())
  221. }
  222. // If config does not exist implies that this configuration method was not used
  223. if !exists {
  224. return nil
  225. }
  226. }
  227. log.Debugf("MultiCloudWatcher GetConfigs: multiConfigPath: %s", multiConfigPath)
  228. configurations := &Configurations{}
  229. err = loadFile(multiConfigPath, configurations)
  230. if err != nil {
  231. log.Errorf("MultiCloudWatcher: Error getting file '%s': %s", multiConfigPath, err.Error())
  232. return nil
  233. }
  234. return configurations.ToSlice()
  235. }
  236. func GetCloudBillingWatchers(providerConfig models.ProviderConfig) map[ConfigSource]cloud.KeyedConfigWatcher {
  237. watchers := make(map[ConfigSource]cloud.KeyedConfigWatcher, 3)
  238. watchers[MultiCloudSource] = &MultiCloudWatcher{}
  239. if providerConfig != nil {
  240. watchers[HelmSource] = &HelmWatcher{providerConfig: providerConfig}
  241. watchers[ConfigFileSource] = &ConfigFileWatcher{providerConfig: providerConfig}
  242. }
  243. return watchers
  244. }
  245. // loadFile unmarshals the json content of a file into the provided object
  246. // an empty return with no error indicates that the file did not exist.
  247. func loadFile[T any](path string, content T) error {
  248. exists, err := fileutil.FileExists(path)
  249. if err != nil {
  250. return fmt.Errorf("loadFile: error checking file at '%s': %s", path, err.Error())
  251. }
  252. // If file does not exist implies that this configuration method was not used
  253. if !exists {
  254. return nil
  255. }
  256. result, err := ioutil.ReadFile(path)
  257. if err != nil {
  258. return fmt.Errorf("loadFile: Error reading file: %s", err.Error())
  259. }
  260. err = json.Unmarshal(result, content)
  261. if err != nil {
  262. return fmt.Errorf("loadFile: Error reading json: %s", err.Error())
  263. }
  264. return nil
  265. }
  266. // ConfigSource is an Enum of the sources int value of the Source determines its priority
  267. type ConfigSource int
  268. const (
  269. UnknownSource ConfigSource = iota
  270. ConfigControllerSource
  271. MultiCloudSource
  272. ConfigFileSource
  273. HelmSource
  274. )
  275. func GetConfigSource(str string) ConfigSource {
  276. switch str {
  277. case "configController":
  278. return ConfigControllerSource
  279. case "configfile":
  280. return ConfigFileSource
  281. case "helm":
  282. return HelmSource
  283. case "multicloud":
  284. return MultiCloudSource
  285. default:
  286. return UnknownSource
  287. }
  288. }
  289. func (cs ConfigSource) String() string {
  290. switch cs {
  291. case ConfigControllerSource:
  292. return "configController"
  293. case ConfigFileSource:
  294. return "configfile"
  295. case HelmSource:
  296. return "helm"
  297. case MultiCloudSource:
  298. return "multicloud"
  299. case UnknownSource:
  300. return "unknown"
  301. default:
  302. return "unknown"
  303. }
  304. }