router.go 53 KB

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