k8s_handler.go 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688
  1. package api
  2. import (
  3. "encoding/json"
  4. "fmt"
  5. "net/http"
  6. "net/url"
  7. "github.com/go-chi/chi"
  8. "github.com/gorilla/schema"
  9. "github.com/gorilla/websocket"
  10. "github.com/porter-dev/porter/internal/forms"
  11. "github.com/porter-dev/porter/internal/kubernetes"
  12. "github.com/porter-dev/porter/internal/kubernetes/prometheus"
  13. v1 "k8s.io/api/core/v1"
  14. "k8s.io/client-go/tools/clientcmd"
  15. )
  16. // Enumeration of k8s API error codes, represented as int64
  17. const (
  18. ErrK8sDecode ErrorCode = iota + 600
  19. ErrK8sValidate
  20. )
  21. var upgrader = websocket.Upgrader{
  22. ReadBufferSize: 1024,
  23. WriteBufferSize: 1024,
  24. }
  25. // HandleListNamespaces retrieves a list of namespaces
  26. func (app *App) HandleListNamespaces(w http.ResponseWriter, r *http.Request) {
  27. vals, err := url.ParseQuery(r.URL.RawQuery)
  28. if err != nil {
  29. app.handleErrorFormDecoding(err, ErrReleaseDecode, w)
  30. return
  31. }
  32. // get the filter options
  33. form := &forms.K8sForm{
  34. OutOfClusterConfig: &kubernetes.OutOfClusterConfig{
  35. Repo: app.Repo,
  36. DigitalOceanOAuth: app.DOConf,
  37. },
  38. }
  39. form.PopulateK8sOptionsFromQueryParams(vals, app.Repo.Cluster)
  40. // validate the form
  41. if err := app.validator.Struct(form); err != nil {
  42. app.handleErrorFormValidation(err, ErrK8sValidate, w)
  43. return
  44. }
  45. // create a new agent
  46. var agent *kubernetes.Agent
  47. if app.ServerConf.IsTesting {
  48. agent = app.TestAgents.K8sAgent
  49. } else {
  50. agent, err = kubernetes.GetAgentOutOfClusterConfig(form.OutOfClusterConfig)
  51. }
  52. namespaces, err := agent.ListNamespaces()
  53. if err != nil {
  54. app.handleErrorDataRead(err, w)
  55. return
  56. }
  57. if err := json.NewEncoder(w).Encode(namespaces); err != nil {
  58. app.handleErrorFormDecoding(err, ErrK8sDecode, w)
  59. return
  60. }
  61. }
  62. // HandleGetPodLogs returns real-time logs of the pod via websockets
  63. // TODO: Refactor repeated calls.
  64. func (app *App) HandleGetPodLogs(w http.ResponseWriter, r *http.Request) {
  65. // get session to retrieve correct kubeconfig
  66. _, err := app.Store.Get(r, app.ServerConf.CookieName)
  67. // get path parameters
  68. namespace := chi.URLParam(r, "namespace")
  69. podName := chi.URLParam(r, "name")
  70. if err != nil {
  71. app.handleErrorFormDecoding(err, ErrReleaseDecode, w)
  72. return
  73. }
  74. vals, err := url.ParseQuery(r.URL.RawQuery)
  75. if err != nil {
  76. app.handleErrorFormDecoding(err, ErrReleaseDecode, w)
  77. return
  78. }
  79. // get the filter options
  80. form := &forms.K8sForm{
  81. OutOfClusterConfig: &kubernetes.OutOfClusterConfig{
  82. Repo: app.Repo,
  83. DigitalOceanOAuth: app.DOConf,
  84. },
  85. }
  86. form.PopulateK8sOptionsFromQueryParams(vals, app.Repo.Cluster)
  87. // validate the form
  88. if err := app.validator.Struct(form); err != nil {
  89. app.handleErrorFormValidation(err, ErrK8sValidate, w)
  90. return
  91. }
  92. // create a new agent
  93. var agent *kubernetes.Agent
  94. if app.ServerConf.IsTesting {
  95. agent = app.TestAgents.K8sAgent
  96. } else {
  97. agent, err = kubernetes.GetAgentOutOfClusterConfig(form.OutOfClusterConfig)
  98. }
  99. upgrader.CheckOrigin = func(r *http.Request) bool { return true }
  100. // upgrade to websocket.
  101. conn, err := upgrader.Upgrade(w, r, nil)
  102. if err != nil {
  103. app.handleErrorUpgradeWebsocket(err, w)
  104. }
  105. err = agent.GetPodLogs(namespace, podName, conn)
  106. if err != nil {
  107. app.handleErrorWebsocketWrite(err, w)
  108. return
  109. }
  110. }
  111. // HandleDeletePod deletes the pod given the name and namespace.
  112. func (app *App) HandleDeletePod(w http.ResponseWriter, r *http.Request) {
  113. // get path parameters
  114. namespace := chi.URLParam(r, "namespace")
  115. name := chi.URLParam(r, "name")
  116. vals, err := url.ParseQuery(r.URL.RawQuery)
  117. if err != nil {
  118. app.handleErrorFormDecoding(err, ErrReleaseDecode, w)
  119. return
  120. }
  121. // get the filter options
  122. form := &forms.K8sForm{
  123. OutOfClusterConfig: &kubernetes.OutOfClusterConfig{
  124. Repo: app.Repo,
  125. DigitalOceanOAuth: app.DOConf,
  126. },
  127. }
  128. form.PopulateK8sOptionsFromQueryParams(vals, app.Repo.Cluster)
  129. // validate the form
  130. if err := app.validator.Struct(form); err != nil {
  131. app.handleErrorFormValidation(err, ErrK8sValidate, w)
  132. return
  133. }
  134. // create a new agent
  135. var agent *kubernetes.Agent
  136. if app.ServerConf.IsTesting {
  137. agent = app.TestAgents.K8sAgent
  138. } else {
  139. agent, err = kubernetes.GetAgentOutOfClusterConfig(form.OutOfClusterConfig)
  140. }
  141. err = agent.DeletePod(namespace, name)
  142. if err != nil {
  143. app.handleErrorInternal(err, w)
  144. return
  145. }
  146. w.WriteHeader(http.StatusOK)
  147. return
  148. }
  149. // HandleGetIngress returns the ingress object given the name and namespace.
  150. func (app *App) HandleGetIngress(w http.ResponseWriter, r *http.Request) {
  151. // get session to retrieve correct kubeconfig
  152. _, err := app.Store.Get(r, app.ServerConf.CookieName)
  153. // get path parameters
  154. namespace := chi.URLParam(r, "namespace")
  155. name := chi.URLParam(r, "name")
  156. if err != nil {
  157. app.handleErrorFormDecoding(err, ErrReleaseDecode, w)
  158. return
  159. }
  160. vals, err := url.ParseQuery(r.URL.RawQuery)
  161. if err != nil {
  162. app.handleErrorFormDecoding(err, ErrReleaseDecode, w)
  163. return
  164. }
  165. // get the filter options
  166. form := &forms.K8sForm{
  167. OutOfClusterConfig: &kubernetes.OutOfClusterConfig{
  168. Repo: app.Repo,
  169. DigitalOceanOAuth: app.DOConf,
  170. },
  171. }
  172. form.PopulateK8sOptionsFromQueryParams(vals, app.Repo.Cluster)
  173. // validate the form
  174. if err := app.validator.Struct(form); err != nil {
  175. app.handleErrorFormValidation(err, ErrK8sValidate, w)
  176. return
  177. }
  178. // create a new agent
  179. var agent *kubernetes.Agent
  180. if app.ServerConf.IsTesting {
  181. agent = app.TestAgents.K8sAgent
  182. } else {
  183. agent, err = kubernetes.GetAgentOutOfClusterConfig(form.OutOfClusterConfig)
  184. }
  185. ingress, err := agent.GetIngress(namespace, name)
  186. if err != nil {
  187. app.handleErrorFormDecoding(err, ErrReleaseDecode, w)
  188. return
  189. }
  190. if err := json.NewEncoder(w).Encode(ingress); err != nil {
  191. app.handleErrorFormDecoding(err, ErrK8sDecode, w)
  192. return
  193. }
  194. }
  195. // HandleListPods returns all pods that match the given selectors
  196. // TODO: Refactor repeated calls.
  197. func (app *App) HandleListPods(w http.ResponseWriter, r *http.Request) {
  198. // get session to retrieve correct kubeconfig
  199. _, err := app.Store.Get(r, app.ServerConf.CookieName)
  200. if err != nil {
  201. app.handleErrorFormDecoding(err, ErrReleaseDecode, w)
  202. return
  203. }
  204. vals, err := url.ParseQuery(r.URL.RawQuery)
  205. if err != nil {
  206. app.handleErrorFormDecoding(err, ErrReleaseDecode, w)
  207. return
  208. }
  209. // get the filter options
  210. form := &forms.K8sForm{
  211. OutOfClusterConfig: &kubernetes.OutOfClusterConfig{
  212. Repo: app.Repo,
  213. DigitalOceanOAuth: app.DOConf,
  214. },
  215. }
  216. form.PopulateK8sOptionsFromQueryParams(vals, app.Repo.Cluster)
  217. // validate the form
  218. if err := app.validator.Struct(form); err != nil {
  219. app.handleErrorFormValidation(err, ErrK8sValidate, w)
  220. return
  221. }
  222. // create a new agent
  223. var agent *kubernetes.Agent
  224. if app.ServerConf.IsTesting {
  225. agent = app.TestAgents.K8sAgent
  226. } else {
  227. agent, err = kubernetes.GetAgentOutOfClusterConfig(form.OutOfClusterConfig)
  228. }
  229. pods := []v1.Pod{}
  230. for _, selector := range vals["selectors"] {
  231. podsList, err := agent.GetPodsByLabel(selector)
  232. if err != nil {
  233. app.handleErrorFormValidation(err, ErrK8sValidate, w)
  234. return
  235. }
  236. for _, pod := range podsList.Items {
  237. pods = append(pods, pod)
  238. }
  239. }
  240. if err := json.NewEncoder(w).Encode(pods); err != nil {
  241. app.handleErrorFormDecoding(err, ErrK8sDecode, w)
  242. return
  243. }
  244. }
  245. // HandleListJobsByChart lists all jobs belonging to a specific Helm chart
  246. func (app *App) HandleListJobsByChart(w http.ResponseWriter, r *http.Request) {
  247. // get path parameters
  248. namespace := chi.URLParam(r, "namespace")
  249. chart := chi.URLParam(r, "chart")
  250. releaseName := chi.URLParam(r, "release_name")
  251. vals, err := url.ParseQuery(r.URL.RawQuery)
  252. if err != nil {
  253. app.handleErrorFormDecoding(err, ErrReleaseDecode, w)
  254. return
  255. }
  256. // get the filter options
  257. form := &forms.K8sForm{
  258. OutOfClusterConfig: &kubernetes.OutOfClusterConfig{
  259. Repo: app.Repo,
  260. DigitalOceanOAuth: app.DOConf,
  261. },
  262. }
  263. form.PopulateK8sOptionsFromQueryParams(vals, app.Repo.Cluster)
  264. // validate the form
  265. if err := app.validator.Struct(form); err != nil {
  266. app.handleErrorFormValidation(err, ErrK8sValidate, w)
  267. return
  268. }
  269. // create a new agent
  270. var agent *kubernetes.Agent
  271. if app.ServerConf.IsTesting {
  272. agent = app.TestAgents.K8sAgent
  273. } else {
  274. agent, err = kubernetes.GetAgentOutOfClusterConfig(form.OutOfClusterConfig)
  275. }
  276. jobs, err := agent.ListJobsByLabel(namespace, kubernetes.Label{
  277. Key: "helm.sh/chart",
  278. Val: chart,
  279. }, kubernetes.Label{
  280. Key: "meta.helm.sh/release-name",
  281. Val: releaseName,
  282. })
  283. if err != nil {
  284. app.handleErrorFormDecoding(err, ErrReleaseDecode, w)
  285. return
  286. }
  287. if err := json.NewEncoder(w).Encode(jobs); err != nil {
  288. app.handleErrorFormDecoding(err, ErrK8sDecode, w)
  289. return
  290. }
  291. }
  292. // HandleListJobPods lists all pods belonging to a specific job
  293. func (app *App) HandleListJobPods(w http.ResponseWriter, r *http.Request) {
  294. // get path parameters
  295. namespace := chi.URLParam(r, "namespace")
  296. name := chi.URLParam(r, "name")
  297. vals, err := url.ParseQuery(r.URL.RawQuery)
  298. if err != nil {
  299. app.handleErrorFormDecoding(err, ErrReleaseDecode, w)
  300. return
  301. }
  302. // get the filter options
  303. form := &forms.K8sForm{
  304. OutOfClusterConfig: &kubernetes.OutOfClusterConfig{
  305. Repo: app.Repo,
  306. DigitalOceanOAuth: app.DOConf,
  307. },
  308. }
  309. form.PopulateK8sOptionsFromQueryParams(vals, app.Repo.Cluster)
  310. // validate the form
  311. if err := app.validator.Struct(form); err != nil {
  312. app.handleErrorFormValidation(err, ErrK8sValidate, w)
  313. return
  314. }
  315. // create a new agent
  316. var agent *kubernetes.Agent
  317. if app.ServerConf.IsTesting {
  318. agent = app.TestAgents.K8sAgent
  319. } else {
  320. agent, err = kubernetes.GetAgentOutOfClusterConfig(form.OutOfClusterConfig)
  321. }
  322. pods, err := agent.GetJobPods(namespace, name)
  323. if err != nil {
  324. app.handleErrorFormDecoding(err, ErrReleaseDecode, w)
  325. return
  326. }
  327. if err := json.NewEncoder(w).Encode(pods); err != nil {
  328. app.handleErrorFormDecoding(err, ErrK8sDecode, w)
  329. return
  330. }
  331. }
  332. // HandleStreamControllerStatus test calls
  333. // TODO: Refactor repeated calls.
  334. func (app *App) HandleStreamControllerStatus(w http.ResponseWriter, r *http.Request) {
  335. // get session to retrieve correct kubeconfig
  336. _, err := app.Store.Get(r, app.ServerConf.CookieName)
  337. if err != nil {
  338. app.handleErrorFormDecoding(err, ErrReleaseDecode, w)
  339. return
  340. }
  341. vals, err := url.ParseQuery(r.URL.RawQuery)
  342. if err != nil {
  343. app.handleErrorFormDecoding(err, ErrReleaseDecode, w)
  344. return
  345. }
  346. // get the filter options
  347. form := &forms.K8sForm{
  348. OutOfClusterConfig: &kubernetes.OutOfClusterConfig{
  349. Repo: app.Repo,
  350. DigitalOceanOAuth: app.DOConf,
  351. },
  352. }
  353. form.PopulateK8sOptionsFromQueryParams(vals, app.Repo.Cluster)
  354. // validate the form
  355. if err := app.validator.Struct(form); err != nil {
  356. app.handleErrorFormValidation(err, ErrK8sValidate, w)
  357. return
  358. }
  359. // create a new agent
  360. var agent *kubernetes.Agent
  361. if app.ServerConf.IsTesting {
  362. agent = app.TestAgents.K8sAgent
  363. } else {
  364. agent, err = kubernetes.GetAgentOutOfClusterConfig(form.OutOfClusterConfig)
  365. }
  366. upgrader.CheckOrigin = func(r *http.Request) bool { return true }
  367. // upgrade to websocket.
  368. conn, err := upgrader.Upgrade(w, r, nil)
  369. if err != nil {
  370. app.handleErrorUpgradeWebsocket(err, w)
  371. }
  372. // get path parameters
  373. kind := chi.URLParam(r, "kind")
  374. err = agent.StreamControllerStatus(conn, kind)
  375. if err != nil {
  376. app.handleErrorWebsocketWrite(err, w)
  377. return
  378. }
  379. }
  380. // HandleDetectPrometheusInstalled detects a prometheus installation in the target cluster
  381. func (app *App) HandleDetectPrometheusInstalled(w http.ResponseWriter, r *http.Request) {
  382. vals, err := url.ParseQuery(r.URL.RawQuery)
  383. if err != nil {
  384. app.handleErrorFormDecoding(err, ErrReleaseDecode, w)
  385. return
  386. }
  387. // get the filter options
  388. form := &forms.K8sForm{
  389. OutOfClusterConfig: &kubernetes.OutOfClusterConfig{
  390. Repo: app.Repo,
  391. DigitalOceanOAuth: app.DOConf,
  392. },
  393. }
  394. form.PopulateK8sOptionsFromQueryParams(vals, app.Repo.Cluster)
  395. // validate the form
  396. if err := app.validator.Struct(form); err != nil {
  397. app.handleErrorFormValidation(err, ErrK8sValidate, w)
  398. return
  399. }
  400. // create a new agent
  401. var agent *kubernetes.Agent
  402. if app.ServerConf.IsTesting {
  403. agent = app.TestAgents.K8sAgent
  404. } else {
  405. agent, err = kubernetes.GetAgentOutOfClusterConfig(form.OutOfClusterConfig)
  406. }
  407. // detect prometheus service
  408. _, found, err := prometheus.GetPrometheusService(agent.Clientset)
  409. if !found {
  410. http.NotFound(w, r)
  411. return
  412. }
  413. w.WriteHeader(http.StatusOK)
  414. return
  415. }
  416. // HandleListNGINXIngresses lists all NGINX ingresses in a target cluster
  417. func (app *App) HandleListNGINXIngresses(w http.ResponseWriter, r *http.Request) {
  418. vals, err := url.ParseQuery(r.URL.RawQuery)
  419. if err != nil {
  420. app.handleErrorFormDecoding(err, ErrReleaseDecode, w)
  421. return
  422. }
  423. // get the filter options
  424. form := &forms.K8sForm{
  425. OutOfClusterConfig: &kubernetes.OutOfClusterConfig{
  426. Repo: app.Repo,
  427. DigitalOceanOAuth: app.DOConf,
  428. },
  429. }
  430. form.PopulateK8sOptionsFromQueryParams(vals, app.Repo.Cluster)
  431. // validate the form
  432. if err := app.validator.Struct(form); err != nil {
  433. app.handleErrorFormValidation(err, ErrK8sValidate, w)
  434. return
  435. }
  436. // create a new agent
  437. var agent *kubernetes.Agent
  438. if app.ServerConf.IsTesting {
  439. agent = app.TestAgents.K8sAgent
  440. } else {
  441. agent, err = kubernetes.GetAgentOutOfClusterConfig(form.OutOfClusterConfig)
  442. }
  443. res, err := prometheus.GetIngressesWithNGINXAnnotation(agent.Clientset)
  444. if err != nil {
  445. app.handleErrorInternal(err, w)
  446. return
  447. }
  448. w.WriteHeader(http.StatusOK)
  449. if err := json.NewEncoder(w).Encode(res); err != nil {
  450. app.handleErrorFormDecoding(err, ErrK8sDecode, w)
  451. return
  452. }
  453. }
  454. func (app *App) HandleGetPodMetrics(w http.ResponseWriter, r *http.Request) {
  455. vals, err := url.ParseQuery(r.URL.RawQuery)
  456. if err != nil {
  457. app.handleErrorFormDecoding(err, ErrReleaseDecode, w)
  458. return
  459. }
  460. // get the filter options
  461. form := &forms.MetricsQueryForm{
  462. K8sForm: &forms.K8sForm{
  463. OutOfClusterConfig: &kubernetes.OutOfClusterConfig{
  464. Repo: app.Repo,
  465. DigitalOceanOAuth: app.DOConf,
  466. },
  467. },
  468. QueryOpts: &prometheus.QueryOpts{},
  469. }
  470. form.K8sForm.PopulateK8sOptionsFromQueryParams(vals, app.Repo.Cluster)
  471. // decode from JSON to form value
  472. decoder := schema.NewDecoder()
  473. decoder.IgnoreUnknownKeys(true)
  474. if err := decoder.Decode(form.QueryOpts, vals); err != nil {
  475. app.handleErrorFormDecoding(err, ErrProjectDecode, w)
  476. return
  477. }
  478. // validate the form
  479. if err := app.validator.Struct(form); err != nil {
  480. app.handleErrorFormValidation(err, ErrK8sValidate, w)
  481. return
  482. }
  483. // create a new agent
  484. var agent *kubernetes.Agent
  485. if app.ServerConf.IsTesting {
  486. agent = app.TestAgents.K8sAgent
  487. } else {
  488. agent, err = kubernetes.GetAgentOutOfClusterConfig(form.OutOfClusterConfig)
  489. }
  490. // get prometheus service
  491. promSvc, found, err := prometheus.GetPrometheusService(agent.Clientset)
  492. if err != nil {
  493. app.handleErrorInternal(err, w)
  494. return
  495. }
  496. if !found {
  497. app.handleErrorInternal(err, w)
  498. return
  499. }
  500. rawQuery, err := prometheus.QueryPrometheus(agent.Clientset, promSvc, form.QueryOpts)
  501. if err != nil {
  502. app.handleErrorInternal(err, w)
  503. return
  504. }
  505. fmt.Fprint(w, string(rawQuery))
  506. }
  507. type KubeconfigResponse struct {
  508. Kubeconfig []byte `json:"kubeconfig"`
  509. }
  510. func (app *App) HandleGetTemporaryKubeconfig(w http.ResponseWriter, r *http.Request) {
  511. vals, err := url.ParseQuery(r.URL.RawQuery)
  512. if err != nil {
  513. app.handleErrorFormDecoding(err, ErrReleaseDecode, w)
  514. return
  515. }
  516. // get the filter options
  517. form := &forms.K8sForm{
  518. OutOfClusterConfig: &kubernetes.OutOfClusterConfig{
  519. Repo: app.Repo,
  520. DigitalOceanOAuth: app.DOConf,
  521. },
  522. }
  523. form.PopulateK8sOptionsFromQueryParams(vals, app.Repo.Cluster)
  524. // get the API config
  525. apiConf, err := form.OutOfClusterConfig.CreateRawConfigFromCluster()
  526. if err != nil {
  527. app.handleErrorFormDecoding(err, ErrProjectDecode, w)
  528. return
  529. }
  530. bytes, err := clientcmd.Write(*apiConf)
  531. res := &KubeconfigResponse{
  532. Kubeconfig: bytes,
  533. }
  534. if err := json.NewEncoder(w).Encode(res); err != nil {
  535. app.handleErrorFormDecoding(err, ErrK8sDecode, w)
  536. return
  537. }
  538. }