router.go 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637
  1. package costmodel
  2. import (
  3. "context"
  4. "crypto/subtle"
  5. "encoding/base64"
  6. "fmt"
  7. "net/http"
  8. "os"
  9. "reflect"
  10. "strconv"
  11. "strings"
  12. "sync"
  13. "time"
  14. "github.com/opencost/opencost/core/pkg/kubeconfig"
  15. "github.com/opencost/opencost/core/pkg/nodestats"
  16. "github.com/opencost/opencost/core/pkg/protocol"
  17. "github.com/opencost/opencost/core/pkg/source"
  18. "github.com/opencost/opencost/core/pkg/storage"
  19. "github.com/opencost/opencost/core/pkg/util/retry"
  20. "github.com/opencost/opencost/core/pkg/util/timeutil"
  21. "github.com/opencost/opencost/core/pkg/version"
  22. cloudconfig "github.com/opencost/opencost/pkg/cloud/config"
  23. "github.com/opencost/opencost/pkg/cloud/provider"
  24. "github.com/opencost/opencost/pkg/cloudcost"
  25. "github.com/opencost/opencost/pkg/config"
  26. "github.com/opencost/opencost/pkg/customcost"
  27. "github.com/opencost/opencost/pkg/metrics"
  28. "github.com/opencost/opencost/pkg/util/watcher"
  29. "github.com/julienschmidt/httprouter"
  30. "github.com/opencost/opencost/core/pkg/clustercache"
  31. "github.com/opencost/opencost/core/pkg/clusters"
  32. sysenv "github.com/opencost/opencost/core/pkg/env"
  33. "github.com/opencost/opencost/core/pkg/log"
  34. "github.com/opencost/opencost/core/pkg/util/json"
  35. "github.com/opencost/opencost/modules/collector-source/pkg/collector"
  36. "github.com/opencost/opencost/modules/prometheus-source/pkg/prom"
  37. "github.com/opencost/opencost/pkg/cloud/models"
  38. clusterc "github.com/opencost/opencost/pkg/clustercache"
  39. "github.com/opencost/opencost/pkg/env"
  40. km "github.com/opencost/opencost/pkg/kubemodel"
  41. metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
  42. "github.com/patrickmn/go-cache"
  43. "k8s.io/client-go/kubernetes"
  44. )
  45. const (
  46. RFC3339Milli = "2006-01-02T15:04:05.000Z"
  47. CustomPricingSetting = "CustomPricing"
  48. DiscountSetting = "Discount"
  49. )
  50. var (
  51. // gitCommit is set by the build system
  52. gitCommit string
  53. proto = protocol.HTTP()
  54. )
  55. // Accesses defines a singleton application instance, providing access to
  56. // Prometheus, Kubernetes, the cloud provider, and caches.
  57. type Accesses struct {
  58. DataSource source.OpenCostDataSource
  59. KubeClientSet kubernetes.Interface
  60. ClusterCache clustercache.ClusterCache
  61. ClusterMap clusters.ClusterMap
  62. CloudProvider models.Provider
  63. ConfigFileManager *config.ConfigFileManager
  64. ClusterInfoProvider clusters.ClusterInfoProvider
  65. Model *CostModel
  66. MetricsEmitter *CostModelMetricsEmitter
  67. KubeModelPipeline *km.Pipeline
  68. KubeModelQuerier km.Querier
  69. // SettingsCache stores current state of app settings
  70. SettingsCache *cache.Cache
  71. // settingsSubscribers tracks channels through which changes to different
  72. // settings will be published in a pub/sub model
  73. settingsSubscribers map[string][]chan string
  74. settingsMutex sync.Mutex
  75. }
  76. func filterFields(fields string, data map[string]*CostData) map[string]CostData {
  77. fs := strings.Split(fields, ",")
  78. fmap := make(map[string]bool)
  79. for _, f := range fs {
  80. fieldNameLower := strings.ToLower(f) // convert to go struct name by uppercasing first letter
  81. log.Debugf("to delete: %s", fieldNameLower)
  82. fmap[fieldNameLower] = true
  83. }
  84. filteredData := make(map[string]CostData)
  85. for cname, costdata := range data {
  86. s := reflect.TypeOf(*costdata)
  87. val := reflect.ValueOf(*costdata)
  88. costdata2 := CostData{}
  89. cd2 := reflect.New(reflect.Indirect(reflect.ValueOf(costdata2)).Type()).Elem()
  90. n := s.NumField()
  91. for i := 0; i < n; i++ {
  92. field := s.Field(i)
  93. value := val.Field(i)
  94. value2 := cd2.Field(i)
  95. if _, ok := fmap[strings.ToLower(field.Name)]; !ok {
  96. value2.Set(reflect.Value(value))
  97. }
  98. }
  99. filteredData[cname] = cd2.Interface().(CostData)
  100. }
  101. return filteredData
  102. }
  103. // ParsePercentString takes a string of expected format "N%" and returns a floating point 0.0N.
  104. // If the "%" symbol is missing, it just returns 0.0N. Empty string is interpreted as "0%" and
  105. // return 0.0.
  106. func ParsePercentString(percentStr string) (float64, error) {
  107. if len(percentStr) == 0 {
  108. return 0.0, nil
  109. }
  110. if percentStr[len(percentStr)-1:] == "%" {
  111. percentStr = percentStr[:len(percentStr)-1]
  112. }
  113. discount, err := strconv.ParseFloat(percentStr, 64)
  114. if err != nil {
  115. return 0.0, err
  116. }
  117. discount *= 0.01
  118. return discount, nil
  119. }
  120. // adminAuthMiddleware wraps a handler and requires a Bearer token matching ADMIN_TOKEN env var when set.
  121. // When ADMIN_TOKEN is not set, logs a deduped warning and allows the request through.
  122. // When ADMIN_TOKEN is set, returns 401 if the Bearer token is missing or 403 if it does not match.
  123. func adminAuthMiddleware(next httprouter.Handle) httprouter.Handle {
  124. return func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
  125. adminToken := env.GetAdminToken()
  126. if adminToken == "" {
  127. log.DedupedWarningf(5, "Admin token (ADMIN_TOKEN) not configured; write operations are unauthenticated")
  128. next(w, r, ps)
  129. return
  130. }
  131. authHeader := r.Header.Get("Authorization")
  132. const prefix = "Bearer "
  133. if !strings.HasPrefix(authHeader, prefix) {
  134. http.Error(w, "Missing or invalid authorization", http.StatusUnauthorized)
  135. return
  136. }
  137. bearerToken := strings.TrimPrefix(authHeader, prefix)
  138. if subtle.ConstantTimeCompare([]byte(bearerToken), []byte(adminToken)) != 1 {
  139. http.Error(w, "Missing or invalid authorization", http.StatusForbidden)
  140. return
  141. }
  142. next(w, r, ps)
  143. }
  144. }
  145. func WriteData(w http.ResponseWriter, data interface{}, err error) {
  146. if err != nil {
  147. proto.WriteError(w, proto.InternalServerError(err.Error()))
  148. return
  149. }
  150. proto.WriteData(w, data)
  151. }
  152. // RefreshPricingData needs to be called when a new node joins the fleet, since we cache the relevant subsets of pricing data to avoid storing the whole thing.
  153. func (a *Accesses) RefreshPricingData(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
  154. w.Header().Set("Content-Type", "application/json")
  155. w.Header().Set("Access-Control-Allow-Origin", "*")
  156. err := a.CloudProvider.DownloadPricingData()
  157. if err != nil {
  158. log.Errorf("Error refreshing pricing data: %s", err.Error())
  159. }
  160. WriteData(w, nil, err)
  161. }
  162. func (a *Accesses) CostDataModel(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
  163. w.Header().Set("Content-Type", "application/json")
  164. w.Header().Set("Access-Control-Allow-Origin", "*")
  165. window := r.URL.Query().Get("timeWindow")
  166. offset := r.URL.Query().Get("offset")
  167. fields := r.URL.Query().Get("filterFields")
  168. namespace := r.URL.Query().Get("namespace")
  169. duration, err := timeutil.ParseDuration(window)
  170. if err != nil {
  171. WriteData(w, nil, fmt.Errorf("error parsing window (%s): %s", window, err))
  172. return
  173. }
  174. end := time.Now()
  175. if offset != "" {
  176. offsetDur, err := timeutil.ParseDuration(offset)
  177. if err != nil {
  178. WriteData(w, nil, fmt.Errorf("error parsing offset (%s): %s", offset, err))
  179. return
  180. }
  181. end = end.Add(-offsetDur)
  182. }
  183. start := end.Add(-duration)
  184. data, err := a.Model.ComputeCostData(start, end)
  185. // apply filter by removing if != namespace
  186. if namespace != "" {
  187. for key, costData := range data {
  188. if costData.Namespace != namespace {
  189. delete(data, key)
  190. }
  191. }
  192. }
  193. if fields != "" {
  194. filteredData := filterFields(fields, data)
  195. WriteData(w, filteredData, err)
  196. } else {
  197. WriteData(w, data, err)
  198. }
  199. }
  200. func (a *Accesses) GetAllNodePricing(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
  201. w.Header().Set("Content-Type", "application/json")
  202. w.Header().Set("Access-Control-Allow-Origin", "*")
  203. data, err := a.CloudProvider.AllNodePricing()
  204. WriteData(w, data, err)
  205. }
  206. func (a *Accesses) ManagementPlatform(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
  207. w.Header().Set("Content-Type", "application/json")
  208. w.Header().Set("Access-Control-Allow-Origin", "*")
  209. data, err := a.CloudProvider.GetManagementPlatform()
  210. WriteData(w, data, err)
  211. }
  212. func (a *Accesses) ClusterInfo(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
  213. w.Header().Set("Content-Type", "application/json")
  214. w.Header().Set("Access-Control-Allow-Origin", "*")
  215. data := a.ClusterInfoProvider.GetClusterInfo()
  216. WriteData(w, data, nil)
  217. }
  218. func (a *Accesses) GetClusterInfoMap(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
  219. w.Header().Set("Content-Type", "application/json")
  220. w.Header().Set("Access-Control-Allow-Origin", "*")
  221. data := a.ClusterMap.AsMap()
  222. WriteData(w, data, nil)
  223. }
  224. func (a *Accesses) GetServiceAccountStatus(w http.ResponseWriter, _ *http.Request, _ httprouter.Params) {
  225. w.Header().Set("Content-Type", "application/json")
  226. w.Header().Set("Access-Control-Allow-Origin", "*")
  227. WriteData(w, a.CloudProvider.ServiceAccountStatus(), nil)
  228. }
  229. func (a *Accesses) GetPricingSourceStatus(w http.ResponseWriter, _ *http.Request, _ httprouter.Params) {
  230. w.Header().Set("Content-Type", "application/json")
  231. w.Header().Set("Access-Control-Allow-Origin", "*")
  232. data := a.CloudProvider.PricingSourceStatus()
  233. WriteData(w, data, nil)
  234. }
  235. func (a *Accesses) GetPricingSourceCounts(w http.ResponseWriter, _ *http.Request, _ httprouter.Params) {
  236. w.Header().Set("Content-Type", "application/json")
  237. w.Header().Set("Access-Control-Allow-Origin", "*")
  238. data, err := a.Model.GetPricingSourceCounts()
  239. WriteData(w, data, err)
  240. }
  241. func (a *Accesses) GetPricingSourceSummary(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
  242. w.Header().Set("Content-Type", "application/json")
  243. w.Header().Set("Access-Control-Allow-Origin", "*")
  244. data := a.CloudProvider.PricingSourceSummary()
  245. WriteData(w, data, nil)
  246. }
  247. func (a *Accesses) GetOrphanedPods(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
  248. w.Header().Set("Content-Type", "application/json")
  249. w.Header().Set("Access-Control-Allow-Origin", "*")
  250. podlist := a.ClusterCache.GetAllPods()
  251. var lonePods []*clustercache.Pod
  252. for _, pod := range podlist {
  253. if len(pod.OwnerReferences) == 0 {
  254. lonePods = append(lonePods, pod)
  255. }
  256. }
  257. body, err := json.Marshal(lonePods)
  258. if err != nil {
  259. fmt.Fprintf(w, "Error decoding pod: %s", err)
  260. } else {
  261. w.Write(body)
  262. }
  263. }
  264. func (a *Accesses) GetInstallNamespace(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
  265. w.Header().Set("Content-Type", "application/json")
  266. w.Header().Set("Access-Control-Allow-Origin", "*")
  267. ns := env.GetOpencostNamespace()
  268. w.Write([]byte(ns))
  269. }
  270. type InstallInfo struct {
  271. Containers []ContainerInfo `json:"containers"`
  272. ClusterInfo map[string]string `json:"clusterInfo"`
  273. Version string `json:"version"`
  274. }
  275. type ContainerInfo struct {
  276. ContainerName string `json:"containerName"`
  277. Image string `json:"image"`
  278. StartTime string `json:"startTime"`
  279. }
  280. func (a *Accesses) GetInstallInfo(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
  281. w.Header().Set("Content-Type", "application/json")
  282. w.Header().Set("Access-Control-Allow-Origin", "*")
  283. containers, err := GetKubecostContainers(a.KubeClientSet)
  284. if err != nil {
  285. http.Error(w, fmt.Sprintf("Unable to list pods: %s", err.Error()), http.StatusInternalServerError)
  286. return
  287. }
  288. info := InstallInfo{
  289. Containers: containers,
  290. ClusterInfo: make(map[string]string),
  291. Version: version.FriendlyVersion(),
  292. }
  293. nodes := a.ClusterCache.GetAllNodes()
  294. cachePods := a.ClusterCache.GetAllPods()
  295. info.ClusterInfo["nodeCount"] = strconv.Itoa(len(nodes))
  296. info.ClusterInfo["podCount"] = strconv.Itoa(len(cachePods))
  297. body, err := json.Marshal(info)
  298. if err != nil {
  299. http.Error(w, fmt.Sprintf("Error decoding pod: %s", err.Error()), http.StatusInternalServerError)
  300. return
  301. }
  302. w.Write(body)
  303. }
  304. func GetKubecostContainers(kubeClientSet kubernetes.Interface) ([]ContainerInfo, error) {
  305. pods, err := kubeClientSet.CoreV1().Pods(env.GetOpencostNamespace()).List(context.Background(), metav1.ListOptions{
  306. LabelSelector: "app=cost-analyzer",
  307. FieldSelector: "status.phase=Running",
  308. Limit: 1,
  309. })
  310. if err != nil {
  311. return nil, fmt.Errorf("failed to query kubernetes client for kubecost pods: %s", err)
  312. }
  313. // If we have zero pods either something is weird with the install since the app selector is not exposed in the helm
  314. // chart or more likely we are running locally - in either case Images field will return as null
  315. containers := make([]ContainerInfo, 0)
  316. if len(pods.Items) > 0 {
  317. for _, pod := range pods.Items {
  318. for _, container := range pod.Spec.Containers {
  319. c := ContainerInfo{
  320. ContainerName: container.Name,
  321. Image: container.Image,
  322. StartTime: pod.Status.StartTime.String(),
  323. }
  324. containers = append(containers, c)
  325. }
  326. }
  327. }
  328. return containers, nil
  329. }
  330. func (a *Accesses) AddServiceKey(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
  331. w.Header().Set("Content-Type", "application/json")
  332. w.Header().Set("Access-Control-Allow-Origin", "*")
  333. r.ParseForm()
  334. key := r.PostForm.Get("key")
  335. k := []byte(key)
  336. err := os.WriteFile(env.GetGCPAuthSecretFilePath(), k, 0644)
  337. if err != nil {
  338. fmt.Fprintf(w, "Error writing service key: %s", err)
  339. }
  340. w.WriteHeader(http.StatusOK)
  341. }
  342. func (a *Accesses) GetHelmValues(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
  343. w.Header().Set("Content-Type", "application/json")
  344. w.Header().Set("Access-Control-Allow-Origin", "*")
  345. encodedValues := sysenv.Get("HELM_VALUES", "")
  346. if encodedValues == "" {
  347. fmt.Fprintf(w, "Values reporting disabled")
  348. return
  349. }
  350. result, err := base64.StdEncoding.DecodeString(encodedValues)
  351. if err != nil {
  352. fmt.Fprintf(w, "Failed to decode encoded values: %s", err)
  353. return
  354. }
  355. w.Write(result)
  356. }
  357. func Initialize(router *httprouter.Router, additionalConfigWatchers ...*watcher.ConfigMapWatcher) *Accesses {
  358. var err error
  359. // Kubernetes API setup
  360. kubeClientset, err := kubeconfig.LoadKubeClient("")
  361. if err != nil {
  362. log.Fatalf("Failed to build Kubernetes client: %s", err.Error())
  363. }
  364. clusterUID, err := kubeconfig.GetClusterUID(kubeClientset)
  365. if err != nil {
  366. log.Fatalf("Failed to determine cluster UID: %s", err)
  367. }
  368. // Create Kubernetes Cluster Cache + Watchers
  369. k8sCache := clusterc.NewKubernetesClusterCache(kubeClientset)
  370. k8sCache.Run()
  371. // Create ConfigFileManager for synchronization of shared configuration
  372. confManager := config.NewConfigFileManager(nil)
  373. store := storage.GetConfiguredStorage()
  374. cloudProviderKey := env.GetCloudProviderAPIKey()
  375. cloudProvider, err := provider.NewProvider(k8sCache, cloudProviderKey, confManager)
  376. if err != nil {
  377. panic(err.Error())
  378. }
  379. // ClusterInfo Provider to provide the cluster map with local and remote cluster data
  380. var clusterInfoProvider clusters.ClusterInfoProvider
  381. if env.IsClusterInfoFileEnabled() {
  382. clusterInfoFile := confManager.ConfigFileAt(env.GetClusterInfoFilePath())
  383. clusterInfoProvider = NewConfiguredClusterInfoProvider(clusterInfoFile)
  384. } else {
  385. clusterInfoProvider = NewLocalClusterInfoProvider(kubeClientset, cloudProvider)
  386. }
  387. const maxRetries = 10
  388. const retryInterval = 10 * time.Second
  389. var fatalErr error
  390. ctx, cancel := context.WithCancel(context.Background())
  391. fn := func() (source.OpenCostDataSource, error) {
  392. ds, e := prom.NewDefaultPrometheusDataSource(clusterInfoProvider)
  393. if e != nil {
  394. if source.IsRetryable(e) {
  395. return nil, e
  396. }
  397. fatalErr = e
  398. cancel()
  399. }
  400. return ds, e
  401. }
  402. if env.IsCollectorDataSourceEnabled() {
  403. fn = func() (source.OpenCostDataSource, error) {
  404. nodeStatConf, err := NewNodeClientConfigFromEnv()
  405. if err != nil {
  406. return nil, fmt.Errorf("failed to get node client config: %w", err)
  407. }
  408. clusterConfig, err := kubeconfig.LoadKubeconfig("")
  409. if err != nil {
  410. return nil, fmt.Errorf("failed to load kube config: %w", err)
  411. }
  412. nodeStatClient := nodestats.NewNodeStatsSummaryClient(k8sCache, nodeStatConf, clusterConfig)
  413. ds := collector.NewDefaultCollectorDataSource(
  414. clusterUID,
  415. store,
  416. clusterInfoProvider,
  417. k8sCache,
  418. nodeStatClient,
  419. )
  420. return ds, nil
  421. }
  422. }
  423. dataSource, _ := retry.Retry(
  424. ctx,
  425. fn,
  426. maxRetries,
  427. retryInterval,
  428. )
  429. if fatalErr != nil {
  430. log.Fatalf("Failed to create Prometheus data source: %s", fatalErr)
  431. panic(fatalErr)
  432. }
  433. // Append the pricing config watcher
  434. installNamespace := env.GetOpencostNamespace()
  435. configWatchers := watcher.NewConfigMapWatchers(kubeClientset, installNamespace, additionalConfigWatchers...)
  436. configWatchers.AddWatcher(provider.ConfigWatcherFor(cloudProvider))
  437. configWatchers.AddWatcher(metrics.GetMetricsConfigWatcher())
  438. configWatchers.Watch()
  439. clusterMap := dataSource.ClusterMap()
  440. settingsCache := cache.New(cache.NoExpiration, cache.NoExpiration)
  441. costModel := NewCostModel(clusterUID, dataSource, cloudProvider, k8sCache, clusterMap, dataSource.BatchDuration())
  442. metricsEmitter := NewCostModelMetricsEmitter(k8sCache, cloudProvider, clusterInfoProvider, costModel)
  443. appName := sysenv.GetAppName()
  444. var kubeModelPipeline *km.Pipeline
  445. if p, err := km.NewPipeline(appName, clusterUID, store, costModel); err != nil {
  446. log.Errorf("Failed to initialize KubeModel pipeline: %v", err)
  447. } else {
  448. p.Start()
  449. kubeModelPipeline = p
  450. }
  451. kubeModelQuerier := km.NewQuerier(appName, clusterUID, store)
  452. a := &Accesses{
  453. DataSource: dataSource,
  454. KubeClientSet: kubeClientset,
  455. ClusterCache: k8sCache,
  456. ClusterMap: clusterMap,
  457. CloudProvider: cloudProvider,
  458. ConfigFileManager: confManager,
  459. ClusterInfoProvider: clusterInfoProvider,
  460. Model: costModel,
  461. MetricsEmitter: metricsEmitter,
  462. KubeModelPipeline: kubeModelPipeline,
  463. KubeModelQuerier: kubeModelQuerier,
  464. SettingsCache: settingsCache,
  465. }
  466. // Initialize mechanism for subscribing to settings changes
  467. a.InitializeSettingsPubSub()
  468. err = a.CloudProvider.DownloadPricingData()
  469. if err != nil {
  470. log.Infof("Failed to download pricing data: %s", err)
  471. }
  472. if !env.IsKubecostMetricsPodEnabled() {
  473. a.MetricsEmitter.Start()
  474. }
  475. a.DataSource.RegisterEndPoints(router)
  476. router.GET("/costDataModel", a.CostDataModel)
  477. router.GET("/allocation/compute", a.ComputeAllocationHandler)
  478. router.GET("/allocation/compute/summary", a.ComputeAllocationHandlerSummary)
  479. router.GET("/allNodePricing", a.GetAllNodePricing)
  480. router.POST("/refreshPricing", a.RefreshPricingData)
  481. router.GET("/managementPlatform", a.ManagementPlatform)
  482. router.GET("/clusterInfo", a.ClusterInfo)
  483. router.GET("/clusterInfoMap", a.GetClusterInfoMap)
  484. router.GET("/serviceAccountStatus", a.GetServiceAccountStatus)
  485. router.GET("/pricingSourceStatus", a.GetPricingSourceStatus)
  486. router.GET("/pricingSourceSummary", a.GetPricingSourceSummary)
  487. router.GET("/pricingSourceCounts", a.GetPricingSourceCounts)
  488. router.GET("/orphanedPods", a.GetOrphanedPods)
  489. router.GET("/installNamespace", a.GetInstallNamespace)
  490. router.GET("/installInfo", a.GetInstallInfo)
  491. router.POST("/serviceKey", adminAuthMiddleware(a.AddServiceKey))
  492. router.GET("/helmValues", a.GetHelmValues)
  493. return a
  494. }
  495. // InitializeCloudCost Initializes Cloud Cost pipeline and querier and registers endpoints
  496. func InitializeCloudCost(router *httprouter.Router) *cloudcost.PipelineService {
  497. log.Debugf("Cloud Cost config path: %s", env.GetCloudCostConfigPath())
  498. cloudConfigController := cloudconfig.NewMemoryController(nil)
  499. repo := cloudcost.NewMemoryRepository()
  500. cloudCostPipelineService := cloudcost.NewPipelineService(repo, cloudConfigController, cloudcost.DefaultIngestorConfiguration())
  501. repoQuerier := cloudcost.NewRepositoryQuerier(repo)
  502. cloudCostQueryService := cloudcost.NewQueryService(repoQuerier, repoQuerier)
  503. router.GET("/cloudCost", cloudCostQueryService.GetCloudCostHandler())
  504. router.GET("/cloudCost/view/graph", cloudCostQueryService.GetCloudCostViewGraphHandler())
  505. router.GET("/cloudCost/view/totals", cloudCostQueryService.GetCloudCostViewTotalsHandler())
  506. router.GET("/cloudCost/view/table", cloudCostQueryService.GetCloudCostViewTableHandler(nil))
  507. router.GET("/cloudCost/status", cloudCostPipelineService.GetCloudCostStatusHandler())
  508. router.GET("/cloudCost/rebuild", adminAuthMiddleware(cloudCostPipelineService.GetCloudCostRebuildHandler()))
  509. router.GET("/cloudCost/repair", adminAuthMiddleware(cloudCostPipelineService.GetCloudCostRepairHandler()))
  510. router.GET("/cloud/config/export", adminAuthMiddleware(cloudConfigController.GetExportConfigHandler()))
  511. router.GET("/cloud/config/enable", adminAuthMiddleware(cloudConfigController.GetEnableConfigHandler()))
  512. router.GET("/cloud/config/disable", adminAuthMiddleware(cloudConfigController.GetDisableConfigHandler()))
  513. router.GET("/cloud/config/delete", adminAuthMiddleware(cloudConfigController.GetDeleteConfigHandler()))
  514. return cloudCostPipelineService
  515. }
  516. func InitializeCustomCost(router *httprouter.Router) *customcost.PipelineService {
  517. hourlyRepo := customcost.NewMemoryRepository()
  518. dailyRepo := customcost.NewMemoryRepository()
  519. ingConfig := customcost.DefaultIngestorConfiguration()
  520. var err error
  521. customCostPipelineService, err := customcost.NewPipelineService(hourlyRepo, dailyRepo, ingConfig)
  522. if err != nil {
  523. log.Errorf("error instantiating custom cost pipeline service: %v", err)
  524. return nil
  525. }
  526. customCostQuerier := customcost.NewRepositoryQuerier(hourlyRepo, dailyRepo, ingConfig.HourlyDuration, ingConfig.DailyDuration)
  527. customCostQueryService := customcost.NewQueryService(customCostQuerier)
  528. router.GET("/customCost/total", customCostQueryService.GetCustomCostTotalHandler())
  529. router.GET("/customCost/timeseries", customCostQueryService.GetCustomCostTimeseriesHandler())
  530. return customCostPipelineService
  531. }