router.go 57 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879
  1. package costmodel
  2. import (
  3. "context"
  4. "encoding/base64"
  5. "fmt"
  6. "io"
  7. "net/http"
  8. "os"
  9. "path"
  10. "reflect"
  11. "regexp"
  12. "strconv"
  13. "strings"
  14. "sync"
  15. "time"
  16. "github.com/microcosm-cc/bluemonday"
  17. "github.com/opencost/opencost/core/pkg/opencost"
  18. "github.com/opencost/opencost/core/pkg/util/httputil"
  19. "github.com/opencost/opencost/core/pkg/util/timeutil"
  20. "github.com/opencost/opencost/core/pkg/util/watcher"
  21. "github.com/opencost/opencost/core/pkg/version"
  22. "github.com/opencost/opencost/pkg/cloud/aws"
  23. cloudconfig "github.com/opencost/opencost/pkg/cloud/config"
  24. "github.com/opencost/opencost/pkg/cloud/gcp"
  25. "github.com/opencost/opencost/pkg/cloud/provider"
  26. "github.com/opencost/opencost/pkg/cloudcost"
  27. "github.com/opencost/opencost/pkg/config"
  28. clustermap "github.com/opencost/opencost/pkg/costmodel/clusters"
  29. "github.com/opencost/opencost/pkg/customcost"
  30. "github.com/opencost/opencost/pkg/kubeconfig"
  31. "github.com/opencost/opencost/pkg/metrics"
  32. "github.com/opencost/opencost/pkg/services"
  33. "github.com/spf13/viper"
  34. v1 "k8s.io/api/core/v1"
  35. "github.com/julienschmidt/httprouter"
  36. "github.com/getsentry/sentry-go"
  37. "github.com/opencost/opencost/core/pkg/clusters"
  38. sysenv "github.com/opencost/opencost/core/pkg/env"
  39. "github.com/opencost/opencost/core/pkg/log"
  40. "github.com/opencost/opencost/core/pkg/util/json"
  41. "github.com/opencost/opencost/pkg/cloud/azure"
  42. "github.com/opencost/opencost/pkg/cloud/models"
  43. "github.com/opencost/opencost/pkg/cloud/utils"
  44. "github.com/opencost/opencost/pkg/clustercache"
  45. "github.com/opencost/opencost/pkg/env"
  46. "github.com/opencost/opencost/pkg/errors"
  47. "github.com/opencost/opencost/pkg/prom"
  48. "github.com/opencost/opencost/pkg/thanos"
  49. prometheus "github.com/prometheus/client_golang/api"
  50. prometheusAPI "github.com/prometheus/client_golang/api/prometheus/v1"
  51. appsv1 "k8s.io/api/apps/v1"
  52. metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
  53. "github.com/patrickmn/go-cache"
  54. "k8s.io/client-go/kubernetes"
  55. )
  56. var sanitizePolicy = bluemonday.UGCPolicy()
  57. const (
  58. RFC3339Milli = "2006-01-02T15:04:05.000Z"
  59. maxCacheMinutes1d = 11
  60. maxCacheMinutes2d = 17
  61. maxCacheMinutes7d = 37
  62. maxCacheMinutes30d = 137
  63. CustomPricingSetting = "CustomPricing"
  64. DiscountSetting = "Discount"
  65. epRules = apiPrefix + "/rules"
  66. LogSeparator = "+-------------------------------------------------------------------------------------"
  67. )
  68. var (
  69. // gitCommit is set by the build system
  70. gitCommit string
  71. // ANSIRegex matches ANSI escape and colors https://en.wikipedia.org/wiki/ANSI_escape_code
  72. ANSIRegex = regexp.MustCompile("\x1b\\[[0-9;]*m")
  73. )
  74. // Accesses defines a singleton application instance, providing access to
  75. // Prometheus, Kubernetes, the cloud provider, and caches.
  76. type Accesses struct {
  77. Router *httprouter.Router
  78. PrometheusClient prometheus.Client
  79. ThanosClient prometheus.Client
  80. KubeClientSet kubernetes.Interface
  81. ClusterCache clustercache.ClusterCache
  82. ClusterMap clusters.ClusterMap
  83. CloudProvider models.Provider
  84. ConfigFileManager *config.ConfigFileManager
  85. CloudConfigController *cloudconfig.Controller
  86. CloudCostPipelineService *cloudcost.PipelineService
  87. CloudCostQueryService *cloudcost.QueryService
  88. CustomCostPipelineService *customcost.PipelineService
  89. ClusterInfoProvider clusters.ClusterInfoProvider
  90. Model *CostModel
  91. MetricsEmitter *CostModelMetricsEmitter
  92. OutOfClusterCache *cache.Cache
  93. AggregateCache *cache.Cache
  94. CostDataCache *cache.Cache
  95. ClusterCostsCache *cache.Cache
  96. CacheExpiration map[time.Duration]time.Duration
  97. AggAPI Aggregator
  98. // SettingsCache stores current state of app settings
  99. SettingsCache *cache.Cache
  100. // settingsSubscribers tracks channels through which changes to different
  101. // settings will be published in a pub/sub model
  102. settingsSubscribers map[string][]chan string
  103. settingsMutex sync.Mutex
  104. // registered http service instances
  105. httpServices services.HTTPServices
  106. }
  107. // GetPrometheusClient decides whether the default Prometheus client or the Thanos client
  108. // should be used.
  109. func (a *Accesses) GetPrometheusClient(remote bool) prometheus.Client {
  110. // Use Thanos Client if it exists (enabled) and remote flag set
  111. var pc prometheus.Client
  112. if remote && a.ThanosClient != nil {
  113. pc = a.ThanosClient
  114. } else {
  115. pc = a.PrometheusClient
  116. }
  117. return pc
  118. }
  119. // GetCacheExpiration looks up and returns custom cache expiration for the given duration.
  120. // If one does not exists, it returns the default cache expiration, which is defined by
  121. // the particular cache.
  122. func (a *Accesses) GetCacheExpiration(dur time.Duration) time.Duration {
  123. if expiration, ok := a.CacheExpiration[dur]; ok {
  124. return expiration
  125. }
  126. return cache.DefaultExpiration
  127. }
  128. // GetCacheRefresh determines how long to wait before refreshing the cache for the given duration,
  129. // which is done 1 minute before we expect the cache to expire, or 1 minute if expiration is
  130. // not found or is less than 2 minutes.
  131. func (a *Accesses) GetCacheRefresh(dur time.Duration) time.Duration {
  132. expiry := a.GetCacheExpiration(dur).Minutes()
  133. if expiry <= 2.0 {
  134. return time.Minute
  135. }
  136. mins := time.Duration(expiry/2.0) * time.Minute
  137. return mins
  138. }
  139. func (a *Accesses) ClusterCostsFromCacheHandler(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
  140. w.Header().Set("Content-Type", "application/json")
  141. duration := 24 * time.Hour
  142. offset := time.Minute
  143. durationHrs := "24h"
  144. fmtOffset := "1m"
  145. pClient := a.GetPrometheusClient(true)
  146. key := fmt.Sprintf("%s:%s", durationHrs, fmtOffset)
  147. if data, valid := a.ClusterCostsCache.Get(key); valid {
  148. clusterCosts := data.(map[string]*ClusterCosts)
  149. w.Write(WrapDataWithMessage(clusterCosts, nil, "clusterCosts cache hit"))
  150. } else {
  151. data, err := a.ComputeClusterCosts(pClient, a.CloudProvider, duration, offset, true)
  152. w.Write(WrapDataWithMessage(data, err, fmt.Sprintf("clusterCosts cache miss: %s", key)))
  153. }
  154. }
  155. type Response struct {
  156. Code int `json:"code"`
  157. Status string `json:"status"`
  158. Data interface{} `json:"data"`
  159. Message string `json:"message,omitempty"`
  160. Warning string `json:"warning,omitempty"`
  161. }
  162. // FilterFunc is a filter that returns true iff the given CostData should be filtered out, and the environment that was used as the filter criteria, if it was an aggregate
  163. type FilterFunc func(*CostData) (bool, string)
  164. // FilterCostData allows through only CostData that matches all the given filter functions
  165. func FilterCostData(data map[string]*CostData, retains []FilterFunc, filters []FilterFunc) (map[string]*CostData, int, map[string]int) {
  166. result := make(map[string]*CostData)
  167. filteredEnvironments := make(map[string]int)
  168. filteredContainers := 0
  169. DataLoop:
  170. for key, datum := range data {
  171. for _, rf := range retains {
  172. if ok, _ := rf(datum); ok {
  173. result[key] = datum
  174. // if any retain function passes, the data is retained and move on
  175. continue DataLoop
  176. }
  177. }
  178. for _, ff := range filters {
  179. if ok, environment := ff(datum); !ok {
  180. if environment != "" {
  181. filteredEnvironments[environment]++
  182. }
  183. filteredContainers++
  184. // if any filter function check fails, move on to the next datum
  185. continue DataLoop
  186. }
  187. }
  188. result[key] = datum
  189. }
  190. return result, filteredContainers, filteredEnvironments
  191. }
  192. func filterFields(fields string, data map[string]*CostData) map[string]CostData {
  193. fs := strings.Split(fields, ",")
  194. fmap := make(map[string]bool)
  195. for _, f := range fs {
  196. fieldNameLower := strings.ToLower(f) // convert to go struct name by uppercasing first letter
  197. log.Debugf("to delete: %s", fieldNameLower)
  198. fmap[fieldNameLower] = true
  199. }
  200. filteredData := make(map[string]CostData)
  201. for cname, costdata := range data {
  202. s := reflect.TypeOf(*costdata)
  203. val := reflect.ValueOf(*costdata)
  204. costdata2 := CostData{}
  205. cd2 := reflect.New(reflect.Indirect(reflect.ValueOf(costdata2)).Type()).Elem()
  206. n := s.NumField()
  207. for i := 0; i < n; i++ {
  208. field := s.Field(i)
  209. value := val.Field(i)
  210. value2 := cd2.Field(i)
  211. if _, ok := fmap[strings.ToLower(field.Name)]; !ok {
  212. value2.Set(reflect.Value(value))
  213. }
  214. }
  215. filteredData[cname] = cd2.Interface().(CostData)
  216. }
  217. return filteredData
  218. }
  219. func normalizeTimeParam(param string) (string, error) {
  220. if param == "" {
  221. return "", fmt.Errorf("invalid time param")
  222. }
  223. // convert days to hours
  224. if param[len(param)-1:] == "d" {
  225. count := param[:len(param)-1]
  226. val, err := strconv.ParseInt(count, 10, 64)
  227. if err != nil {
  228. return "", err
  229. }
  230. val = val * 24
  231. param = fmt.Sprintf("%dh", val)
  232. }
  233. return param, nil
  234. }
  235. // ParsePercentString takes a string of expected format "N%" and returns a floating point 0.0N.
  236. // If the "%" symbol is missing, it just returns 0.0N. Empty string is interpreted as "0%" and
  237. // return 0.0.
  238. func ParsePercentString(percentStr string) (float64, error) {
  239. if len(percentStr) == 0 {
  240. return 0.0, nil
  241. }
  242. if percentStr[len(percentStr)-1:] == "%" {
  243. percentStr = percentStr[:len(percentStr)-1]
  244. }
  245. discount, err := strconv.ParseFloat(percentStr, 64)
  246. if err != nil {
  247. return 0.0, err
  248. }
  249. discount *= 0.01
  250. return discount, nil
  251. }
  252. func WrapData(data interface{}, err error) []byte {
  253. var resp []byte
  254. if err != nil {
  255. log.Errorf("Error returned to client: %s", err.Error())
  256. resp, _ = json.Marshal(&Response{
  257. Code: http.StatusInternalServerError,
  258. Status: "error",
  259. Message: err.Error(),
  260. Data: data,
  261. })
  262. } else {
  263. resp, err = json.Marshal(&Response{
  264. Code: http.StatusOK,
  265. Status: "success",
  266. Data: data,
  267. })
  268. if err != nil {
  269. log.Errorf("error marshaling response json: %s", err.Error())
  270. }
  271. }
  272. return resp
  273. }
  274. func WrapDataWithMessage(data interface{}, err error, message string) []byte {
  275. var resp []byte
  276. if err != nil {
  277. log.Errorf("Error returned to client: %s", err.Error())
  278. resp, _ = json.Marshal(&Response{
  279. Code: http.StatusInternalServerError,
  280. Status: "error",
  281. Message: err.Error(),
  282. Data: data,
  283. })
  284. } else {
  285. resp, _ = json.Marshal(&Response{
  286. Code: http.StatusOK,
  287. Status: "success",
  288. Data: data,
  289. Message: message,
  290. })
  291. }
  292. return resp
  293. }
  294. func WrapDataWithWarning(data interface{}, err error, warning string) []byte {
  295. var resp []byte
  296. if err != nil {
  297. log.Errorf("Error returned to client: %s", err.Error())
  298. resp, _ = json.Marshal(&Response{
  299. Code: http.StatusInternalServerError,
  300. Status: "error",
  301. Message: err.Error(),
  302. Warning: warning,
  303. Data: data,
  304. })
  305. } else {
  306. resp, _ = json.Marshal(&Response{
  307. Code: http.StatusOK,
  308. Status: "success",
  309. Data: data,
  310. Warning: warning,
  311. })
  312. }
  313. return resp
  314. }
  315. func WrapDataWithMessageAndWarning(data interface{}, err error, message, warning string) []byte {
  316. var resp []byte
  317. if err != nil {
  318. log.Errorf("Error returned to client: %s", err.Error())
  319. resp, _ = json.Marshal(&Response{
  320. Code: http.StatusInternalServerError,
  321. Status: "error",
  322. Message: err.Error(),
  323. Warning: warning,
  324. Data: data,
  325. })
  326. } else {
  327. resp, _ = json.Marshal(&Response{
  328. Code: http.StatusOK,
  329. Status: "success",
  330. Data: data,
  331. Message: message,
  332. Warning: warning,
  333. })
  334. }
  335. return resp
  336. }
  337. // wrapAsObjectItems wraps a slice of items into an object containing a single items list
  338. // allows our k8s proxy methods to emulate a List() request to k8s API
  339. func wrapAsObjectItems(items interface{}) map[string]interface{} {
  340. return map[string]interface{}{
  341. "items": items,
  342. }
  343. }
  344. // 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.
  345. func (a *Accesses) RefreshPricingData(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
  346. w.Header().Set("Content-Type", "application/json")
  347. w.Header().Set("Access-Control-Allow-Origin", "*")
  348. err := a.CloudProvider.DownloadPricingData()
  349. if err != nil {
  350. log.Errorf("Error refreshing pricing data: %s", err.Error())
  351. }
  352. w.Write(WrapData(nil, err))
  353. }
  354. func (a *Accesses) CostDataModel(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
  355. w.Header().Set("Content-Type", "application/json")
  356. w.Header().Set("Access-Control-Allow-Origin", "*")
  357. window := r.URL.Query().Get("timeWindow")
  358. offset := r.URL.Query().Get("offset")
  359. fields := r.URL.Query().Get("filterFields")
  360. namespace := r.URL.Query().Get("namespace")
  361. if offset != "" {
  362. offset = "offset " + offset
  363. }
  364. data, err := a.Model.ComputeCostData(a.PrometheusClient, a.CloudProvider, window, offset, namespace)
  365. if fields != "" {
  366. filteredData := filterFields(fields, data)
  367. w.Write(WrapData(filteredData, err))
  368. } else {
  369. w.Write(WrapData(data, err))
  370. }
  371. }
  372. func (a *Accesses) ClusterCosts(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
  373. w.Header().Set("Content-Type", "application/json")
  374. w.Header().Set("Access-Control-Allow-Origin", "*")
  375. window := r.URL.Query().Get("window")
  376. offset := r.URL.Query().Get("offset")
  377. if window == "" {
  378. w.Write(WrapData(nil, fmt.Errorf("missing window argument")))
  379. return
  380. }
  381. windowDur, err := timeutil.ParseDuration(window)
  382. if err != nil {
  383. w.Write(WrapData(nil, fmt.Errorf("error parsing window (%s): %s", window, err)))
  384. return
  385. }
  386. // offset is not a required parameter
  387. var offsetDur time.Duration
  388. if offset != "" {
  389. offsetDur, err = timeutil.ParseDuration(offset)
  390. if err != nil {
  391. w.Write(WrapData(nil, fmt.Errorf("error parsing offset (%s): %s", offset, err)))
  392. return
  393. }
  394. }
  395. useThanos, _ := strconv.ParseBool(r.URL.Query().Get("multi"))
  396. if useThanos && !thanos.IsEnabled() {
  397. w.Write(WrapData(nil, fmt.Errorf("Multi=true while Thanos is not enabled.")))
  398. return
  399. }
  400. var client prometheus.Client
  401. if useThanos {
  402. client = a.ThanosClient
  403. offsetDur = thanos.OffsetDuration()
  404. } else {
  405. client = a.PrometheusClient
  406. }
  407. data, err := a.ComputeClusterCosts(client, a.CloudProvider, windowDur, offsetDur, true)
  408. w.Write(WrapData(data, err))
  409. }
  410. func (a *Accesses) ClusterCostsOverTime(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
  411. w.Header().Set("Content-Type", "application/json")
  412. w.Header().Set("Access-Control-Allow-Origin", "*")
  413. start := r.URL.Query().Get("start")
  414. end := r.URL.Query().Get("end")
  415. window := r.URL.Query().Get("window")
  416. offset := r.URL.Query().Get("offset")
  417. if window == "" {
  418. w.Write(WrapData(nil, fmt.Errorf("missing window argument")))
  419. return
  420. }
  421. windowDur, err := timeutil.ParseDuration(window)
  422. if err != nil {
  423. w.Write(WrapData(nil, fmt.Errorf("error parsing window (%s): %s", window, err)))
  424. return
  425. }
  426. // offset is not a required parameter
  427. var offsetDur time.Duration
  428. if offset != "" {
  429. offsetDur, err = timeutil.ParseDuration(offset)
  430. if err != nil {
  431. w.Write(WrapData(nil, fmt.Errorf("error parsing offset (%s): %s", offset, err)))
  432. return
  433. }
  434. }
  435. data, err := ClusterCostsOverTime(a.PrometheusClient, a.CloudProvider, start, end, windowDur, offsetDur)
  436. w.Write(WrapData(data, err))
  437. }
  438. func (a *Accesses) CostDataModelRange(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
  439. w.Header().Set("Content-Type", "application/json")
  440. w.Header().Set("Access-Control-Allow-Origin", "*")
  441. startStr := r.URL.Query().Get("start")
  442. endStr := r.URL.Query().Get("end")
  443. windowStr := r.URL.Query().Get("window")
  444. fields := r.URL.Query().Get("filterFields")
  445. namespace := r.URL.Query().Get("namespace")
  446. cluster := r.URL.Query().Get("cluster")
  447. remote := r.URL.Query().Get("remote")
  448. remoteEnabled := env.IsRemoteEnabled() && remote != "false"
  449. layout := "2006-01-02T15:04:05.000Z"
  450. start, err := time.Parse(layout, startStr)
  451. if err != nil {
  452. w.Write(WrapDataWithMessage(nil, fmt.Errorf("invalid start date: %s", startStr), fmt.Sprintf("invalid start date: %s", startStr)))
  453. return
  454. }
  455. end, err := time.Parse(layout, endStr)
  456. if err != nil {
  457. w.Write(WrapDataWithMessage(nil, fmt.Errorf("invalid end date: %s", endStr), fmt.Sprintf("invalid end date: %s", endStr)))
  458. return
  459. }
  460. window := opencost.NewWindow(&start, &end)
  461. if window.IsOpen() || !window.HasDuration() || window.IsNegative() {
  462. w.Write(WrapDataWithMessage(nil, fmt.Errorf("invalid date range: %s", window), fmt.Sprintf("invalid date range: %s", window)))
  463. return
  464. }
  465. resolution := time.Hour
  466. if resDur, err := time.ParseDuration(windowStr); err == nil {
  467. resolution = resDur
  468. }
  469. // Use Thanos Client if it exists (enabled) and remote flag set
  470. var pClient prometheus.Client
  471. if remote != "false" && a.ThanosClient != nil {
  472. pClient = a.ThanosClient
  473. } else {
  474. pClient = a.PrometheusClient
  475. }
  476. data, err := a.Model.ComputeCostDataRange(pClient, a.CloudProvider, window, resolution, namespace, cluster, remoteEnabled)
  477. if err != nil {
  478. w.Write(WrapData(nil, err))
  479. }
  480. if fields != "" {
  481. filteredData := filterFields(fields, data)
  482. w.Write(WrapData(filteredData, err))
  483. } else {
  484. w.Write(WrapData(data, err))
  485. }
  486. }
  487. func parseAggregations(customAggregation, aggregator, filterType string) (string, []string, string) {
  488. var key string
  489. var filter string
  490. var val []string
  491. if customAggregation != "" {
  492. key = customAggregation
  493. filter = filterType
  494. val = strings.Split(customAggregation, ",")
  495. } else {
  496. aggregations := strings.Split(aggregator, ",")
  497. for i, agg := range aggregations {
  498. aggregations[i] = "kubernetes_" + agg
  499. }
  500. key = strings.Join(aggregations, ",")
  501. filter = "kubernetes_" + filterType
  502. val = aggregations
  503. }
  504. return key, val, filter
  505. }
  506. func (a *Accesses) GetAllNodePricing(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
  507. w.Header().Set("Content-Type", "application/json")
  508. w.Header().Set("Access-Control-Allow-Origin", "*")
  509. data, err := a.CloudProvider.AllNodePricing()
  510. w.Write(WrapData(data, err))
  511. }
  512. func (a *Accesses) GetConfigs(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
  513. w.Header().Set("Content-Type", "application/json")
  514. w.Header().Set("Access-Control-Allow-Origin", "*")
  515. data, err := a.CloudProvider.GetConfig()
  516. w.Write(WrapData(data, err))
  517. }
  518. func (a *Accesses) UpdateSpotInfoConfigs(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
  519. w.Header().Set("Content-Type", "application/json")
  520. w.Header().Set("Access-Control-Allow-Origin", "*")
  521. data, err := a.CloudProvider.UpdateConfig(r.Body, aws.SpotInfoUpdateType)
  522. if err != nil {
  523. w.Write(WrapData(data, err))
  524. return
  525. }
  526. w.Write(WrapData(data, err))
  527. err = a.CloudProvider.DownloadPricingData()
  528. if err != nil {
  529. log.Errorf("Error redownloading data on config update: %s", err.Error())
  530. }
  531. return
  532. }
  533. func (a *Accesses) UpdateAthenaInfoConfigs(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
  534. w.Header().Set("Content-Type", "application/json")
  535. w.Header().Set("Access-Control-Allow-Origin", "*")
  536. data, err := a.CloudProvider.UpdateConfig(r.Body, aws.AthenaInfoUpdateType)
  537. if err != nil {
  538. w.Write(WrapData(data, err))
  539. return
  540. }
  541. w.Write(WrapData(data, err))
  542. return
  543. }
  544. func (a *Accesses) UpdateBigQueryInfoConfigs(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
  545. w.Header().Set("Content-Type", "application/json")
  546. w.Header().Set("Access-Control-Allow-Origin", "*")
  547. data, err := a.CloudProvider.UpdateConfig(r.Body, gcp.BigqueryUpdateType)
  548. if err != nil {
  549. w.Write(WrapData(data, err))
  550. return
  551. }
  552. w.Write(WrapData(data, err))
  553. return
  554. }
  555. func (a *Accesses) UpdateAzureStorageConfigs(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
  556. w.Header().Set("Content-Type", "application/json")
  557. w.Header().Set("Access-Control-Allow-Origin", "*")
  558. data, err := a.CloudProvider.UpdateConfig(r.Body, azure.AzureStorageUpdateType)
  559. if err != nil {
  560. w.Write(WrapData(data, err))
  561. return
  562. }
  563. w.Write(WrapData(data, err))
  564. return
  565. }
  566. func (a *Accesses) UpdateConfigByKey(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
  567. w.Header().Set("Content-Type", "application/json")
  568. w.Header().Set("Access-Control-Allow-Origin", "*")
  569. data, err := a.CloudProvider.UpdateConfig(r.Body, "")
  570. if err != nil {
  571. w.Write(WrapData(data, err))
  572. return
  573. }
  574. w.Write(WrapData(data, err))
  575. return
  576. }
  577. func (a *Accesses) ManagementPlatform(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
  578. w.Header().Set("Content-Type", "application/json")
  579. w.Header().Set("Access-Control-Allow-Origin", "*")
  580. data, err := a.CloudProvider.GetManagementPlatform()
  581. if err != nil {
  582. w.Write(WrapData(data, err))
  583. return
  584. }
  585. w.Write(WrapData(data, err))
  586. return
  587. }
  588. func (a *Accesses) ClusterInfo(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
  589. w.Header().Set("Content-Type", "application/json")
  590. w.Header().Set("Access-Control-Allow-Origin", "*")
  591. data := a.ClusterInfoProvider.GetClusterInfo()
  592. w.Write(WrapData(data, nil))
  593. }
  594. func (a *Accesses) GetClusterInfoMap(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
  595. w.Header().Set("Content-Type", "application/json")
  596. w.Header().Set("Access-Control-Allow-Origin", "*")
  597. data := a.ClusterMap.AsMap()
  598. w.Write(WrapData(data, nil))
  599. }
  600. func (a *Accesses) GetServiceAccountStatus(w http.ResponseWriter, _ *http.Request, _ httprouter.Params) {
  601. w.Header().Set("Content-Type", "application/json")
  602. w.Header().Set("Access-Control-Allow-Origin", "*")
  603. w.Write(WrapData(a.CloudProvider.ServiceAccountStatus(), nil))
  604. }
  605. func (a *Accesses) GetPricingSourceStatus(w http.ResponseWriter, _ *http.Request, _ httprouter.Params) {
  606. w.Header().Set("Content-Type", "application/json")
  607. w.Header().Set("Access-Control-Allow-Origin", "*")
  608. w.Write(WrapData(a.CloudProvider.PricingSourceStatus(), nil))
  609. }
  610. func (a *Accesses) GetPricingSourceCounts(w http.ResponseWriter, _ *http.Request, _ httprouter.Params) {
  611. w.Header().Set("Content-Type", "application/json")
  612. w.Header().Set("Access-Control-Allow-Origin", "*")
  613. w.Write(WrapData(a.Model.GetPricingSourceCounts()))
  614. }
  615. func (a *Accesses) GetPricingSourceSummary(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
  616. w.Header().Set("Content-Type", "application/json")
  617. w.Header().Set("Access-Control-Allow-Origin", "*")
  618. data := a.CloudProvider.PricingSourceSummary()
  619. w.Write(WrapData(data, nil))
  620. }
  621. func (a *Accesses) GetPrometheusMetadata(w http.ResponseWriter, _ *http.Request, _ httprouter.Params) {
  622. w.Header().Set("Content-Type", "application/json")
  623. w.Header().Set("Access-Control-Allow-Origin", "*")
  624. w.Write(WrapData(prom.Validate(a.PrometheusClient)))
  625. }
  626. func (a *Accesses) PrometheusQuery(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
  627. w.Header().Set("Content-Type", "application/json")
  628. w.Header().Set("Access-Control-Allow-Origin", "*")
  629. qp := httputil.NewQueryParams(r.URL.Query())
  630. query := qp.Get("query", "")
  631. if query == "" {
  632. w.Write(WrapData(nil, fmt.Errorf("Query Parameter 'query' is unset'")))
  633. return
  634. }
  635. // Attempt to parse time as either a unix timestamp or as an RFC3339 value
  636. var timeVal time.Time
  637. timeStr := qp.Get("time", "")
  638. if len(timeStr) > 0 {
  639. if t, err := strconv.ParseInt(timeStr, 10, 64); err == nil {
  640. timeVal = time.Unix(t, 0)
  641. } else if t, err := time.Parse(time.RFC3339, timeStr); err == nil {
  642. timeVal = t
  643. }
  644. // If time is given, but not parse-able, return an error
  645. if timeVal.IsZero() {
  646. http.Error(w, fmt.Sprintf("time must be a unix timestamp or RFC3339 value; illegal value given: %s", timeStr), http.StatusBadRequest)
  647. }
  648. }
  649. ctx := prom.NewNamedContext(a.PrometheusClient, prom.FrontendContextName)
  650. body, err := ctx.RawQuery(query, timeVal)
  651. if err != nil {
  652. w.Write(WrapData(nil, fmt.Errorf("Error running query %s. Error: %s", query, err)))
  653. return
  654. }
  655. w.Write(body)
  656. }
  657. func (a *Accesses) PrometheusQueryRange(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
  658. w.Header().Set("Content-Type", "application/json")
  659. w.Header().Set("Access-Control-Allow-Origin", "*")
  660. qp := httputil.NewQueryParams(r.URL.Query())
  661. query := qp.Get("query", "")
  662. if query == "" {
  663. fmt.Fprintf(w, "Error parsing query from request parameters.")
  664. return
  665. }
  666. start, end, duration, err := toStartEndStep(qp)
  667. if err != nil {
  668. fmt.Fprintf(w, err.Error())
  669. return
  670. }
  671. ctx := prom.NewNamedContext(a.PrometheusClient, prom.FrontendContextName)
  672. body, err := ctx.RawQueryRange(query, start, end, duration)
  673. if err != nil {
  674. fmt.Fprintf(w, "Error running query %s. Error: %s", query, err)
  675. return
  676. }
  677. w.Write(body)
  678. }
  679. func (a *Accesses) ThanosQuery(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
  680. w.Header().Set("Content-Type", "application/json")
  681. w.Header().Set("Access-Control-Allow-Origin", "*")
  682. if !thanos.IsEnabled() {
  683. w.Write(WrapData(nil, fmt.Errorf("ThanosDisabled")))
  684. return
  685. }
  686. qp := httputil.NewQueryParams(r.URL.Query())
  687. query := qp.Get("query", "")
  688. if query == "" {
  689. w.Write(WrapData(nil, fmt.Errorf("Query Parameter 'query' is unset'")))
  690. return
  691. }
  692. // Attempt to parse time as either a unix timestamp or as an RFC3339 value
  693. var timeVal time.Time
  694. timeStr := qp.Get("time", "")
  695. if len(timeStr) > 0 {
  696. if t, err := strconv.ParseInt(timeStr, 10, 64); err == nil {
  697. timeVal = time.Unix(t, 0)
  698. } else if t, err := time.Parse(time.RFC3339, timeStr); err == nil {
  699. timeVal = t
  700. }
  701. // If time is given, but not parse-able, return an error
  702. if timeVal.IsZero() {
  703. http.Error(w, fmt.Sprintf("time must be a unix timestamp or RFC3339 value; illegal value given: %s", timeStr), http.StatusBadRequest)
  704. }
  705. }
  706. ctx := prom.NewNamedContext(a.ThanosClient, prom.FrontendContextName)
  707. body, err := ctx.RawQuery(query, timeVal)
  708. if err != nil {
  709. w.Write(WrapData(nil, fmt.Errorf("Error running query %s. Error: %s", query, err)))
  710. return
  711. }
  712. w.Write(body)
  713. }
  714. func (a *Accesses) ThanosQueryRange(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
  715. w.Header().Set("Content-Type", "application/json")
  716. w.Header().Set("Access-Control-Allow-Origin", "*")
  717. if !thanos.IsEnabled() {
  718. w.Write(WrapData(nil, fmt.Errorf("ThanosDisabled")))
  719. return
  720. }
  721. qp := httputil.NewQueryParams(r.URL.Query())
  722. query := qp.Get("query", "")
  723. if query == "" {
  724. fmt.Fprintf(w, "Error parsing query from request parameters.")
  725. return
  726. }
  727. start, end, duration, err := toStartEndStep(qp)
  728. if err != nil {
  729. fmt.Fprintf(w, err.Error())
  730. return
  731. }
  732. ctx := prom.NewNamedContext(a.ThanosClient, prom.FrontendContextName)
  733. body, err := ctx.RawQueryRange(query, start, end, duration)
  734. if err != nil {
  735. fmt.Fprintf(w, "Error running query %s. Error: %s", query, err)
  736. return
  737. }
  738. w.Write(body)
  739. }
  740. // helper for query range proxy requests
  741. func toStartEndStep(qp httputil.QueryParams) (start, end time.Time, step time.Duration, err error) {
  742. var e error
  743. ss := qp.Get("start", "")
  744. es := qp.Get("end", "")
  745. ds := qp.Get("duration", "")
  746. layout := "2006-01-02T15:04:05.000Z"
  747. start, e = time.Parse(layout, ss)
  748. if e != nil {
  749. err = fmt.Errorf("Error parsing time %s. Error: %s", ss, err)
  750. return
  751. }
  752. end, e = time.Parse(layout, es)
  753. if e != nil {
  754. err = fmt.Errorf("Error parsing time %s. Error: %s", es, err)
  755. return
  756. }
  757. step, e = time.ParseDuration(ds)
  758. if e != nil {
  759. err = fmt.Errorf("Error parsing duration %s. Error: %s", ds, err)
  760. return
  761. }
  762. err = nil
  763. return
  764. }
  765. func (a *Accesses) GetPrometheusQueueState(w http.ResponseWriter, _ *http.Request, _ httprouter.Params) {
  766. w.Header().Set("Content-Type", "application/json")
  767. w.Header().Set("Access-Control-Allow-Origin", "*")
  768. promQueueState, err := prom.GetPrometheusQueueState(a.PrometheusClient)
  769. if err != nil {
  770. w.Write(WrapData(nil, err))
  771. return
  772. }
  773. result := map[string]*prom.PrometheusQueueState{
  774. "prometheus": promQueueState,
  775. }
  776. if thanos.IsEnabled() {
  777. thanosQueueState, err := prom.GetPrometheusQueueState(a.ThanosClient)
  778. if err != nil {
  779. log.Warnf("Error getting Thanos queue state: %s", err)
  780. } else {
  781. result["thanos"] = thanosQueueState
  782. }
  783. }
  784. w.Write(WrapData(result, nil))
  785. }
  786. // GetPrometheusMetrics retrieves availability of Prometheus and Thanos metrics
  787. func (a *Accesses) GetPrometheusMetrics(w http.ResponseWriter, _ *http.Request, _ httprouter.Params) {
  788. w.Header().Set("Content-Type", "application/json")
  789. w.Header().Set("Access-Control-Allow-Origin", "*")
  790. promMetrics := prom.GetPrometheusMetrics(a.PrometheusClient, "")
  791. result := map[string][]*prom.PrometheusDiagnostic{
  792. "prometheus": promMetrics,
  793. }
  794. if thanos.IsEnabled() {
  795. thanosMetrics := prom.GetPrometheusMetrics(a.ThanosClient, thanos.QueryOffset())
  796. result["thanos"] = thanosMetrics
  797. }
  798. w.Write(WrapData(result, nil))
  799. }
  800. func (a *Accesses) GetAllPersistentVolumes(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
  801. w.Header().Set("Content-Type", "application/json")
  802. w.Header().Set("Access-Control-Allow-Origin", "*")
  803. pvList := a.ClusterCache.GetAllPersistentVolumes()
  804. body, err := json.Marshal(wrapAsObjectItems(pvList))
  805. if err != nil {
  806. fmt.Fprintf(w, "Error decoding persistent volumes: "+err.Error())
  807. } else {
  808. w.Write(body)
  809. }
  810. }
  811. func (a *Accesses) GetAllDeployments(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
  812. w.Header().Set("Content-Type", "application/json")
  813. w.Header().Set("Access-Control-Allow-Origin", "*")
  814. qp := httputil.NewQueryParams(r.URL.Query())
  815. namespace := qp.Get("namespace", "")
  816. deploymentsList := a.ClusterCache.GetAllDeployments()
  817. // filter for provided namespace
  818. var deployments []*appsv1.Deployment
  819. if namespace == "" {
  820. deployments = deploymentsList
  821. } else {
  822. deployments = []*appsv1.Deployment{}
  823. for _, d := range deploymentsList {
  824. if d.Namespace == namespace {
  825. deployments = append(deployments, d)
  826. }
  827. }
  828. }
  829. body, err := json.Marshal(wrapAsObjectItems(deployments))
  830. if err != nil {
  831. fmt.Fprintf(w, "Error decoding deployment: "+err.Error())
  832. } else {
  833. w.Write(body)
  834. }
  835. }
  836. func (a *Accesses) GetAllStorageClasses(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
  837. w.Header().Set("Content-Type", "application/json")
  838. w.Header().Set("Access-Control-Allow-Origin", "*")
  839. scList := a.ClusterCache.GetAllStorageClasses()
  840. body, err := json.Marshal(wrapAsObjectItems(scList))
  841. if err != nil {
  842. fmt.Fprintf(w, "Error decoding storageclasses: "+err.Error())
  843. } else {
  844. w.Write(body)
  845. }
  846. }
  847. func (a *Accesses) GetAllStatefulSets(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
  848. w.Header().Set("Content-Type", "application/json")
  849. w.Header().Set("Access-Control-Allow-Origin", "*")
  850. qp := httputil.NewQueryParams(r.URL.Query())
  851. namespace := qp.Get("namespace", "")
  852. statefulSetsList := a.ClusterCache.GetAllStatefulSets()
  853. // filter for provided namespace
  854. var statefulSets []*appsv1.StatefulSet
  855. if namespace == "" {
  856. statefulSets = statefulSetsList
  857. } else {
  858. statefulSets = []*appsv1.StatefulSet{}
  859. for _, ss := range statefulSetsList {
  860. if ss.Namespace == namespace {
  861. statefulSets = append(statefulSets, ss)
  862. }
  863. }
  864. }
  865. body, err := json.Marshal(wrapAsObjectItems(statefulSets))
  866. if err != nil {
  867. fmt.Fprintf(w, "Error decoding deployment: "+err.Error())
  868. } else {
  869. w.Write(body)
  870. }
  871. }
  872. func (a *Accesses) GetAllNodes(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
  873. w.Header().Set("Content-Type", "application/json")
  874. w.Header().Set("Access-Control-Allow-Origin", "*")
  875. nodeList := a.ClusterCache.GetAllNodes()
  876. body, err := json.Marshal(wrapAsObjectItems(nodeList))
  877. if err != nil {
  878. fmt.Fprintf(w, "Error decoding nodes: "+err.Error())
  879. } else {
  880. w.Write(body)
  881. }
  882. }
  883. func (a *Accesses) GetAllPods(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
  884. w.Header().Set("Content-Type", "application/json")
  885. w.Header().Set("Access-Control-Allow-Origin", "*")
  886. podlist := a.ClusterCache.GetAllPods()
  887. body, err := json.Marshal(wrapAsObjectItems(podlist))
  888. if err != nil {
  889. fmt.Fprintf(w, "Error decoding pods: "+err.Error())
  890. } else {
  891. w.Write(body)
  892. }
  893. }
  894. func (a *Accesses) GetAllNamespaces(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
  895. w.Header().Set("Content-Type", "application/json")
  896. w.Header().Set("Access-Control-Allow-Origin", "*")
  897. namespaces := a.ClusterCache.GetAllNamespaces()
  898. body, err := json.Marshal(wrapAsObjectItems(namespaces))
  899. if err != nil {
  900. fmt.Fprintf(w, "Error decoding deployment: "+err.Error())
  901. } else {
  902. w.Write(body)
  903. }
  904. }
  905. func (a *Accesses) GetAllDaemonSets(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
  906. w.Header().Set("Content-Type", "application/json")
  907. w.Header().Set("Access-Control-Allow-Origin", "*")
  908. daemonSets := a.ClusterCache.GetAllDaemonSets()
  909. body, err := json.Marshal(wrapAsObjectItems(daemonSets))
  910. if err != nil {
  911. fmt.Fprintf(w, "Error decoding daemon set: "+err.Error())
  912. } else {
  913. w.Write(body)
  914. }
  915. }
  916. func (a *Accesses) GetPod(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
  917. w.Header().Set("Content-Type", "application/json")
  918. w.Header().Set("Access-Control-Allow-Origin", "*")
  919. podName := ps.ByName("name")
  920. podNamespace := ps.ByName("namespace")
  921. // TODO: ClusterCache API could probably afford to have some better filtering
  922. allPods := a.ClusterCache.GetAllPods()
  923. for _, pod := range allPods {
  924. for _, container := range pod.Spec.Containers {
  925. container.Env = make([]v1.EnvVar, 0)
  926. }
  927. if pod.Namespace == podNamespace && pod.Name == podName {
  928. body, err := json.Marshal(pod)
  929. if err != nil {
  930. fmt.Fprintf(w, "Error decoding pod: "+err.Error())
  931. } else {
  932. w.Write(body)
  933. }
  934. return
  935. }
  936. }
  937. fmt.Fprintf(w, "Pod not found\n")
  938. }
  939. func (a *Accesses) PrometheusRecordingRules(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
  940. w.Header().Set("Content-Type", "application/json")
  941. w.Header().Set("Access-Control-Allow-Origin", "*")
  942. u := a.PrometheusClient.URL(epRules, nil)
  943. req, err := http.NewRequest(http.MethodGet, u.String(), nil)
  944. if err != nil {
  945. fmt.Fprintf(w, "Error creating Prometheus rule request: "+err.Error())
  946. }
  947. _, body, err := a.PrometheusClient.Do(r.Context(), req)
  948. if err != nil {
  949. fmt.Fprintf(w, "Error making Prometheus rule request: "+err.Error())
  950. } else {
  951. w.Write(body)
  952. }
  953. }
  954. func (a *Accesses) PrometheusConfig(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
  955. w.Header().Set("Content-Type", "application/json")
  956. w.Header().Set("Access-Control-Allow-Origin", "*")
  957. pConfig := map[string]string{
  958. "address": env.GetPrometheusServerEndpoint(),
  959. }
  960. body, err := json.Marshal(pConfig)
  961. if err != nil {
  962. fmt.Fprintf(w, "Error marshalling prometheus config")
  963. } else {
  964. w.Write(body)
  965. }
  966. }
  967. func (a *Accesses) PrometheusTargets(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
  968. w.Header().Set("Content-Type", "application/json")
  969. w.Header().Set("Access-Control-Allow-Origin", "*")
  970. u := a.PrometheusClient.URL(epTargets, nil)
  971. req, err := http.NewRequest(http.MethodGet, u.String(), nil)
  972. if err != nil {
  973. fmt.Fprintf(w, "Error creating Prometheus rule request: "+err.Error())
  974. }
  975. _, body, err := a.PrometheusClient.Do(r.Context(), req)
  976. if err != nil {
  977. fmt.Fprintf(w, "Error making Prometheus rule request: "+err.Error())
  978. } else {
  979. w.Write(body)
  980. }
  981. }
  982. func (a *Accesses) GetOrphanedPods(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
  983. w.Header().Set("Content-Type", "application/json")
  984. w.Header().Set("Access-Control-Allow-Origin", "*")
  985. podlist := a.ClusterCache.GetAllPods()
  986. var lonePods []*v1.Pod
  987. for _, pod := range podlist {
  988. if len(pod.OwnerReferences) == 0 {
  989. lonePods = append(lonePods, pod)
  990. }
  991. }
  992. body, err := json.Marshal(lonePods)
  993. if err != nil {
  994. fmt.Fprintf(w, "Error decoding pod: "+err.Error())
  995. } else {
  996. w.Write(body)
  997. }
  998. }
  999. func (a *Accesses) GetInstallNamespace(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
  1000. w.Header().Set("Content-Type", "application/json")
  1001. w.Header().Set("Access-Control-Allow-Origin", "*")
  1002. ns := env.GetKubecostNamespace()
  1003. w.Write([]byte(ns))
  1004. }
  1005. type InstallInfo struct {
  1006. Containers []ContainerInfo `json:"containers"`
  1007. ClusterInfo map[string]string `json:"clusterInfo"`
  1008. Version string `json:"version"`
  1009. }
  1010. type ContainerInfo struct {
  1011. ContainerName string `json:"containerName"`
  1012. Image string `json:"image"`
  1013. ImageID string `json:"imageID"`
  1014. StartTime string `json:"startTime"`
  1015. Restarts int32 `json:"restarts"`
  1016. }
  1017. func (a *Accesses) GetInstallInfo(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
  1018. w.Header().Set("Content-Type", "application/json")
  1019. w.Header().Set("Access-Control-Allow-Origin", "*")
  1020. pods, err := a.KubeClientSet.CoreV1().Pods(env.GetKubecostNamespace()).List(context.Background(), metav1.ListOptions{
  1021. LabelSelector: "app=cost-analyzer",
  1022. FieldSelector: "status.phase=Running",
  1023. Limit: 1,
  1024. })
  1025. if err != nil {
  1026. writeErrorResponse(w, 500, fmt.Sprintf("Unable to list pods: %s", err.Error()))
  1027. return
  1028. }
  1029. info := InstallInfo{
  1030. ClusterInfo: make(map[string]string),
  1031. Version: version.FriendlyVersion(),
  1032. }
  1033. // If we have zero pods either something is weird with the install since the app selector is not exposed in the helm
  1034. // chart or more likely we are running locally - in either case Images field will return as null
  1035. if len(pods.Items) > 0 {
  1036. for _, pod := range pods.Items {
  1037. for _, container := range pod.Status.ContainerStatuses {
  1038. c := ContainerInfo{
  1039. ContainerName: container.Name,
  1040. Image: container.Image,
  1041. ImageID: container.ImageID,
  1042. StartTime: pod.Status.StartTime.String(),
  1043. Restarts: container.RestartCount,
  1044. }
  1045. info.Containers = append(info.Containers, c)
  1046. }
  1047. }
  1048. }
  1049. nodes := a.ClusterCache.GetAllNodes()
  1050. cachePods := a.ClusterCache.GetAllPods()
  1051. info.ClusterInfo["nodeCount"] = strconv.Itoa(len(nodes))
  1052. info.ClusterInfo["podCount"] = strconv.Itoa(len(cachePods))
  1053. body, err := json.Marshal(info)
  1054. if err != nil {
  1055. writeErrorResponse(w, 500, fmt.Sprintf("Error decoding pod: %s", err.Error()))
  1056. return
  1057. }
  1058. w.Write(body)
  1059. }
  1060. // logsFor pulls the logs for a specific pod, namespace, and container
  1061. func logsFor(c kubernetes.Interface, namespace string, pod string, container string, dur time.Duration, ctx context.Context) (string, error) {
  1062. since := time.Now().UTC().Add(-dur)
  1063. logOpts := v1.PodLogOptions{
  1064. SinceTime: &metav1.Time{Time: since},
  1065. }
  1066. if container != "" {
  1067. logOpts.Container = container
  1068. }
  1069. req := c.CoreV1().Pods(namespace).GetLogs(pod, &logOpts)
  1070. reader, err := req.Stream(ctx)
  1071. if err != nil {
  1072. return "", err
  1073. }
  1074. podLogs, err := io.ReadAll(reader)
  1075. if err != nil {
  1076. return "", err
  1077. }
  1078. // If color is already disabled then we don't need to process the logs
  1079. // to drop ANSI colors
  1080. if !viper.GetBool("disable-log-color") {
  1081. podLogs = ANSIRegex.ReplaceAll(podLogs, []byte{})
  1082. }
  1083. return string(podLogs), nil
  1084. }
  1085. func (a *Accesses) GetPodLogs(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
  1086. w.Header().Set("Content-Type", "application/json")
  1087. w.Header().Set("Access-Control-Allow-Origin", "*")
  1088. qp := httputil.NewQueryParams(r.URL.Query())
  1089. ns := qp.Get("namespace", env.GetKubecostNamespace())
  1090. pod := qp.Get("pod", "")
  1091. selector := qp.Get("selector", "")
  1092. container := qp.Get("container", "")
  1093. since := qp.Get("since", "24h")
  1094. sinceDuration, err := time.ParseDuration(since)
  1095. if err != nil {
  1096. fmt.Fprintf(w, "Invalid Duration String: "+err.Error())
  1097. return
  1098. }
  1099. var logResult string
  1100. appendLog := func(ns string, pod string, container string, l string) {
  1101. if l == "" {
  1102. return
  1103. }
  1104. logResult += fmt.Sprintf("%s\n| %s:%s:%s\n%s\n%s\n\n", LogSeparator, ns, pod, container, LogSeparator, l)
  1105. }
  1106. if pod != "" {
  1107. pd, err := a.KubeClientSet.CoreV1().Pods(ns).Get(r.Context(), pod, metav1.GetOptions{})
  1108. if err != nil {
  1109. fmt.Fprintf(w, "Error Finding Pod: "+err.Error())
  1110. return
  1111. }
  1112. if container != "" {
  1113. var foundContainer bool
  1114. for _, cont := range pd.Spec.Containers {
  1115. if strings.EqualFold(cont.Name, container) {
  1116. foundContainer = true
  1117. break
  1118. }
  1119. }
  1120. if !foundContainer {
  1121. fmt.Fprintf(w, "Could not find container: "+container)
  1122. return
  1123. }
  1124. }
  1125. logs, err := logsFor(a.KubeClientSet, ns, pod, container, sinceDuration, r.Context())
  1126. if err != nil {
  1127. fmt.Fprintf(w, "Error Getting Logs: "+err.Error())
  1128. return
  1129. }
  1130. appendLog(ns, pod, container, logs)
  1131. w.Write([]byte(logResult))
  1132. return
  1133. }
  1134. if selector != "" {
  1135. pods, err := a.KubeClientSet.CoreV1().Pods(ns).List(r.Context(), metav1.ListOptions{LabelSelector: selector})
  1136. if err != nil {
  1137. fmt.Fprintf(w, "Error Finding Pod: "+err.Error())
  1138. return
  1139. }
  1140. for _, pd := range pods.Items {
  1141. for _, cont := range pd.Spec.Containers {
  1142. logs, err := logsFor(a.KubeClientSet, ns, pd.Name, cont.Name, sinceDuration, r.Context())
  1143. if err != nil {
  1144. continue
  1145. }
  1146. appendLog(ns, pd.Name, cont.Name, logs)
  1147. }
  1148. }
  1149. }
  1150. w.Write([]byte(logResult))
  1151. }
  1152. func (a *Accesses) AddServiceKey(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
  1153. w.Header().Set("Content-Type", "application/json")
  1154. w.Header().Set("Access-Control-Allow-Origin", "*")
  1155. r.ParseForm()
  1156. key := r.PostForm.Get("key")
  1157. k := []byte(key)
  1158. err := os.WriteFile(path.Join(env.GetConfigPathWithDefault(env.DefaultConfigMountPath), "key.json"), k, 0644)
  1159. if err != nil {
  1160. fmt.Fprintf(w, "Error writing service key: "+err.Error())
  1161. }
  1162. w.WriteHeader(http.StatusOK)
  1163. }
  1164. func (a *Accesses) GetHelmValues(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
  1165. w.Header().Set("Content-Type", "application/json")
  1166. w.Header().Set("Access-Control-Allow-Origin", "*")
  1167. encodedValues := sysenv.Get("HELM_VALUES", "")
  1168. if encodedValues == "" {
  1169. fmt.Fprintf(w, "Values reporting disabled")
  1170. return
  1171. }
  1172. result, err := base64.StdEncoding.DecodeString(encodedValues)
  1173. if err != nil {
  1174. fmt.Fprintf(w, "Failed to decode encoded values: %s", err)
  1175. return
  1176. }
  1177. w.Write(result)
  1178. }
  1179. func (a *Accesses) Status(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
  1180. w.Header().Set("Content-Type", "application/json")
  1181. w.Header().Set("Access-Control-Allow-Origin", "*")
  1182. promServer := env.GetPrometheusServerEndpoint()
  1183. api := prometheusAPI.NewAPI(a.PrometheusClient)
  1184. result, err := api.Buildinfo(r.Context())
  1185. if err != nil {
  1186. fmt.Fprintf(w, "Using Prometheus at "+promServer+". Error: "+err.Error())
  1187. } else {
  1188. fmt.Fprintf(w, "Using Prometheus at "+promServer+". Version: "+result.Version)
  1189. }
  1190. }
  1191. type LogLevelRequestResponse struct {
  1192. Level string `json:"level"`
  1193. }
  1194. func (a *Accesses) GetLogLevel(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
  1195. w.Header().Set("Content-Type", "application/json")
  1196. w.Header().Set("Access-Control-Allow-Origin", "*")
  1197. level := log.GetLogLevel()
  1198. llrr := LogLevelRequestResponse{
  1199. Level: level,
  1200. }
  1201. body, err := json.Marshal(llrr)
  1202. if err != nil {
  1203. http.Error(w, fmt.Sprintf("unable to retrive log level"), http.StatusInternalServerError)
  1204. return
  1205. }
  1206. _, err = w.Write(body)
  1207. if err != nil {
  1208. http.Error(w, fmt.Sprintf("unable to write response: %s", body), http.StatusInternalServerError)
  1209. return
  1210. }
  1211. }
  1212. func (a *Accesses) SetLogLevel(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
  1213. params := LogLevelRequestResponse{}
  1214. err := json.NewDecoder(r.Body).Decode(&params)
  1215. if err != nil {
  1216. http.Error(w, fmt.Sprintf("unable to decode request body, error: %s", err), http.StatusBadRequest)
  1217. return
  1218. }
  1219. err = log.SetLogLevel(params.Level)
  1220. if err != nil {
  1221. http.Error(w, fmt.Sprintf("level must be a valid log level according to zerolog; level given: %s, error: %s", params.Level, err), http.StatusBadRequest)
  1222. return
  1223. }
  1224. w.WriteHeader(http.StatusOK)
  1225. }
  1226. // captures the panic event in sentry
  1227. func capturePanicEvent(err string, stack string) {
  1228. msg := fmt.Sprintf("Panic: %s\nStackTrace: %s\n", err, stack)
  1229. log.Infof(msg)
  1230. sentry.CurrentHub().CaptureEvent(&sentry.Event{
  1231. Level: sentry.LevelError,
  1232. Message: msg,
  1233. })
  1234. sentry.Flush(5 * time.Second)
  1235. }
  1236. // handle any panics reported by the errors package
  1237. func handlePanic(p errors.Panic) bool {
  1238. err := p.Error
  1239. if err != nil {
  1240. if err, ok := err.(error); ok {
  1241. capturePanicEvent(err.Error(), p.Stack)
  1242. }
  1243. if err, ok := err.(string); ok {
  1244. capturePanicEvent(err, p.Stack)
  1245. }
  1246. }
  1247. // Return true to recover iff the type is http, otherwise allow kubernetes
  1248. // to recover.
  1249. return p.Type == errors.PanicTypeHTTP
  1250. }
  1251. func Initialize(additionalConfigWatchers ...*watcher.ConfigMapWatcher) *Accesses {
  1252. configWatchers := watcher.NewConfigMapWatchers(additionalConfigWatchers...)
  1253. var err error
  1254. if errorReportingEnabled {
  1255. err = sentry.Init(sentry.ClientOptions{Release: version.FriendlyVersion()})
  1256. if err != nil {
  1257. log.Infof("Failed to initialize sentry for error reporting")
  1258. } else {
  1259. err = errors.SetPanicHandler(handlePanic)
  1260. if err != nil {
  1261. log.Infof("Failed to set panic handler: %s", err)
  1262. }
  1263. }
  1264. }
  1265. address := env.GetPrometheusServerEndpoint()
  1266. if address == "" {
  1267. log.Fatalf("No address for prometheus set in $%s. Aborting.", env.PrometheusServerEndpointEnvVar)
  1268. }
  1269. queryConcurrency := env.GetMaxQueryConcurrency()
  1270. log.Infof("Prometheus/Thanos Client Max Concurrency set to %d", queryConcurrency)
  1271. timeout := 120 * time.Second
  1272. keepAlive := 120 * time.Second
  1273. tlsHandshakeTimeout := 10 * time.Second
  1274. scrapeInterval := env.GetKubecostScrapeInterval()
  1275. var rateLimitRetryOpts *prom.RateLimitRetryOpts = nil
  1276. if env.IsPrometheusRetryOnRateLimitResponse() {
  1277. rateLimitRetryOpts = &prom.RateLimitRetryOpts{
  1278. MaxRetries: env.GetPrometheusRetryOnRateLimitMaxRetries(),
  1279. DefaultRetryWait: env.GetPrometheusRetryOnRateLimitDefaultWait(),
  1280. }
  1281. }
  1282. promCli, err := prom.NewPrometheusClient(address, &prom.PrometheusClientConfig{
  1283. Timeout: timeout,
  1284. KeepAlive: keepAlive,
  1285. TLSHandshakeTimeout: tlsHandshakeTimeout,
  1286. TLSInsecureSkipVerify: env.GetInsecureSkipVerify(),
  1287. RateLimitRetryOpts: rateLimitRetryOpts,
  1288. Auth: &prom.ClientAuth{
  1289. Username: env.GetDBBasicAuthUsername(),
  1290. Password: env.GetDBBasicAuthUserPassword(),
  1291. BearerToken: env.GetDBBearerToken(),
  1292. },
  1293. QueryConcurrency: queryConcurrency,
  1294. QueryLogFile: "",
  1295. HeaderXScopeOrgId: env.GetPrometheusHeaderXScopeOrgId(),
  1296. })
  1297. if err != nil {
  1298. log.Fatalf("Failed to create prometheus client, Error: %v", err)
  1299. }
  1300. m, err := prom.Validate(promCli)
  1301. if err != nil || !m.Running {
  1302. if err != nil {
  1303. log.Errorf("Failed to query prometheus at %s. Error: %s . Troubleshooting help available at: %s", address, err.Error(), prom.PrometheusTroubleshootingURL)
  1304. } else if !m.Running {
  1305. log.Errorf("Prometheus at %s is not running. Troubleshooting help available at: %s", address, prom.PrometheusTroubleshootingURL)
  1306. }
  1307. } else {
  1308. log.Infof("Success: retrieved the 'up' query against prometheus at: " + address)
  1309. }
  1310. api := prometheusAPI.NewAPI(promCli)
  1311. _, err = api.Buildinfo(context.Background())
  1312. if err != nil {
  1313. log.Infof("No valid prometheus config file at %s. Error: %s . Troubleshooting help available at: %s. Ignore if using cortex/mimir/thanos here.", address, err.Error(), prom.PrometheusTroubleshootingURL)
  1314. } else {
  1315. log.Infof("Retrieved a prometheus config file from: %s", address)
  1316. }
  1317. if scrapeInterval == 0 {
  1318. scrapeInterval = time.Minute
  1319. // Lookup scrape interval for kubecost job, update if found
  1320. si, err := prom.ScrapeIntervalFor(promCli, env.GetKubecostJobName())
  1321. if err == nil {
  1322. scrapeInterval = si
  1323. }
  1324. }
  1325. log.Infof("Using scrape interval of %f", scrapeInterval.Seconds())
  1326. // Kubernetes API setup
  1327. kubeClientset, err := kubeconfig.LoadKubeClient("")
  1328. if err != nil {
  1329. log.Fatalf("Failed to build Kubernetes client: %s", err.Error())
  1330. }
  1331. // Create ConfigFileManager for synchronization of shared configuration
  1332. confManager := config.NewConfigFileManager(&config.ConfigFileManagerOpts{
  1333. BucketStoreConfig: env.GetKubecostConfigBucket(),
  1334. LocalConfigPath: "/",
  1335. })
  1336. configPrefix := env.GetConfigPathWithDefault("/var/configs/")
  1337. // Create Kubernetes Cluster Cache + Watchers
  1338. var k8sCache clustercache.ClusterCache
  1339. if env.IsClusterCacheFileEnabled() {
  1340. importLocation := confManager.ConfigFileAt(path.Join(configPrefix, "cluster-cache.json"))
  1341. k8sCache = clustercache.NewClusterImporter(importLocation)
  1342. } else {
  1343. k8sCache = clustercache.NewKubernetesClusterCache(kubeClientset)
  1344. }
  1345. k8sCache.Run()
  1346. cloudProviderKey := env.GetCloudProviderAPIKey()
  1347. cloudProvider, err := provider.NewProvider(k8sCache, cloudProviderKey, confManager)
  1348. if err != nil {
  1349. panic(err.Error())
  1350. }
  1351. // Append the pricing config watcher
  1352. configWatchers.AddWatcher(provider.ConfigWatcherFor(cloudProvider))
  1353. configWatchers.AddWatcher(metrics.GetMetricsConfigWatcher())
  1354. watchConfigFunc := configWatchers.ToWatchFunc()
  1355. watchedConfigs := configWatchers.GetWatchedConfigs()
  1356. kubecostNamespace := env.GetKubecostNamespace()
  1357. // We need an initial invocation because the init of the cache has happened before we had access to the provider.
  1358. for _, cw := range watchedConfigs {
  1359. configs, err := kubeClientset.CoreV1().ConfigMaps(kubecostNamespace).Get(context.Background(), cw, metav1.GetOptions{})
  1360. if err != nil {
  1361. log.Infof("No %s configmap found at install time, using existing configs: %s", cw, err.Error())
  1362. } else {
  1363. log.Infof("Found configmap %s, watching...", configs.Name)
  1364. watchConfigFunc(configs)
  1365. }
  1366. }
  1367. k8sCache.SetConfigMapUpdateFunc(watchConfigFunc)
  1368. remoteEnabled := env.IsRemoteEnabled()
  1369. if remoteEnabled {
  1370. info, err := cloudProvider.ClusterInfo()
  1371. log.Infof("Saving cluster with id:'%s', and name:'%s' to durable storage", info["id"], info["name"])
  1372. if err != nil {
  1373. log.Infof("Error saving cluster id %s", err.Error())
  1374. }
  1375. _, _, err = utils.GetOrCreateClusterMeta(info["id"], info["name"])
  1376. if err != nil {
  1377. log.Infof("Unable to set cluster id '%s' for cluster '%s', %s", info["id"], info["name"], err.Error())
  1378. }
  1379. }
  1380. // Thanos Client
  1381. var thanosClient prometheus.Client
  1382. if thanos.IsEnabled() {
  1383. thanosAddress := thanos.QueryURL()
  1384. if thanosAddress != "" {
  1385. thanosCli, _ := thanos.NewThanosClient(thanosAddress, &prom.PrometheusClientConfig{
  1386. Timeout: timeout,
  1387. KeepAlive: keepAlive,
  1388. TLSHandshakeTimeout: tlsHandshakeTimeout,
  1389. TLSInsecureSkipVerify: env.GetInsecureSkipVerify(),
  1390. RateLimitRetryOpts: rateLimitRetryOpts,
  1391. Auth: &prom.ClientAuth{
  1392. Username: env.GetMultiClusterBasicAuthUsername(),
  1393. Password: env.GetMultiClusterBasicAuthPassword(),
  1394. BearerToken: env.GetMultiClusterBearerToken(),
  1395. },
  1396. QueryConcurrency: queryConcurrency,
  1397. QueryLogFile: env.GetQueryLoggingFile(),
  1398. })
  1399. _, err = prom.Validate(thanosCli)
  1400. if err != nil {
  1401. log.Warnf("Failed to query Thanos at %s. Error: %s.", thanosAddress, err.Error())
  1402. thanosClient = thanosCli
  1403. } else {
  1404. log.Infof("Success: retrieved the 'up' query against Thanos at: " + thanosAddress)
  1405. thanosClient = thanosCli
  1406. }
  1407. } else {
  1408. log.Infof("Error resolving environment variable: $%s", env.ThanosQueryUrlEnvVar)
  1409. }
  1410. }
  1411. // ClusterInfo Provider to provide the cluster map with local and remote cluster data
  1412. var clusterInfoProvider clusters.ClusterInfoProvider
  1413. if env.IsClusterInfoFileEnabled() {
  1414. clusterInfoFile := confManager.ConfigFileAt(path.Join(configPrefix, "cluster-info.json"))
  1415. clusterInfoProvider = NewConfiguredClusterInfoProvider(clusterInfoFile)
  1416. } else {
  1417. clusterInfoProvider = NewLocalClusterInfoProvider(kubeClientset, cloudProvider)
  1418. }
  1419. // Initialize ClusterMap for maintaining ClusterInfo by ClusterID
  1420. var clusterMap clusters.ClusterMap
  1421. if thanosClient != nil {
  1422. clusterMap = clustermap.NewClusterMap(thanosClient, clusterInfoProvider, 10*time.Minute)
  1423. } else {
  1424. clusterMap = clustermap.NewClusterMap(promCli, clusterInfoProvider, 5*time.Minute)
  1425. }
  1426. // cache responses from model and aggregation for a default of 10 minutes;
  1427. // clear expired responses every 20 minutes
  1428. aggregateCache := cache.New(time.Minute*10, time.Minute*20)
  1429. costDataCache := cache.New(time.Minute*10, time.Minute*20)
  1430. clusterCostsCache := cache.New(cache.NoExpiration, cache.NoExpiration)
  1431. outOfClusterCache := cache.New(time.Minute*5, time.Minute*10)
  1432. settingsCache := cache.New(cache.NoExpiration, cache.NoExpiration)
  1433. // query durations that should be cached longer should be registered here
  1434. // use relatively prime numbers to minimize likelihood of synchronized
  1435. // attempts at cache warming
  1436. day := 24 * time.Hour
  1437. cacheExpiration := map[time.Duration]time.Duration{
  1438. day: maxCacheMinutes1d * time.Minute,
  1439. 2 * day: maxCacheMinutes2d * time.Minute,
  1440. 7 * day: maxCacheMinutes7d * time.Minute,
  1441. 30 * day: maxCacheMinutes30d * time.Minute,
  1442. }
  1443. var pc prometheus.Client
  1444. if thanosClient != nil {
  1445. pc = thanosClient
  1446. } else {
  1447. pc = promCli
  1448. }
  1449. costModel := NewCostModel(pc, cloudProvider, k8sCache, clusterMap, scrapeInterval)
  1450. metricsEmitter := NewCostModelMetricsEmitter(promCli, k8sCache, cloudProvider, clusterInfoProvider, costModel)
  1451. a := &Accesses{
  1452. Router: httprouter.New(),
  1453. PrometheusClient: promCli,
  1454. ThanosClient: thanosClient,
  1455. KubeClientSet: kubeClientset,
  1456. ClusterCache: k8sCache,
  1457. ClusterMap: clusterMap,
  1458. CloudProvider: cloudProvider,
  1459. CloudConfigController: cloudconfig.NewController(cloudProvider),
  1460. ConfigFileManager: confManager,
  1461. ClusterInfoProvider: clusterInfoProvider,
  1462. Model: costModel,
  1463. MetricsEmitter: metricsEmitter,
  1464. AggregateCache: aggregateCache,
  1465. CostDataCache: costDataCache,
  1466. ClusterCostsCache: clusterCostsCache,
  1467. OutOfClusterCache: outOfClusterCache,
  1468. SettingsCache: settingsCache,
  1469. CacheExpiration: cacheExpiration,
  1470. httpServices: services.NewCostModelServices(),
  1471. }
  1472. // Use the Accesses instance, itself, as the CostModelAggregator. This is
  1473. // confusing and unconventional, but necessary so that we can swap it
  1474. // out for the ETL-adapted version elsewhere.
  1475. // TODO clean this up once ETL is open-sourced.
  1476. a.AggAPI = a
  1477. // Initialize mechanism for subscribing to settings changes
  1478. a.InitializeSettingsPubSub()
  1479. err = a.CloudProvider.DownloadPricingData()
  1480. if err != nil {
  1481. log.Infof("Failed to download pricing data: " + err.Error())
  1482. }
  1483. // Warm the aggregate cache unless explicitly set to false
  1484. if env.IsCacheWarmingEnabled() {
  1485. log.Infof("Init: AggregateCostModel cache warming enabled")
  1486. a.warmAggregateCostModelCache()
  1487. } else {
  1488. log.Infof("Init: AggregateCostModel cache warming disabled")
  1489. }
  1490. if !env.IsKubecostMetricsPodEnabled() {
  1491. a.MetricsEmitter.Start()
  1492. }
  1493. a.Router.GET("/costDataModel", a.CostDataModel)
  1494. a.Router.GET("/costDataModelRange", a.CostDataModelRange)
  1495. a.Router.GET("/aggregatedCostModel", a.AggregateCostModelHandler)
  1496. a.Router.GET("/allocation/compute", a.ComputeAllocationHandler)
  1497. a.Router.GET("/allocation/compute/summary", a.ComputeAllocationHandlerSummary)
  1498. a.Router.GET("/allNodePricing", a.GetAllNodePricing)
  1499. a.Router.POST("/refreshPricing", a.RefreshPricingData)
  1500. a.Router.GET("/clusterCostsOverTime", a.ClusterCostsOverTime)
  1501. a.Router.GET("/clusterCosts", a.ClusterCosts)
  1502. a.Router.GET("/clusterCostsFromCache", a.ClusterCostsFromCacheHandler)
  1503. a.Router.GET("/validatePrometheus", a.GetPrometheusMetadata)
  1504. a.Router.GET("/managementPlatform", a.ManagementPlatform)
  1505. a.Router.GET("/clusterInfo", a.ClusterInfo)
  1506. a.Router.GET("/clusterInfoMap", a.GetClusterInfoMap)
  1507. a.Router.GET("/serviceAccountStatus", a.GetServiceAccountStatus)
  1508. a.Router.GET("/pricingSourceStatus", a.GetPricingSourceStatus)
  1509. a.Router.GET("/pricingSourceSummary", a.GetPricingSourceSummary)
  1510. a.Router.GET("/pricingSourceCounts", a.GetPricingSourceCounts)
  1511. // endpoints migrated from server
  1512. a.Router.GET("/allPersistentVolumes", a.GetAllPersistentVolumes)
  1513. a.Router.GET("/allDeployments", a.GetAllDeployments)
  1514. a.Router.GET("/allStorageClasses", a.GetAllStorageClasses)
  1515. a.Router.GET("/allStatefulSets", a.GetAllStatefulSets)
  1516. a.Router.GET("/allNodes", a.GetAllNodes)
  1517. a.Router.GET("/allPods", a.GetAllPods)
  1518. a.Router.GET("/allNamespaces", a.GetAllNamespaces)
  1519. a.Router.GET("/allDaemonSets", a.GetAllDaemonSets)
  1520. a.Router.GET("/pod/:namespace/:name", a.GetPod)
  1521. a.Router.GET("/prometheusRecordingRules", a.PrometheusRecordingRules)
  1522. a.Router.GET("/prometheusConfig", a.PrometheusConfig)
  1523. a.Router.GET("/prometheusTargets", a.PrometheusTargets)
  1524. a.Router.GET("/orphanedPods", a.GetOrphanedPods)
  1525. a.Router.GET("/installNamespace", a.GetInstallNamespace)
  1526. a.Router.GET("/installInfo", a.GetInstallInfo)
  1527. a.Router.GET("/podLogs", a.GetPodLogs)
  1528. a.Router.POST("/serviceKey", a.AddServiceKey)
  1529. a.Router.GET("/helmValues", a.GetHelmValues)
  1530. a.Router.GET("/status", a.Status)
  1531. // prom query proxies
  1532. a.Router.GET("/prometheusQuery", a.PrometheusQuery)
  1533. a.Router.GET("/prometheusQueryRange", a.PrometheusQueryRange)
  1534. a.Router.GET("/thanosQuery", a.ThanosQuery)
  1535. a.Router.GET("/thanosQueryRange", a.ThanosQueryRange)
  1536. // diagnostics
  1537. a.Router.GET("/diagnostics/requestQueue", a.GetPrometheusQueueState)
  1538. a.Router.GET("/diagnostics/prometheusMetrics", a.GetPrometheusMetrics)
  1539. a.Router.GET("/logs/level", a.GetLogLevel)
  1540. a.Router.POST("/logs/level", a.SetLogLevel)
  1541. a.Router.GET("/cloud/config/export", a.CloudConfigController.GetExportConfigHandler())
  1542. a.Router.GET("/cloud/config/enable", a.CloudConfigController.GetEnableConfigHandler())
  1543. a.Router.GET("/cloud/config/disable", a.CloudConfigController.GetDisableConfigHandler())
  1544. a.Router.GET("/cloud/config/delete", a.CloudConfigController.GetDeleteConfigHandler())
  1545. a.httpServices.RegisterAll(a.Router)
  1546. return a
  1547. }
  1548. func InitializeWithoutKubernetes() *Accesses {
  1549. var err error
  1550. if errorReportingEnabled {
  1551. err = sentry.Init(sentry.ClientOptions{Release: version.FriendlyVersion()})
  1552. if err != nil {
  1553. log.Infof("Failed to initialize sentry for error reporting")
  1554. } else {
  1555. err = errors.SetPanicHandler(handlePanic)
  1556. if err != nil {
  1557. log.Infof("Failed to set panic handler: %s", err)
  1558. }
  1559. }
  1560. }
  1561. a := &Accesses{
  1562. Router: httprouter.New(),
  1563. CloudConfigController: cloudconfig.NewController(nil),
  1564. httpServices: services.NewCostModelServices(),
  1565. }
  1566. a.Router.GET("/logs/level", a.GetLogLevel)
  1567. a.Router.POST("/logs/level", a.SetLogLevel)
  1568. a.httpServices.RegisterAll(a.Router)
  1569. return a
  1570. }
  1571. func writeErrorResponse(w http.ResponseWriter, code int, message string) {
  1572. out := map[string]string{
  1573. "message": message,
  1574. }
  1575. bytes, err := json.Marshal(out)
  1576. if err != nil {
  1577. w.Header().Set("Content-Type", "text/plain")
  1578. w.WriteHeader(500)
  1579. fmt.Fprint(w, "unable to marshall json for error")
  1580. log.Warnf("Failed to marshall JSON for error response: %s", err.Error())
  1581. return
  1582. }
  1583. w.WriteHeader(code)
  1584. fmt.Fprint(w, string(bytes))
  1585. }