app.go 37 KB

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