router.go 57 KB

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