app.go 36 KB

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