app.go 36 KB

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