2
0

app.go 27 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061
  1. package cmd
  2. import (
  3. "context"
  4. "encoding/json"
  5. "errors"
  6. "fmt"
  7. "io"
  8. "os"
  9. "strings"
  10. "time"
  11. "github.com/fatih/color"
  12. api "github.com/porter-dev/porter/api/client"
  13. "github.com/porter-dev/porter/api/types"
  14. "github.com/porter-dev/porter/cli/cmd/utils"
  15. "github.com/spf13/cobra"
  16. batchv1 "k8s.io/api/batch/v1"
  17. v1 "k8s.io/api/core/v1"
  18. rbacv1 "k8s.io/api/rbac/v1"
  19. metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
  20. "k8s.io/apimachinery/pkg/fields"
  21. "k8s.io/apimachinery/pkg/watch"
  22. "k8s.io/kubectl/pkg/util/term"
  23. templaterUtils "github.com/porter-dev/porter/internal/templater/utils"
  24. "k8s.io/apimachinery/pkg/runtime"
  25. "k8s.io/apimachinery/pkg/runtime/schema"
  26. "k8s.io/client-go/kubernetes"
  27. "k8s.io/client-go/rest"
  28. "k8s.io/client-go/tools/clientcmd"
  29. "k8s.io/client-go/tools/remotecommand"
  30. )
  31. var (
  32. appNamespace string
  33. appVerbose bool
  34. appExistingPod bool
  35. appInteractive bool
  36. appContainerName string
  37. appTag string
  38. )
  39. // appCmd represents the "porter app" base command when called
  40. // without any subcommands
  41. var appCmd = &cobra.Command{
  42. Use: "app",
  43. Short: "Runs a command for your application.",
  44. }
  45. // appRunCmd represents the "porter app run" subcommand
  46. var appRunCmd = &cobra.Command{
  47. Use: "run [application] -- COMMAND [args...]",
  48. Args: cobra.MinimumNArgs(2),
  49. Short: "Runs a command inside a connected cluster container.",
  50. Run: func(cmd *cobra.Command, args []string) {
  51. err := checkLoginAndRun(args, appRun)
  52. if err != nil {
  53. os.Exit(1)
  54. }
  55. },
  56. }
  57. // appRunCleanupCmd represents the "porter app run cleanup" subcommand
  58. var appRunCleanupCmd = &cobra.Command{
  59. Use: "cleanup",
  60. Args: cobra.NoArgs,
  61. Short: "Delete any lingering ephemeral pods that were created with \"porter app run\".",
  62. Run: func(cmd *cobra.Command, args []string) {
  63. err := checkLoginAndRun(args, appCleanup)
  64. if err != nil {
  65. os.Exit(1)
  66. }
  67. },
  68. }
  69. // appUpdateTagCmd represents the "porter app update-tag" subcommand
  70. var appUpdateTagCmd = &cobra.Command{
  71. Use: "update-tag [application]",
  72. Args: cobra.MinimumNArgs(1),
  73. Short: "Updates the image tag for an application.",
  74. Run: func(cmd *cobra.Command, args []string) {
  75. err := checkLoginAndRun(args, appUpdateTag)
  76. if err != nil {
  77. os.Exit(1)
  78. }
  79. },
  80. }
  81. func init() {
  82. rootCmd.AddCommand(appCmd)
  83. appRunCmd.PersistentFlags().BoolVarP(
  84. &appExistingPod,
  85. "existing_pod",
  86. "e",
  87. false,
  88. "whether to connect to an existing pod (default false)",
  89. )
  90. appRunCmd.PersistentFlags().BoolVarP(
  91. &appVerbose,
  92. "verbose",
  93. "v",
  94. false,
  95. "whether to print verbose output",
  96. )
  97. appRunCmd.PersistentFlags().BoolVar(
  98. &appInteractive,
  99. "interactive",
  100. false,
  101. "whether to run in interactive mode (default false)",
  102. )
  103. appRunCmd.PersistentFlags().StringVarP(
  104. &appContainerName,
  105. "container",
  106. "c",
  107. "",
  108. "name of the container inside pod to run the command in",
  109. )
  110. appRunCmd.AddCommand(appRunCleanupCmd)
  111. appUpdateTagCmd.PersistentFlags().StringVarP(
  112. &appTag,
  113. "tag",
  114. "t",
  115. "",
  116. "the specified tag to use, default is \"latest\"",
  117. )
  118. appCmd.AddCommand(appRunCmd)
  119. appCmd.AddCommand(appUpdateTagCmd)
  120. }
  121. func appRun(_ *types.GetAuthenticatedUserResponse, client *api.Client, args []string) error {
  122. execArgs := args[1:]
  123. color.New(color.FgGreen).Println("Running", strings.Join(execArgs, " "), "for application", args[0])
  124. appNamespace = fmt.Sprintf("porter-stack-%s", args[0])
  125. if len(execArgs) > 0 {
  126. res, err := client.GetPorterApp(context.Background(), cliConf.Project, cliConf.Cluster, args[0])
  127. if err != nil {
  128. return fmt.Errorf("Unable to run command - application not found: %w", err)
  129. }
  130. if res.Builder != "" &&
  131. (strings.Contains(res.Builder, "heroku") ||
  132. strings.Contains(res.Builder, "paketo")) &&
  133. execArgs[0] != "/cnb/lifecycle/launcher" &&
  134. execArgs[0] != "launcher" {
  135. // this is a buildpacks release using a heroku builder, prepend the launcher
  136. execArgs = append([]string{"/cnb/lifecycle/launcher"}, execArgs...)
  137. }
  138. }
  139. podsSimple, err := appGetPods(client, appNamespace, args[0])
  140. if err != nil {
  141. return fmt.Errorf("Could not retrieve list of pods: %s", err.Error())
  142. }
  143. // if length of pods is 0, throw error
  144. var selectedPod appPodSimple
  145. if len(podsSimple) == 0 {
  146. return fmt.Errorf("At least one pod must exist in this deployment.")
  147. } else if !appInteractive || len(podsSimple) == 1 {
  148. selectedPod = podsSimple[0]
  149. } else {
  150. podNames := make([]string, 0)
  151. for _, podSimple := range podsSimple {
  152. podNames = append(podNames, podSimple.Name)
  153. }
  154. selectedPodName, err := utils.PromptSelect("Select the pod:", podNames)
  155. if err != nil {
  156. return err
  157. }
  158. // find selected pod
  159. for _, podSimple := range podsSimple {
  160. if selectedPodName == podSimple.Name {
  161. selectedPod = podSimple
  162. }
  163. }
  164. }
  165. var selectedContainerName string
  166. if len(selectedPod.ContainerNames) == 0 {
  167. return fmt.Errorf("At least one container must exist in the selected pod.")
  168. } else if len(selectedPod.ContainerNames) == 1 {
  169. if appContainerName != "" && appContainerName != selectedPod.ContainerNames[0] {
  170. return fmt.Errorf("provided container %s does not exist in pod %s", appContainerName, selectedPod.Name)
  171. }
  172. selectedContainerName = selectedPod.ContainerNames[0]
  173. }
  174. if appContainerName != "" && selectedContainerName == "" {
  175. // check if provided container name exists in the pod
  176. for _, name := range selectedPod.ContainerNames {
  177. if name == appContainerName {
  178. selectedContainerName = name
  179. break
  180. }
  181. }
  182. if selectedContainerName == "" {
  183. return fmt.Errorf("provided container %s does not exist in pod %s", appContainerName, selectedPod.Name)
  184. }
  185. }
  186. if selectedContainerName == "" {
  187. if !appInteractive {
  188. return fmt.Errorf("container name must be specified using the --container flag when not using interactive mode")
  189. }
  190. selectedContainer, err := utils.PromptSelect("Select the container:", selectedPod.ContainerNames)
  191. if err != nil {
  192. return err
  193. }
  194. selectedContainerName = selectedContainer
  195. }
  196. config := &AppPorterRunSharedConfig{
  197. Client: client,
  198. }
  199. err = config.setSharedConfig()
  200. if err != nil {
  201. return fmt.Errorf("Could not retrieve kube credentials: %s", err.Error())
  202. }
  203. if appExistingPod {
  204. return appExecuteRun(config, appNamespace, selectedPod.Name, selectedContainerName, execArgs)
  205. }
  206. return appExecuteRunEphemeral(config, appNamespace, selectedPod.Name, selectedContainerName, execArgs)
  207. }
  208. func appCleanup(_ *types.GetAuthenticatedUserResponse, client *api.Client, _ []string) error {
  209. config := &AppPorterRunSharedConfig{
  210. Client: client,
  211. }
  212. err := config.setSharedConfig()
  213. if err != nil {
  214. return fmt.Errorf("Could not retrieve kube credentials: %s", err.Error())
  215. }
  216. proceed, err := utils.PromptSelect(
  217. fmt.Sprintf("You have chosen the '%s' namespace for cleanup. Do you want to proceed?", appNamespace),
  218. []string{"Yes", "No", "All namespaces"},
  219. )
  220. if err != nil {
  221. return err
  222. }
  223. if proceed == "No" {
  224. return nil
  225. }
  226. var podNames []string
  227. color.New(color.FgGreen).Println("Fetching ephemeral pods for cleanup")
  228. if proceed == "All namespaces" {
  229. namespaces, err := config.Clientset.CoreV1().Namespaces().List(context.Background(), metav1.ListOptions{})
  230. if err != nil {
  231. return err
  232. }
  233. for _, namespace := range namespaces.Items {
  234. if pods, err := appGetEphemeralPods(namespace.Name, config.Clientset); err == nil {
  235. podNames = append(podNames, pods...)
  236. } else {
  237. return err
  238. }
  239. }
  240. } else {
  241. if pods, err := appGetEphemeralPods(appNamespace, config.Clientset); err == nil {
  242. podNames = append(podNames, pods...)
  243. } else {
  244. return err
  245. }
  246. }
  247. if len(podNames) == 0 {
  248. color.New(color.FgBlue).Println("No ephemeral pods to delete")
  249. return nil
  250. }
  251. selectedPods, err := utils.PromptMultiselect("Select ephemeral pods to delete", podNames)
  252. if err != nil {
  253. return err
  254. }
  255. for _, podName := range selectedPods {
  256. color.New(color.FgBlue).Printf("Deleting ephemeral pod: %s\n", podName)
  257. err = config.Clientset.CoreV1().Pods(appNamespace).Delete(
  258. context.Background(), podName, metav1.DeleteOptions{},
  259. )
  260. if err != nil {
  261. return err
  262. }
  263. }
  264. return nil
  265. }
  266. func appGetEphemeralPods(namespace string, clientset *kubernetes.Clientset) ([]string, error) {
  267. var podNames []string
  268. pods, err := clientset.CoreV1().Pods(namespace).List(
  269. context.Background(), metav1.ListOptions{LabelSelector: "porter/ephemeral-pod"},
  270. )
  271. if err != nil {
  272. return nil, err
  273. }
  274. for _, pod := range pods.Items {
  275. podNames = append(podNames, pod.Name)
  276. }
  277. return podNames, nil
  278. }
  279. type AppPorterRunSharedConfig struct {
  280. Client *api.Client
  281. RestConf *rest.Config
  282. Clientset *kubernetes.Clientset
  283. RestClient *rest.RESTClient
  284. }
  285. func (p *AppPorterRunSharedConfig) setSharedConfig() error {
  286. pID := cliConf.Project
  287. cID := cliConf.Cluster
  288. kubeResp, err := p.Client.GetKubeconfig(context.Background(), pID, cID, cliConf.Kubeconfig)
  289. if err != nil {
  290. return err
  291. }
  292. kubeBytes := kubeResp.Kubeconfig
  293. cmdConf, err := clientcmd.NewClientConfigFromBytes(kubeBytes)
  294. if err != nil {
  295. return err
  296. }
  297. restConf, err := cmdConf.ClientConfig()
  298. if err != nil {
  299. return err
  300. }
  301. restConf.GroupVersion = &schema.GroupVersion{
  302. Group: "api",
  303. Version: "v1",
  304. }
  305. restConf.NegotiatedSerializer = runtime.NewSimpleNegotiatedSerializer(runtime.SerializerInfo{})
  306. p.RestConf = restConf
  307. clientset, err := kubernetes.NewForConfig(restConf)
  308. if err != nil {
  309. return err
  310. }
  311. p.Clientset = clientset
  312. restClient, err := rest.RESTClientFor(restConf)
  313. if err != nil {
  314. return err
  315. }
  316. p.RestClient = restClient
  317. return nil
  318. }
  319. type appPodSimple struct {
  320. Name string
  321. ContainerNames []string
  322. }
  323. func appGetPods(client *api.Client, namespace, releaseName string) ([]appPodSimple, error) {
  324. pID := cliConf.Project
  325. cID := cliConf.Cluster
  326. resp, err := client.GetK8sAllPods(context.TODO(), pID, cID, namespace, releaseName)
  327. if err != nil {
  328. return nil, err
  329. }
  330. pods := *resp
  331. res := make([]appPodSimple, 0)
  332. for _, pod := range pods {
  333. if pod.Status.Phase == v1.PodRunning {
  334. containerNames := make([]string, 0)
  335. for _, container := range pod.Spec.Containers {
  336. containerNames = append(containerNames, container.Name)
  337. }
  338. res = append(res, appPodSimple{
  339. Name: pod.ObjectMeta.Name,
  340. ContainerNames: containerNames,
  341. })
  342. }
  343. }
  344. return res, nil
  345. }
  346. func appExecuteRun(config *AppPorterRunSharedConfig, namespace, name, container string, args []string) error {
  347. req := config.RestClient.Post().
  348. Resource("pods").
  349. Name(name).
  350. Namespace(namespace).
  351. SubResource("exec")
  352. for _, arg := range args {
  353. req.Param("command", arg)
  354. }
  355. req.Param("stdin", "true")
  356. req.Param("stdout", "true")
  357. req.Param("tty", "true")
  358. req.Param("container", container)
  359. t := term.TTY{
  360. In: os.Stdin,
  361. Out: os.Stdout,
  362. Raw: true,
  363. }
  364. size := t.GetSize()
  365. sizeQueue := t.MonitorSize(size)
  366. return t.Safe(func() error {
  367. exec, err := remotecommand.NewSPDYExecutor(config.RestConf, "POST", req.URL())
  368. if err != nil {
  369. return err
  370. }
  371. return exec.Stream(remotecommand.StreamOptions{
  372. Stdin: os.Stdin,
  373. Stdout: os.Stdout,
  374. Stderr: os.Stderr,
  375. Tty: true,
  376. TerminalSizeQueue: sizeQueue,
  377. })
  378. })
  379. }
  380. func appExecuteRunEphemeral(config *AppPorterRunSharedConfig, namespace, name, container string, args []string) error {
  381. existing, err := appGetExistingPod(config, name, namespace)
  382. if err != nil {
  383. return err
  384. }
  385. newPod, err := appCreateEphemeralPodFromExisting(config, existing, container, args)
  386. if err != nil {
  387. return err
  388. }
  389. podName := newPod.ObjectMeta.Name
  390. // delete the ephemeral pod no matter what
  391. defer appDeletePod(config, podName, namespace)
  392. color.New(color.FgYellow).Printf("Waiting for pod %s to be ready...", podName)
  393. if err = appWaitForPod(config, newPod); err != nil {
  394. color.New(color.FgRed).Println("failed")
  395. return appHandlePodAttachError(err, config, namespace, podName, container)
  396. }
  397. err = appCheckForPodDeletionCronJob(config)
  398. if err != nil {
  399. return err
  400. }
  401. // refresh pod info for latest status
  402. newPod, err = config.Clientset.CoreV1().
  403. Pods(newPod.Namespace).
  404. Get(context.Background(), newPod.Name, metav1.GetOptions{})
  405. // pod exited while we were waiting. maybe an error maybe not.
  406. // we dont know if the user wanted an interactive shell or not.
  407. // if it was an error the logs hopefully say so.
  408. if appIsPodExited(newPod) {
  409. color.New(color.FgGreen).Println("complete!")
  410. var writtenBytes int64
  411. writtenBytes, _ = appPipePodLogsToStdout(config, namespace, podName, container, false)
  412. if appVerbose || writtenBytes == 0 {
  413. color.New(color.FgYellow).Println("Could not get logs. Pod events:")
  414. appPipeEventsToStdout(config, namespace, podName, container, false)
  415. }
  416. return nil
  417. }
  418. color.New(color.FgGreen).Println("ready!")
  419. color.New(color.FgYellow).Println("Attempting connection to the container. If you don't see a command prompt, try pressing enter.")
  420. req := config.RestClient.Post().
  421. Resource("pods").
  422. Name(podName).
  423. Namespace(namespace).
  424. SubResource("attach")
  425. req.Param("stdin", "true")
  426. req.Param("stdout", "true")
  427. req.Param("tty", "true")
  428. req.Param("container", container)
  429. t := term.TTY{
  430. In: os.Stdin,
  431. Out: os.Stdout,
  432. Raw: true,
  433. }
  434. size := t.GetSize()
  435. sizeQueue := t.MonitorSize(size)
  436. if err = t.Safe(func() error {
  437. exec, err := remotecommand.NewSPDYExecutor(config.RestConf, "POST", req.URL())
  438. if err != nil {
  439. return err
  440. }
  441. return exec.Stream(remotecommand.StreamOptions{
  442. Stdin: os.Stdin,
  443. Stdout: os.Stdout,
  444. Stderr: os.Stderr,
  445. Tty: true,
  446. TerminalSizeQueue: sizeQueue,
  447. })
  448. }); err != nil {
  449. // ugly way to catch no TTY errors, such as when running command "echo \"hello\""
  450. return appHandlePodAttachError(err, config, namespace, podName, container)
  451. }
  452. if appVerbose {
  453. color.New(color.FgYellow).Println("Pod events:")
  454. appPipeEventsToStdout(config, namespace, podName, container, false)
  455. }
  456. return err
  457. }
  458. func appCheckForPodDeletionCronJob(config *AppPorterRunSharedConfig) error {
  459. // try and create the cron job and all of the other required resources as necessary,
  460. // starting with the service account, then role and then a role binding
  461. err := appCheckForServiceAccount(config)
  462. if err != nil {
  463. return err
  464. }
  465. err = appCheckForClusterRole(config)
  466. if err != nil {
  467. return err
  468. }
  469. err = appCheckForRoleBinding(config)
  470. if err != nil {
  471. return err
  472. }
  473. namespaces, err := config.Clientset.CoreV1().Namespaces().List(context.Background(), metav1.ListOptions{})
  474. if err != nil {
  475. return err
  476. }
  477. for _, namespace := range namespaces.Items {
  478. cronJobs, err := config.Clientset.BatchV1().CronJobs(namespace.Name).List(
  479. context.Background(), metav1.ListOptions{},
  480. )
  481. if err != nil {
  482. return err
  483. }
  484. if namespace.Name == "default" {
  485. for _, cronJob := range cronJobs.Items {
  486. if cronJob.Name == "porter-ephemeral-pod-deletion-cronjob" {
  487. return nil
  488. }
  489. }
  490. } else {
  491. for _, cronJob := range cronJobs.Items {
  492. if cronJob.Name == "porter-ephemeral-pod-deletion-cronjob" {
  493. err = config.Clientset.BatchV1().CronJobs(namespace.Name).Delete(
  494. context.Background(), cronJob.Name, metav1.DeleteOptions{},
  495. )
  496. if err != nil {
  497. return err
  498. }
  499. }
  500. }
  501. }
  502. }
  503. // create the cronjob
  504. cronJob := &batchv1.CronJob{
  505. ObjectMeta: metav1.ObjectMeta{
  506. Name: "porter-ephemeral-pod-deletion-cronjob",
  507. },
  508. Spec: batchv1.CronJobSpec{
  509. Schedule: "0 * * * *",
  510. JobTemplate: batchv1.JobTemplateSpec{
  511. Spec: batchv1.JobSpec{
  512. Template: v1.PodTemplateSpec{
  513. Spec: v1.PodSpec{
  514. ServiceAccountName: "porter-ephemeral-pod-deletion-service-account",
  515. RestartPolicy: v1.RestartPolicyNever,
  516. Containers: []v1.Container{
  517. {
  518. Name: "ephemeral-pods-manager",
  519. Image: "public.ecr.aws/o1j4x7p4/porter-ephemeral-pods-manager:latest",
  520. ImagePullPolicy: v1.PullAlways,
  521. Args: []string{"delete"},
  522. },
  523. },
  524. },
  525. },
  526. },
  527. },
  528. },
  529. }
  530. _, err = config.Clientset.BatchV1().CronJobs("default").Create(
  531. context.Background(), cronJob, metav1.CreateOptions{},
  532. )
  533. if err != nil {
  534. return err
  535. }
  536. return nil
  537. }
  538. func appCheckForServiceAccount(config *AppPorterRunSharedConfig) error {
  539. namespaces, err := config.Clientset.CoreV1().Namespaces().List(context.Background(), metav1.ListOptions{})
  540. if err != nil {
  541. return err
  542. }
  543. for _, namespace := range namespaces.Items {
  544. serviceAccounts, err := config.Clientset.CoreV1().ServiceAccounts(namespace.Name).List(
  545. context.Background(), metav1.ListOptions{},
  546. )
  547. if err != nil {
  548. return err
  549. }
  550. if namespace.Name == "default" {
  551. for _, svcAccount := range serviceAccounts.Items {
  552. if svcAccount.Name == "porter-ephemeral-pod-deletion-service-account" {
  553. return nil
  554. }
  555. }
  556. } else {
  557. for _, svcAccount := range serviceAccounts.Items {
  558. if svcAccount.Name == "porter-ephemeral-pod-deletion-service-account" {
  559. err = config.Clientset.CoreV1().ServiceAccounts(namespace.Name).Delete(
  560. context.Background(), svcAccount.Name, metav1.DeleteOptions{},
  561. )
  562. if err != nil {
  563. return err
  564. }
  565. }
  566. }
  567. }
  568. }
  569. serviceAccount := &v1.ServiceAccount{
  570. ObjectMeta: metav1.ObjectMeta{
  571. Name: "porter-ephemeral-pod-deletion-service-account",
  572. },
  573. }
  574. _, err = config.Clientset.CoreV1().ServiceAccounts("default").Create(
  575. context.Background(), serviceAccount, metav1.CreateOptions{},
  576. )
  577. if err != nil {
  578. return err
  579. }
  580. return nil
  581. }
  582. func appCheckForClusterRole(config *AppPorterRunSharedConfig) error {
  583. roles, err := config.Clientset.RbacV1().ClusterRoles().List(
  584. context.Background(), metav1.ListOptions{},
  585. )
  586. if err != nil {
  587. return err
  588. }
  589. for _, role := range roles.Items {
  590. if role.Name == "porter-ephemeral-pod-deletion-cluster-role" {
  591. return nil
  592. }
  593. }
  594. role := &rbacv1.ClusterRole{
  595. ObjectMeta: metav1.ObjectMeta{
  596. Name: "porter-ephemeral-pod-deletion-cluster-role",
  597. },
  598. Rules: []rbacv1.PolicyRule{
  599. {
  600. APIGroups: []string{""},
  601. Resources: []string{"pods"},
  602. Verbs: []string{"list", "delete"},
  603. },
  604. {
  605. APIGroups: []string{""},
  606. Resources: []string{"namespaces"},
  607. Verbs: []string{"list"},
  608. },
  609. },
  610. }
  611. _, err = config.Clientset.RbacV1().ClusterRoles().Create(
  612. context.Background(), role, metav1.CreateOptions{},
  613. )
  614. if err != nil {
  615. return err
  616. }
  617. return nil
  618. }
  619. func appCheckForRoleBinding(config *AppPorterRunSharedConfig) error {
  620. bindings, err := config.Clientset.RbacV1().ClusterRoleBindings().List(
  621. context.Background(), metav1.ListOptions{},
  622. )
  623. if err != nil {
  624. return err
  625. }
  626. for _, binding := range bindings.Items {
  627. if binding.Name == "porter-ephemeral-pod-deletion-cluster-rolebinding" {
  628. return nil
  629. }
  630. }
  631. binding := &rbacv1.ClusterRoleBinding{
  632. ObjectMeta: metav1.ObjectMeta{
  633. Name: "porter-ephemeral-pod-deletion-cluster-rolebinding",
  634. },
  635. RoleRef: rbacv1.RoleRef{
  636. APIGroup: "rbac.authorization.k8s.io",
  637. Kind: "ClusterRole",
  638. Name: "porter-ephemeral-pod-deletion-cluster-role",
  639. },
  640. Subjects: []rbacv1.Subject{
  641. {
  642. APIGroup: "",
  643. Kind: "ServiceAccount",
  644. Name: "porter-ephemeral-pod-deletion-service-account",
  645. Namespace: "default",
  646. },
  647. },
  648. }
  649. _, err = config.Clientset.RbacV1().ClusterRoleBindings().Create(
  650. context.Background(), binding, metav1.CreateOptions{},
  651. )
  652. if err != nil {
  653. return err
  654. }
  655. return nil
  656. }
  657. func appWaitForPod(config *AppPorterRunSharedConfig, pod *v1.Pod) error {
  658. var (
  659. w watch.Interface
  660. err error
  661. ok bool
  662. )
  663. // immediately after creating a pod, the API may return a 404. heuristically 1
  664. // second seems to be plenty.
  665. watchRetries := 3
  666. for i := 0; i < watchRetries; i++ {
  667. selector := fields.OneTermEqualSelector("metadata.name", pod.Name).String()
  668. w, err = config.Clientset.CoreV1().
  669. Pods(pod.Namespace).
  670. Watch(context.Background(), metav1.ListOptions{FieldSelector: selector})
  671. if err == nil {
  672. break
  673. }
  674. time.Sleep(time.Second)
  675. }
  676. if err != nil {
  677. return err
  678. }
  679. defer w.Stop()
  680. for {
  681. select {
  682. case <-time.Tick(time.Second):
  683. // poll every second in case we already missed the ready event while
  684. // creating the listener.
  685. pod, err = config.Clientset.CoreV1().
  686. Pods(pod.Namespace).
  687. Get(context.Background(), pod.Name, metav1.GetOptions{})
  688. if appIsPodReady(pod) || appIsPodExited(pod) {
  689. return nil
  690. }
  691. case evt := <-w.ResultChan():
  692. pod, ok = evt.Object.(*v1.Pod)
  693. if !ok {
  694. return fmt.Errorf("unexpected object type: %T", evt.Object)
  695. }
  696. if appIsPodReady(pod) || appIsPodExited(pod) {
  697. return nil
  698. }
  699. case <-time.After(time.Second * 10):
  700. return errors.New("timed out waiting for pod")
  701. }
  702. }
  703. }
  704. func appIsPodReady(pod *v1.Pod) bool {
  705. ready := false
  706. conditions := pod.Status.Conditions
  707. for i := range conditions {
  708. if conditions[i].Type == v1.PodReady {
  709. ready = pod.Status.Conditions[i].Status == v1.ConditionTrue
  710. }
  711. }
  712. return ready
  713. }
  714. func appIsPodExited(pod *v1.Pod) bool {
  715. return pod.Status.Phase == v1.PodSucceeded || pod.Status.Phase == v1.PodFailed
  716. }
  717. func appHandlePodAttachError(err error, config *AppPorterRunSharedConfig, namespace, podName, container string) error {
  718. if appVerbose {
  719. color.New(color.FgYellow).Fprintf(os.Stderr, "Error: %s\n", err)
  720. }
  721. color.New(color.FgYellow).Fprintln(os.Stderr, "Could not open a shell to this container. Container logs:")
  722. var writtenBytes int64
  723. writtenBytes, _ = appPipePodLogsToStdout(config, namespace, podName, container, false)
  724. if appVerbose || writtenBytes == 0 {
  725. color.New(color.FgYellow).Fprintln(os.Stderr, "Could not get logs. Pod events:")
  726. appPipeEventsToStdout(config, namespace, podName, container, false)
  727. }
  728. return err
  729. }
  730. func appPipePodLogsToStdout(config *AppPorterRunSharedConfig, namespace, name, container string, follow bool) (int64, error) {
  731. podLogOpts := v1.PodLogOptions{
  732. Container: container,
  733. Follow: follow,
  734. }
  735. req := config.Clientset.CoreV1().Pods(namespace).GetLogs(name, &podLogOpts)
  736. podLogs, err := req.Stream(
  737. context.Background(),
  738. )
  739. if err != nil {
  740. return 0, err
  741. }
  742. defer podLogs.Close()
  743. return io.Copy(os.Stdout, podLogs)
  744. }
  745. func appPipeEventsToStdout(config *AppPorterRunSharedConfig, namespace, name, container string, follow bool) error {
  746. // update the config in case the operation has taken longer than token expiry time
  747. config.setSharedConfig()
  748. // creates the clientset
  749. resp, err := config.Clientset.CoreV1().Events(namespace).List(
  750. context.TODO(),
  751. metav1.ListOptions{
  752. FieldSelector: fmt.Sprintf("involvedObject.name=%s,involvedObject.namespace=%s", name, namespace),
  753. },
  754. )
  755. if err != nil {
  756. return err
  757. }
  758. for _, event := range resp.Items {
  759. color.New(color.FgRed).Println(event.Message)
  760. }
  761. return nil
  762. }
  763. func appGetExistingPod(config *AppPorterRunSharedConfig, name, namespace string) (*v1.Pod, error) {
  764. return config.Clientset.CoreV1().Pods(namespace).Get(
  765. context.Background(),
  766. name,
  767. metav1.GetOptions{},
  768. )
  769. }
  770. func appDeletePod(config *AppPorterRunSharedConfig, name, namespace string) error {
  771. // update the config in case the operation has taken longer than token expiry time
  772. config.setSharedConfig()
  773. err := config.Clientset.CoreV1().Pods(namespace).Delete(
  774. context.Background(),
  775. name,
  776. metav1.DeleteOptions{},
  777. )
  778. if err != nil {
  779. color.New(color.FgRed).Fprintf(os.Stderr, "Could not delete ephemeral pod: %s\n", err.Error())
  780. return err
  781. }
  782. color.New(color.FgGreen).Println("Sucessfully deleted ephemeral pod")
  783. return nil
  784. }
  785. func appCreateEphemeralPodFromExisting(
  786. config *AppPorterRunSharedConfig,
  787. existing *v1.Pod,
  788. container string,
  789. args []string,
  790. ) (*v1.Pod, error) {
  791. newPod := existing.DeepCopy()
  792. // only copy the pod spec, overwrite metadata
  793. newPod.ObjectMeta = metav1.ObjectMeta{
  794. Name: strings.ToLower(fmt.Sprintf("%s-copy-%s", existing.ObjectMeta.Name, utils.String(4))),
  795. Namespace: existing.ObjectMeta.Namespace,
  796. }
  797. newPod.Status = v1.PodStatus{}
  798. // set restart policy to never
  799. newPod.Spec.RestartPolicy = v1.RestartPolicyNever
  800. // change the command in the pod to the passed in pod command
  801. cmdRoot := args[0]
  802. cmdArgs := make([]string, 0)
  803. // annotate with the ephemeral pod tag
  804. newPod.Labels = make(map[string]string)
  805. newPod.Labels["porter/ephemeral-pod"] = "true"
  806. if len(args) > 1 {
  807. cmdArgs = args[1:]
  808. }
  809. for i := 0; i < len(newPod.Spec.Containers); i++ {
  810. if newPod.Spec.Containers[i].Name == container {
  811. newPod.Spec.Containers[i].Command = []string{cmdRoot}
  812. newPod.Spec.Containers[i].Args = cmdArgs
  813. newPod.Spec.Containers[i].TTY = true
  814. newPod.Spec.Containers[i].Stdin = true
  815. newPod.Spec.Containers[i].StdinOnce = true
  816. }
  817. // remove health checks and probes
  818. newPod.Spec.Containers[i].LivenessProbe = nil
  819. newPod.Spec.Containers[i].ReadinessProbe = nil
  820. newPod.Spec.Containers[i].StartupProbe = nil
  821. }
  822. newPod.Spec.NodeName = ""
  823. // create the pod and return it
  824. return config.Clientset.CoreV1().Pods(existing.ObjectMeta.Namespace).Create(
  825. context.Background(),
  826. newPod,
  827. metav1.CreateOptions{},
  828. )
  829. }
  830. func appUpdateTag(_ *types.GetAuthenticatedUserResponse, client *api.Client, args []string) error {
  831. namespace := fmt.Sprintf("porter-stack-%s", args[0])
  832. if appTag == "" {
  833. appTag = "latest"
  834. }
  835. release, err := client.GetRelease(context.TODO(), cliConf.Project, cliConf.Cluster, namespace, args[0])
  836. if err != nil {
  837. return fmt.Errorf("Unable to find application %s", args[0])
  838. }
  839. // check for the post-deploy job associated with the release
  840. postDeployReleaseName := fmt.Sprintf("%s-r", args[0])
  841. postDeployRelease, postDeployReleaseErr := client.GetRelease(context.TODO(), cliConf.Project, cliConf.Cluster, namespace, postDeployReleaseName)
  842. color.New(color.FgGreen).Printf("Updating application %s to build using tag \"%s\"\n", args[0], appTag)
  843. overrideValues := map[string]interface{}{
  844. "global": map[string]interface{}{
  845. "image": map[string]interface{}{
  846. "tag": appTag,
  847. },
  848. },
  849. }
  850. mergedValues := templaterUtils.CoalesceValues(release.Config, overrideValues)
  851. bytes, err := json.Marshal(mergedValues)
  852. if err != nil {
  853. return fmt.Errorf("Unable to update application %s: %w", args[0], err)
  854. }
  855. err = client.UpgradeRelease(
  856. context.Background(),
  857. cliConf.Project,
  858. cliConf.Cluster,
  859. namespace,
  860. args[0],
  861. &types.UpgradeReleaseRequest{
  862. Values: string(bytes),
  863. IgnoreDependencies: true,
  864. },
  865. )
  866. if err != nil {
  867. return fmt.Errorf("Unable to update application %s: %w", args[0], err)
  868. }
  869. color.New(color.FgGreen).Printf("Successfully updated application %s\n", args[0])
  870. if postDeployReleaseErr != nil {
  871. // didn't find a post-deploy job, so we're done
  872. return nil
  873. }
  874. color.New(color.FgGreen).Printf("Post-deploy job for application %s found. Updating job to build using tag \"%s\" as well\n", args[0], appTag)
  875. overrideValues = map[string]interface{}{
  876. "image": map[string]interface{}{
  877. "tag": appTag,
  878. },
  879. }
  880. mergedValues = templaterUtils.CoalesceValues(postDeployRelease.Config, overrideValues)
  881. bytes, err = json.Marshal(mergedValues)
  882. if err != nil {
  883. return fmt.Errorf("Unable to update post-deploy job for application %s: %w", args[0], err)
  884. }
  885. err = client.UpgradeRelease(
  886. context.Background(),
  887. cliConf.Project,
  888. cliConf.Cluster,
  889. namespace,
  890. postDeployReleaseName,
  891. &types.UpgradeReleaseRequest{
  892. Values: string(bytes),
  893. },
  894. )
  895. if err != nil {
  896. return fmt.Errorf("Unable to update post-deploy job for application %s: %w", args[0], err)
  897. }
  898. return nil
  899. }