app.go 33 KB

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