app.go 37 KB

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