watcher.go 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351
  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. 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. multiConfigPath := path.Join(env.GetConfigPathWithDefault("/var/configs"), cloudIntegrationSecretPath)
  205. exists, err := fileutil.FileExists(multiConfigPath)
  206. if err != nil {
  207. log.Errorf("MultiCloudWatcher: error checking file at '%s': %s", multiConfigPath, err.Error())
  208. }
  209. // If config does not exist implies that this configuration method was not used
  210. if !exists {
  211. // check the original location of secret mount
  212. multiConfigPath = path.Join("/var", cloudIntegrationSecretPath)
  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. // If config does not exist implies that this configuration method was not used
  218. if !exists {
  219. return nil
  220. }
  221. }
  222. configurations := &Configurations{}
  223. err = loadFile(multiConfigPath, configurations)
  224. if err != nil {
  225. log.Errorf("MultiCloudWatcher: Error getting file '%s': %s", multiConfigPath, err.Error())
  226. return nil
  227. }
  228. return configurations.ToSlice()
  229. }
  230. func GetCloudBillingWatchers(providerConfig models.ProviderConfig) map[ConfigSource]cloud.KeyedConfigWatcher {
  231. watchers := make(map[ConfigSource]cloud.KeyedConfigWatcher, 3)
  232. watchers[MultiCloudSource] = &MultiCloudWatcher{}
  233. if providerConfig != nil {
  234. watchers[HelmSource] = &HelmWatcher{providerConfig: providerConfig}
  235. watchers[ConfigFileSource] = &ConfigFileWatcher{providerConfig: providerConfig}
  236. }
  237. return watchers
  238. }
  239. // loadFile unmarshals the json content of a file into the provided object
  240. // an empty return with no error indicates that the file did not exist.
  241. func loadFile[T any](path string, content T) error {
  242. exists, err := fileutil.FileExists(path)
  243. if err != nil {
  244. return fmt.Errorf("loadFile: error checking file at '%s': %s", path, err.Error())
  245. }
  246. // If file does not exist implies that this configuration method was not used
  247. if !exists {
  248. return nil
  249. }
  250. result, err := ioutil.ReadFile(path)
  251. if err != nil {
  252. return fmt.Errorf("loadFile: Error reading file: %s", err.Error())
  253. }
  254. err = json.Unmarshal(result, content)
  255. if err != nil {
  256. return fmt.Errorf("loadFile: Error reading json: %s", err.Error())
  257. }
  258. return nil
  259. }
  260. // ConfigSource is an Enum of the sources int value of the Source determines its priority
  261. type ConfigSource int
  262. const (
  263. UnknownSource ConfigSource = iota
  264. ConfigControllerSource
  265. MultiCloudSource
  266. ConfigFileSource
  267. HelmSource
  268. )
  269. func GetConfigSource(str string) ConfigSource {
  270. switch str {
  271. case "configController":
  272. return ConfigControllerSource
  273. case "configfile":
  274. return ConfigFileSource
  275. case "helm":
  276. return HelmSource
  277. case "multicloud":
  278. return MultiCloudSource
  279. default:
  280. return UnknownSource
  281. }
  282. }
  283. func (cs ConfigSource) String() string {
  284. switch cs {
  285. case ConfigControllerSource:
  286. return "configController"
  287. case ConfigFileSource:
  288. return "configfile"
  289. case HelmSource:
  290. return "helm"
  291. case MultiCloudSource:
  292. return "multicloud"
  293. case UnknownSource:
  294. return "unknown"
  295. default:
  296. return "unknown"
  297. }
  298. }