app.go 37 KB

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