app.go 37 KB

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